├── README.md ├── spark-core ├── IDEA本地执行&调试Spark-Application方法.md ├── Spark---图解-Broadcast-工作原理.md ├── Spark-RPC-简述.md ├── Spark-Shuffle-模块②---Hash-Based-Shuffle-write.md ├── Spark-Storage-①---Spark-Storage-模块整体架构.md ├── Spark-Storage-②---BlockManager-的创建与注册.md ├── Spark-Storage-③---Master-与-Slave-之间的消息传递与时机.md ├── Spark-Storage-④---存储执行类介绍(DiskBlockManager、DiskStore、MemoryStore).md ├── Spark-Task-内存管理(on-heap&off-heap).md ├── Spark-Task-的执行流程①---分配-tasks-给-executors.md ├── Spark-Task-的执行流程②---创建、分发-Task.md ├── Spark-Task-的执行流程③---执行-task.md ├── Spark-Task-的执行流程④---task-结果的处理.md ├── Spark-executor-模块②---AppClient-向-Master-注册-Application.md ├── Spark-executor-模块③---启动-executor.md ├── Spark-executor-模块④---Task-的执行流程.md ├── Spark-executor模块①---主要类以及创建-AppClient.md ├── Spark-内存管理的前世今生(上).md ├── Spark-内存管理的前世今生(下).md ├── Spark-核心-RDD-剖析(上).md ├── Spark-核心-RDD-剖析(下).md ├── Spark的位置优先--TaskSetManager-的有效-Locality-Levels.md ├── [Spark源码剖析]-DAGScheduler划分stage.md ├── [Spark源码剖析]-DAGScheduler提交stage.md ├── [Spark源码剖析]-JobWaiter.md ├── [Spark源码剖析]Pool-Standalone模式下的队列.md ├── [Spark源码剖析]Spark-延迟调度策略.md ├── [Spark源码剖析]Task的调度与执行源码剖析.md ├── [图解Spark]-一张图搞懂DAGScheduler.md ├── [源码剖析]Spark读取配置.md ├── 【源码剖析】--Spark-新旧内存管理方案(上).md ├── 【源码剖析】--Spark-新旧内存管理方案(下).md ├── 举例说明Spark-RDD的分区、依赖.md └── 如何保证一个Spark-Application只有一个SparkContext实例.md ├── spark-sql ├── Spark-SQL,DataFrame以及-Datasets-编程指南---For-2-0.md ├── Spark-Sql-源码剖析(一):sql-执行的主要流程.md └── 如何让你的-Spark-SQL-查询加速数十倍?.md ├── spark-streaming ├── Spark-Streaming-+-Kakfa-编程指北.md ├── 【实战篇】如何优雅的停止你的-Spark-Streaming-Application.md ├── 【容错篇】Spark-Streaming的还原药水——Checkpoint.md ├── 【容错篇】WAL在Spark-Streaming中的应用.md ├── 为什么-Spark-Streaming-+-Kafka-无法保证-exactly-once?.md ├── 揭开Spark-Streaming神秘面纱①---DStreamGraph-与-DStream-DAG.md ├── 揭开Spark-Streaming神秘面纱②---ReceiverTracker-与数据导入.md ├── 揭开Spark-Streaming神秘面纱③---动态生成-job.md ├── 揭开Spark-Streaming神秘面纱④---job-的提交与执行.md ├── 揭开Spark-Streaming神秘面纱⑤---Block-的生成与存储.md └── 揭开Spark-Streaming神秘面纱⑥---Spark-Streaming结合-Kafka-两种不同的数据接收方式比较.md └── structured-streaming └── Structured-Streaming-编程指南.md /README.md: -------------------------------------------------------------------------------- 1 | # spark-sourcecodes-analysis 2 | Spark源码剖析 3 | -------------------------------------------------------------------------------- /spark-core/IDEA本地执行&调试Spark-Application方法.md: -------------------------------------------------------------------------------- 1 | 对于一些比较简单的application,我们可以在IDEA编码并直接以local的方式在IDEA运行。有两种方法: 2 | 3 | ##一 4 | 在创建SparkContext对象时,指定以local方式执行,如下 5 | 6 | ``` 7 | val sc = new SparkContext("local", "app name") 8 | ``` 9 | 10 | ##二 11 | 修改执行配置,如下 12 | 13 | ![](http://upload-images.jianshu.io/upload_images/204749-4e73c1b0e636edc0?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 14 | 15 | --- 16 | 17 | 当然,运行的前提是将必要的jar包在Libraries中配置好,如图: 18 | ![](http://upload-images.jianshu.io/upload_images/204749-de55f3861c012d68?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 19 | 20 | ##三 21 | 如果你还想直接在IDEA中调试spark源码,按f7进入.class后,点击 22 | ![](http://upload-images.jianshu.io/upload_images/204749-0542504740711a8c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 23 | 24 | 选择你在官网下载的与你的jar包版本一致的源码 25 | 26 | ![](http://upload-images.jianshu.io/upload_images/204749-72d5bb9d501d22e1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 27 | 28 | 之后,你就可以任意debug了~ 29 | 30 | --- 31 | 32 | 欢迎关注我的微信公众号:FunnyBigData 33 | 34 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 35 | -------------------------------------------------------------------------------- /spark-core/Spark---图解-Broadcast-工作原理.md: -------------------------------------------------------------------------------- 1 | Broadcast 是 Spark 常用的特性,本文不打算介绍什么是 Broadcast 及如何使用它,只希望能以下面这张图对 Broadcast 的基础知识和工作原理进行描述: 2 | 3 | ![](http://upload-images.jianshu.io/upload_images/204749-4c612149e189216f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 4 | 5 | --- 6 | 7 | > 参考:https://github.com/JerryLead/SparkInternals/blob/master/markdown/7-Broadcast.md 8 | 9 | --- 10 | 11 | 欢迎关注我的微信公众号:FunnyBigData 12 | 13 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 14 | -------------------------------------------------------------------------------- /spark-core/Spark-RPC-简述.md: -------------------------------------------------------------------------------- 1 | Spark 中的消息通信主要涉及 RpcEnv、RpcEndpoint 及 RpcEndpointRef 几个类,下面进行简单介绍 2 | ## RpcEnv、RpcEndpoint 及 RpcEndpointRef 3 | RPCEndpoints 定义了如何处理消息(即,使用哪个函数来处理指定消息),在通过 name 完成注册后,RpcEndpoint 就一直存放在 RpcEnv 中。RpcEndpoint 的生命周期按顺序是 ```onStart```,```receive``` 及 ```onStop```,```receive``` 可以被同时调用,如果希望 ```receive``` 是线程安全的,可以使用 ```ThreadSafeRpcEndpoint``` 4 | 5 | ```RpcEndpointRef``` 是 RpcEnv 中的 RpcEndpoint 的引用,是一个序列化的实体以便于通过网络传送或保存以供之后使用。一个 RpcEndpointRef 有一个地址和名字。可以调用 ```RpcEndpointRef``` 的 ```send``` 方法发送异步的单向的消息给对应的 RpcEndpoint 6 | 7 | RpcEnv 管理各个 RpcEndpoint 并将发送自 RpcEndpointRef 或远程节点的消息分发给对应的 RpcEndpoint。对于 RpcEnv 没有 catch 到的异常,会通过 ```RpcCallContext.sendFailure``` 将该异常发回给消息发送者或记日志 8 | 9 | ![](http://upload-images.jianshu.io/upload_images/204749-d89590e3a1c84c0d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 10 | 11 | 12 | ## RpcEnvFactory 13 | RpcEnvFactory 是构造 RpcEnv 的工厂类,调用其 ```create(config: RpcEnvConfig): RpcEnv``` 会 new 一个 RpcEnv 实例并返回。 14 | 15 | Spark 中实现了两种 RpcEnvFactory: 16 | 17 | * ```org.apache.spark.rpc.netty.NettyRpcEnvFactory``` 使用 ```netty``` 18 | * ```org.apache.spark.rpc.akka.AkkaRpcEnvFactory``` 使用 ```akka``` 19 | 20 | 其中在 Spark 2.0 已经没有了 ```AkkaRpcEnvFactory```,仅保留了 ```NettyRpcEnvFactory```。在 Spark 1.6 中可以通过设置 ```spark.rpc``` 值为 ```netty``` (默认)来使用 ```NettyRpcEnvFactory``` 或设置为 ```akka``` 来使用 ```AkkaRpcEnvFactory```,例如: 21 | 22 | ``` 23 | $ ./bin/spark-shell --conf spark.rpc=netty 24 | $ ./bin/spark-shell --conf spark.rpc=akka 25 | ``` 26 | 27 | ## RpcAddress 与 RpcEndpointAddress 28 | RpcAddress 是一个 RpcEnv 的逻辑地址,包含 hostname 和端口,RpcAddress 像 Spark URL 一样编码,比如:```spark://host:port```。RpcEndpointAddress 是向一个 RpcEnv 注册的 RpcEndpoint 的逻辑地址,包含 RpcAddress 及名字,格式如:```spark://[name]@[rpcAddress.host]:[rpcAddress.port]``` 29 | 30 | ## 参考 31 | * https://jaceklaskowski.gitbooks.io/mastering-apache-spark/content/spark-rpc.html 32 | 33 | --- 34 | 35 | 欢迎关注我的微信公众号:FunnyBigData 36 | 37 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 38 | -------------------------------------------------------------------------------- /spark-core/Spark-Shuffle-模块②---Hash-Based-Shuffle-write.md: -------------------------------------------------------------------------------- 1 | > Spark 2.0 中已经移除 Hash Based Shuffle,但作为曾经的默认 Shuffle 机制,还是值得进行分析 2 | 3 | Spark 最开始只有 Hash Based Shuffle,因为在很多场景中并不需要排序,在这些场景中多余的排序反而会损耗性能。 4 | 5 | 6 | 7 | ## Hash Based Shuffle Write 8 | 该过程实现的核心是在 ```HashShuffleWriter#write(records: Iterator[Product2[K, V]]): Unit``` 其主要流程如下: 9 | 10 | 11 | ![](http://upload-images.jianshu.io/upload_images/204749-c434b3b8b6fcecec.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 12 | 13 | 14 | 该函数的输入是一个 Shuffle Map Task 计算得到的结果(对应的迭代器),若在宽依赖中定义了 map 端的聚合则会先进行聚合,随后对于迭代器(若要聚合则为聚合后的迭代器)的每一项先通过计算 key 的 hash 值来确定要写到哪个文件,然后将 key、value 写入文件。 15 | 16 | 写入的文件名的格式是:```shuffle_$shuffleId_$mapId_$reduceId```。写入时,若文件已存在会删除会创建新文件。 17 | 18 | 上图描述了如何处理一个 Shuffle Map Task 计算结果,在实际应用中,往往有很多 Shuffle Map Tasks 及下游 tasks,即如下情况(图摘自:[JerryLead/SparkInternals-Shuffle 过程](https://github.com/JerryLead/SparkInternals/blob/master/markdown/4-shuffleDetails.md)): 19 | 20 | 21 | ![](http://upload-images.jianshu.io/upload_images/204749-453334071616f4a1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 22 | 23 | 24 | ### 存在的问题 25 | 这种简单的实现会有几个问题,为说明方便,这里设 ```M = Shuffle Map Task 数量```,```R = 下游 tasks 数量```: 26 | 27 | * 产生过多文件:由于每个 Shuffle Map Task 需要为每个下游的 Task 创建一个单独的文件,因此文件的数量就是 ```M * R```。如果 Shuffle Map Tasks 数量是 1000,下游的 tasks 数是 800,那么理论上会产生 80w 个文件(对于 size 为 0的文件会特殊处理) 28 | * 打开多个文件对于系统来说意味着随机写,尤其是每个文件较小且文件特别多的情况。机械硬盘在随机读写方面的性能很差,如果是固态硬盘,会改善很多 29 | * 缓冲区占用内存空间大:每个 Shuffle Map Task 需要开 R 个 bucket(为减少写文件次数的缓冲区),N 个 Shuffle Map Task 就会产生 ```N * R``` 个 bucket。虽然一个 Shuffle Map Task,对应的 buckets 会被回收,但一个节点上的 bucket 个数最多可以达到 ```cores * R``` 个,每个 bucket 默认为 32KB。对于 24 核 1000 个 reducer 来说,占用内存就是 750MB 30 | 31 | ## 改进:Shuffle Consolidate Writer 32 | 在上面提到的几个问题,Spark 提供了 Shuffle Consolidate Files 机制进行优化。该机制的手段是减少 Shuffle 过程产生的文件,若使用这个功能,则需要置 ```spark.shuffle.consolidateFiles``` 为 ```true```,其实现可用下图来表示(图摘自:[JerryLead/SparkInternals-Shuffle 过程](https://github.com/JerryLead/SparkInternals/blob/master/markdown/4-shuffleDetails.md)) 33 | 34 | 35 | ![](http://upload-images.jianshu.io/upload_images/204749-3a09e95b720e2a31.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 36 | 37 | 38 | 即:对于运行在同一个 core 的 Shuffle Map Tasks,对于将要被同一个 reducer read 的数据,第一个 Shuffle Map Task 会创建一个文件,之后的就会将数据追加到这个文件而不是新建一个文件(相当于同一个 core 上的 Shuffle Map Task 写了文件不同的部分)。因此文件数就从原来的 ```M * R``` 个变成了 ```cores * R``` 个。当 ```M / cores``` 的值越大,减少文件数的效果越显著。需要注意的是,该机制虽然在很多时候能缓解上述的几个问题,但是并不能彻底解决。 39 | 40 | ## 参考 41 | * 《Spark 技术内幕》 42 | * [JerryLead/SparkInternals - Shuffle 过程](https://github.com/JerryLead/SparkInternals/blob/master/markdown/4-shuffleDetails.md) 43 | 44 | --- 45 | 46 | 欢迎关注我的微信公众号:FunnyBigData 47 | 48 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 49 | -------------------------------------------------------------------------------- /spark-core/Spark-Storage-①---Spark-Storage-模块整体架构.md: -------------------------------------------------------------------------------- 1 | > 本文为 Spark 2.0 源码分析笔记,某些实现可能与其他版本有所出入 2 | 3 | Storage 模块在整个 Spark 中扮演着重要的角色,管理着 Spark Application 在运行过程中产生的各种数据,包括基于磁盘和内存的,比如 RDD 缓存,shuffle 过程中缓存及写入磁盘的数据,广播变量等。 4 | 5 | Storage 模块也是 Master/Slave 架构,Master 是运行在 driver 上的 BlockManager实例,Slave 是运行在 executor 上的 BlockManager 实例。 6 | 7 | Master 负责: 8 | 9 | * 接受各个 Slaves 注册 10 | * 保存整个 application 各个 blocks 的元数据 11 | * 给各个 Slaves 下发命令 12 | 13 | Slave 负责: 14 | 15 | * 管理存储在其对应节点内存、磁盘上的 Blocks 数据 16 | * 接收并执行 Master 的命令 17 | * 更新 block 信息给 Master 18 | 19 | 整体架构图如下(包含1个 Master 和4个 Slaves): 20 | 21 | 22 | ![Storage 模块 Master Slaves 架构.jpg](http://upload-images.jianshu.io/upload_images/204749-02771855cda616cb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 23 | 24 | 25 | 26 | 在 driver 端,创建 SparkContext 时会创建 driver 端的 SparkEnv,在构造 SparkEnv 时会创建 BlockManager,而该 BlockManager 持有 RpcEnv 和 BlockManagerMaster。其中,RpcEnv 包含 driverRpcEndpoint 和各个 Slave 的 rpcEndpointRef(存储在```blockManagerInfo: mutable.HashMap[BlockManagerId, BlockManagerInfo]``` 中,BlockManagerInfo 包含对应 Slave 的 rpcEndpointRef),Storage Master 就是通过这些 Slaves 的 rpcEndpointRef 来给 Storage Slave 发送消息下达命令的 27 | 28 | 而在 slave 端(各个 executor),同样会创建 SparkEnv,创建 SparkEnv 时同样会创建 BlockManager,slave 端的 BlockManager 同样会持有 RpcEnv 以及 BlockManagerMaster。不同的是,slave 端的 RpcEnv 包含了 slaveRpcEndpoint 而 BlockManagerMaster 持有 driverRpcEndpoint, Storage Slave 就是通过 driverRpcEndpoint 来给 Storage Master 发送消息的 29 | 30 | 好,基于上图和相应的文字说明相信能对 Spark Storage 模块的整体架构有个大致的了解,更深入的分析将在之后的文章中进行~ 31 | 32 | --- 33 | 34 | 欢迎关注我的微信公众号:FunnyBigData 35 | 36 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 37 | -------------------------------------------------------------------------------- /spark-core/Spark-Storage-②---BlockManager-的创建与注册.md: -------------------------------------------------------------------------------- 1 | > 本文为 Spark 2.0 源码分析笔记,某些实现可能与其他版本有所出入 2 | 3 | [上一篇文章](http://www.jianshu.com/p/730eed6a98d2)介绍了 Spark Storage 模块的整体架构,本文将着手介绍在 Storeage Master 和 Slave 上发挥重要作用的 BlockManager 是在什么时机以及如何创建以及注册的。接下来分别介绍 Master 端和 Slave 端的 BlockManager。 4 | 5 | 为了方便阅读,后文中将以 Master 作为 Storage Master(driver) 端的 BlockManager 的简称,以 Slave 作为 Storage Slave(executor) 端的 BlockManager 的简称。 6 | 7 | ## BlockManager 创建时机 8 | ### Master 创建时机 9 | 在 driver 端,构造 SparkContext 时会创建 SparkEnv 实例 _env,创建 _env 是通过调用 object SparkEnv 的 create 方法,在该方法中会创建 Master,即 driver 端的 blockManager。 10 | 11 | 所以,简单来说,Master 是在 driver 创建 SparkContext 时就创建了。 12 | 13 | ### Slave 创建时机 14 | 在 worker 进程起来的的时候,```object CoarseGrainedExecutorBackend``` 初始化时会通过调用 ```SparkEnv#createExecutorEnv```,在该函数中会创建 executor 端的 BlockManager,也即 Slave。这之后,CoarseGrainedExecutorBackend 才向 driver 注册 executor,然后再构造 Executor 实例。 15 | 16 | 接下来,我们看看 BlockManager 是如何创建的。 17 | 18 | ## 创建 BlockManager 19 | 一图胜千言,我们还是先来看看 Master 是如何创建的: 20 | 21 | 22 | ![图1: 创建 BlockManage](http://upload-images.jianshu.io/upload_images/204749-fc5dbb6906d8b93e.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 23 | 24 | 25 | 26 | 结合上图我们来进行 Step By Step 的分析 27 | 28 | ### Step1: 创建 RpcEnv 实例 rpcEnv 29 | 这一步通过 systemName、hostname、port 等创建一个 RpcEnv 类型实例 rpcEnv,更具体的说是一个 NettRpcEnv 实例,在 Spark 2.0 中已经没有 akka rpc 的实现,该 rpcEnv 实例用于: 30 | 31 | * 接受稍后创建的 rpcEndpoint 的注册并持有 rpcEndpoint(该 rpcEndpoint 用于接收对应的 rpcEndpointRef 发送的消息以及将消息指派给相应的函数处理) 32 | * 持有一个消息分发器 ```dispatcher: Dispatcher```,将接收到的消息分发给相应的 rpcEndpoint 处理 33 | 34 | ### Step2: 创建 BlockManagerMaster 实例 blockManagerMaster 35 | BlockManagerMaster 持有 driverRpcEndpointRef,其包含各种方法通过该 driverRpcEndpointRef 来给 Master 发送各种消息来实现注册 BlockManager、移除 block、获取/更新 block、移除 Broadcast 等功能。 36 | 37 | 如上图所示,创建 BlockManagerMaster 的流程如下: 38 | 39 | 1. 先创建 BlockManagerMasterEndpoint 实例 40 | 2. 对于 master(on driver),将上一步得到的 blockManagerMasterEndpoint 注册到 driverRpcEnv,以供之后driverRpcEnv 中的消息分发器分发消息给它来处理特定的消息,并返回 driverRpcEndpointRef;而对于 slave(on executor),通过 driverHost、driverPort 获取 driverRpcEndpointRef 41 | 3. 利用上一步构造的 driverRpcEndpointRef,结合 sparkConf 及是否是 driver 标记来构造 BlockManagerMaster 实例 42 | 43 | ### Step3: 创建 BlockManager 实例 44 | 结合 Step1 中创建的 rpcEnv,Step2 中创建的 blockManagerMaster 以及 executorId、memoryManager、mapOutputTracker、shuffleManager 等创建 BlockManager 实例。该 BlockManager 也就是 Storage 模块的 Master 或 Slave 了。 45 | 46 | BlockManager 运行在所有的节点上,包括 driver 和 executor,用来存取在本地或远程节点上的 blocks,blocks 可以是在内存中、磁盘上火对外内存中。 47 | 48 | ## 注册 BlockManager 49 | BlockManager 实例在被创建后,不能直接使用,必须调用其 ```initialize``` 方法才能使用。对于 Master,是在 BlockManager 创建后就调用了 ```initialize``` 方法;对于 Slave,是在 Executor 的构造函数中调用 ```initialize``` 方法进行初始化。 50 | 51 | 在 ```initialize``` 方法中,会进行 BlockManager 的注册,具体操作时通过 driverRpcEndpointRef 发送 ```RegisterBlockManager``` 消息 52 | 53 | --- 54 | 55 | 欢迎关注我的微信公众号:FunnyBigData 56 | 57 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 58 | -------------------------------------------------------------------------------- /spark-core/Spark-Storage-③---Master-与-Slave-之间的消息传递与时机.md: -------------------------------------------------------------------------------- 1 | > 本文为 Spark 2.0 源码分析笔记,某些实现可能与其他版本有所出入 2 | 3 | 再次重申标题中的 Master 是指 Spark Storage 模块的 Master,是运行在 driver 上的 BlockManager 及其包含的 BlockManagerMaster、RpcEnv 及 RpcEndpoint 等;而 Slave 则是指 Spark Storage 模块的 Slave,是运行在 executor 上的 BlockManager 及其包含的 BlockManagerMaster、RpcEnv 及 RpcEndpoint 等。下文也将沿用 Master 和 Slave 简称。 4 | 5 | Master 与 Slaves 之间是通过消息进行通信的,本文将分析 Master 与 Slaves 之间重要的消息以及这些消息是在什么时机被触发发送的。 6 | 7 | ## Master -> Slave 8 | 先来看看 Master 都会发哪些消息给 Slave 9 | 10 | ### case class RemoveBlock(blockId: BlockId) 11 | 用于移除 slave 上的 block。在以下两个时机会触发: 12 | 13 | * task 结束时 14 | * Spark Streaming 中,清理过期的 batch 对应的 blocks 15 | 16 | --- 17 | 18 | ### case class RemoveRdd(rddId: Int) 19 | 用于移除归属于某个 RDD 的所有 blocks,触发时机: 20 | 21 | * 释放缓存的 RDD 22 | 23 | --- 24 | 25 | ### case class RemoveShuffle(shuffleId: Int) 26 | 用于移除归属于某次 shuffle 所有的 blocks,触发时机: 27 | 28 | * 做 shuffle 清理的时候 29 | 30 | --- 31 | 32 | ### case class RemoveBroadcast(broadcastId: Long, removeFromDriver: Boolean = true) 33 | 用于移除归属于特定 Broadcast 的所有 blocks。触发时机: 34 | 35 | * 调用 ```Broadcast#destroy``` 销毁广播变量 36 | * 调用 ```Broadcast#unpersist``` 删除 executors 上的广播变量拷贝 37 | 38 | 接下来看看 Slaves 发送给 Master 的消息 39 | 40 | ## Slave -> Master 41 | ### case class RegisterBlockManager(blockManagerId: BlockManagerId ...) 42 | 用于 Slave(executor 端 BlockManager) 向 Master(driver 端 BlockManager) 注册,触发时机: 43 | 44 | * executor 端 BlockManager 在初始化时 45 | 46 | --- 47 | 48 | ### case class UpdateBlockInfo(var blockManagerId: BlockManagerId, var blockId: BlockId ...) 49 | 用于向 Master 汇报指定 block 的信息,包括:storageLevel、存储在内存中的 size、存储在磁盘上的 size、是否 cached 等。触发时机: 50 | 51 | * BlockManager 注册时 52 | * block 被移除时 53 | * 原本存储在内存中的 block 因内存不足而转移到磁盘上时 54 | * 生成新的 block 时 55 | 56 | --- 57 | 58 | ### case class GetLocations(blockId: BlockId) 59 | 用于获取指定 blockId 的 block 所在的 BlockManagerId 列表,触发时机: 60 | 61 | * 检查是否包含某个 block 62 | * 以序列化形式读取本地或远程 BlockManagers 上的数据时 63 | * 读取以 blocks 形式存储的 task result 时 64 | * 读取 Broadcast blocks 数据时 65 | * 获取指定 block id 对应的 block 数据(比如获取 RDD partition 对应的 block) 66 | 67 | --- 68 | 69 | ### case class RemoveExecutor(execId: String) 70 | 用于移除已 lost 的 executor 上的 BlockManager(只在 driver 端进行操作),触发时机: 71 | 72 | * executor lost(一般由于 task 连续失败导致) 73 | 74 | --- 75 | 76 | ### case object StopBlockManagerMaster 77 | 用于停止 driver 或 executor 端的 BlockManager,触发时机: 78 | 79 | * SparkContext#stop 被调用时,也即 driver 停止时 80 | 81 | --- 82 | 83 | ### case object GetMemoryStatus 84 | 用于获取各个 BlockManager 的内存使用情况,包括最大可用内存以及当前可用内存(当前可用内存=最大可用内存-已用内存) 85 | 86 | --- 87 | 88 | ### case object GetStorageStatus 89 | 用于获取各个 BlockManager 的存储状态,包括每个 BlockManager 中都存储了哪些 RDD 的哪些 block(对应 partition)以及各个 block 的信息 90 | 91 | --- 92 | 93 | ### case class BlockManagerHeartbeat(blockManagerId: BlockManagerId) 94 | 用于 Slave 向 Master 发心跳信息,以通知 Master 其上的某个 BlockManager 还存活着 95 | 96 | --- 97 | 98 | ### case class HasCachedBlocks(executorId: String) 99 | 用于检查 executor 是否有缓存 blocks(广播变量的 blocks 不作考虑,因为广播变量的 block 不会汇报给 Master),触发时机: 100 | 101 | * 检验某个 executor 是否闲置了一段时间,即一段时间内没有运行任何 tasks(这样的 executor 会慢慢被移除) 102 | 103 | --- 104 | 105 | 欢迎关注我的微信公众号:FunnyBigData 106 | 107 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 108 | -------------------------------------------------------------------------------- /spark-core/Spark-Storage-④---存储执行类介绍(DiskBlockManager、DiskStore、MemoryStore).md: -------------------------------------------------------------------------------- 1 | > 本文为 Spark 2.0 源码分析笔记,某些实现可能与其他版本有所出入 2 | 3 | 这篇文章前半部分我们对直接在 Block 存取发挥重要作用的类进行介绍,主要是 DiskBlockManager、MemoryStore、DiskStore。后半部分以存取 Broadcast 来进一步加深对 Block 存取的理解。 4 | 5 | ## DiskBlockManager 6 | DiskBlockManager 主要用来创建并持有逻辑 blocks 与磁盘上的 blocks之间的映射,一个逻辑 block 通过 BlockId 映射到一个磁盘上的文件。 7 | 8 | ### 主要成员 9 | * ```localDirs: Array[File]```:创建根据 ```spark.local.dir``` (备注①)指定的目录列表,这些目录下会创建子目录,这些子目录用来存放 Application 运行过程中产生的存放在磁盘上的中间数据,比如 cached RDD partition 对应的 block、Shuffle Write 产生的数据等,会根据文件名将 block 文件 hash 到不同的目录下 10 | * ```subDirs: Array.fill(localDirs.length)(new Array[File](subDirsPerLocalDir))```:localDirs 代表的各个目录下的子目录,子目录个数由 ```spark.diskStore.subDirectories``` 指定,子目录用来存储具体的 block 对应的文件,会根据 block file 文件名先 hash 确定放在哪个 localDir,在 hash 决定放在该 localDir 的哪个子目录下(寻找该 block 文件也是通过这种方式) 11 | * ```shutdownHook = addShutdownHook()```:即关闭钩子,在进程结束时会递归删除 localDirs 下所有属于该 Application 的文件 12 | 13 | ### 主要方法 14 | 看了上面几个主要成员的介绍相信已经对逻辑 block 如何与磁盘文件映射已经有了大致了解。接下来看看几个主要的方法: 15 | 16 | * ```getFile(filename: String): File```:通过文件名来查找 block 文件并获取文件句柄,先通过文件名 hash 到指定目录再查找 17 | * ```getFile(blockId: BlockId): File```:通过 blockId 来查找 block 文件并获取文件句柄,事实上是通过调用 ```getFile(filename: String): File``` 来查找的 18 | * ```containsBlock(blockId: BlockId): Boolean```:是否包含某个 blockId 对应的文件 19 | * ```getAllFiles(): Seq[File]```:获取存储在磁盘上所有 block 文件的句柄,以列表的形式返回 20 | * ```getAllBlocks(): Seq[BlockId]```:获取存储在磁盘上的所有 blockId 21 | * ```stop(): Unit```:清理存储在磁盘上所有的 block 文件 22 | * ```createTempLocalBlock(): (TempLocalBlockId, File)```:产生一个唯一的 Block Id 和文件句柄用于存储本地中间结果 23 | * ```createTempShuffleBlock(): (TempShuffleBlockId, File)```:产生一个唯一的 Block Id 和文件句柄用于存储 shuffle 中间结果 24 | 25 | 如上述,DiskBlockManager 提供的方法主要是为了提供映射的方法,而并不会将现成的映射关系保存在某个成员中,这是需要明了的一点。DiskBlockManager 方法主要在需要创建或获取某个 block 对应的磁盘文件以及在 BlockManager 退出时要清理磁盘文件时被调用。 26 | 27 | --- 28 | 29 | ## DiskStore 30 | DiskStore 用来将 block 数据存储至磁盘,是直接的磁盘文件操作者。其封装了: 31 | 32 | ### 两个写方法 33 | * ```put(blockId: BlockId)(writeFunc: FileOutputStream => Unit): Unit```:用文件输出流的方式写 block 数据至磁盘 34 | * ```putBytes(blockId: BlockId, bytes: ChunkedByteBuffer): Unit```:以字节 buffer 的方式写 block 数据至磁盘 35 | 36 | ### 一个读方法 37 | * ```getBytes(blockId: BlockId): ChunkedByteBuffer```:通过 block id 读取存储在磁盘上的 block 数据,以字节 buffer 的形式返回 38 | 39 | ### 两个查方法 40 | * ```getSize(blockId: BlockId): Long```:通过 block id 获取存储在磁盘上的 block 数据的大小 41 | * ```contains(blockId: BlockId): Boolean```:查询磁盘上是否包含某个 block id 的数据 42 | 43 | ### 一个删方法 44 | * ```remove(blockId: BlockId): Boolean```:删除磁盘上某个 block id 的数据 45 | 46 | 需要说明的是,DiskStore 的各个方法中,通过 block id 或文件名来找到对应的 block 文件句柄是通过调用 DiskBlockManager 的方法来达成的 47 | 48 | --- 49 | 50 | ## MemoryStore 51 | MemoryStore 用来将没有序列化的 Java 对象数组和序列化的字节 buffer 存储至内存中。它的实现比 DiskStore 稍复杂,我们先来看看主要成员 52 | 53 | 先说明 ```MemoryEntry```: 54 | 55 | ``` 56 | private sealed trait MemoryEntry[T] { 57 | def size: Long 58 | def memoryMode: MemoryMode 59 | def classTag: ClassTag[T] 60 | } 61 | 62 | public enum MemoryMode { 63 | ON_HEAP, 64 | OFF_HEAP 65 | } 66 | ``` 67 | 68 | 代表 JVM 或对外内存的内存大小 69 | 70 | ### 主要成员 71 | * ```entries: LinkedHashMap[BlockId, MemoryEntry[_]]```:保存每个 block id 及其存储在内存中的数据的大小及是保存在 JVM 内存中还是堆外内存中 72 | * ```unrollMemoryMap: mutable.HashMap[Long, Long]```:保存每个 task 占用的用来存储 block 而占用的 JVM 内存 73 | * ```offHeapUnrollMemoryMap: mutable.HashMap[Long, Long]```:保存每个 task 占用的用来存储 block 而占用的对外内存 74 | 75 | 以上几个成员主要描述了每个 block 占用了多少内存空间,每个 task 占用了多少内存空间以及它们占用的是 JVM 内存还是堆外内存。接下来看看几个重要的方法: 76 | 77 | ### 三个写方法 78 | * ```putBytes[T: ClassTag](blockId: BlockId, size: Long, memoryMode: MemoryMode, _bytes: () => ChunkedByteBuffer): Boolean```:先检查是否还有空余内存来存储参数 size 这么大的 block,若有则将 block 以字节 buffer 形式存入;否则不存入,返回失败 79 | * ```putIteratorAsValues[T](blockId: BlockId, values: Iterator[T], classTag: ClassTag[T]): Either[PartiallyUnrolledIterator[T], Long]```:尝试将参数 blockId 对应的数据通过迭代器的方式写入内存。为避免由于空余内存不足以存放 block 数据而导致的 OOM。该方法会逐步展开迭代器来检查是否还有空余内存。如果迭代器顺利展开了,那么用来展开迭代器的内存直接转换为存储内存,而不用再去分配内存来存储该 block 数据。如果未能完全开展迭代器,则返回一个包含 block 数据的迭代器,其对应的数据是由多个局部块组合而成的 block 数据 80 | * ```putIteratorAsBytes[T](blockId: BlockId, values: Iterator[T], classTag: ClassTag[T], memoryMode: MemoryMode): Either[PartiallySerializedBlock[T], Long]```:尝试将参数 blockId 对应的数据通过字节 buffer 的方式写入内存。为避免由于空余内存不足以存放 block 数据而导致的 OOM。该方法会逐步展开迭代器来检查是否还有空余内存。如果迭代器顺利展开了,那么用来展开迭代器的内存直接转换为存储内存,而不用再去分配内存来存储该 block 数据。如果未能完全开展迭代器,则返回一个包含 block 数据的迭代器,其对应的数据是由多个局部块组合而成的 block 数据 81 | 82 | ### 两个读方法 83 | * ```getBytes(blockId: BlockId): Option[ChunkedByteBuffer]```:以字节 buffer 的形式获取参数 blockId 指定的 block 数据 84 | * ```getValues(blockId: BlockId): Option[Iterator[_]]```:以迭代器的形式获取参数 blockId 指定的 block 数据 85 | 86 | ### 若干个查方法 87 | * ```getSize(blockId: BlockId): Long```:获取 blockId 对应 block 占用的内存大小 88 | * ```contains(blockId: BlockId): Boolean```:内存中是否包含某个 blockId 对应的 block 数据 89 | * ```currentUnrollMemory(): Long```:当前所有 tasks 用于存储 blocks 占用的总内存 90 | * ... 91 | 92 | ### 两个删方法 93 | * ```remove(blockId: BlockId): Boolean```:删除内存中 blockId 指定的 block 数据 94 | * ```clear(): Unit```:清除 MemoryStore 中存储的所有 blocks 数据 95 | 96 | 从上面描述的 MemoryStore 的主要方法来看,其功能和 DiskStore 类似,但由于要考虑到 JVM 内存和堆外内存以及有可能内存不足以存储 block 数据等问题会变得更加复杂 97 | 98 | --- 99 | 100 | ## 备注说明 101 | * 备注①:设置 ```spark.local.dir``` 时可以设置多个目录,目录分别在不同磁盘上,可以增加整体 IO 带宽;也尽量让目录位于更快的磁盘上以获得更快的 IO 速度 102 | 103 | --- 104 | 105 | 欢迎关注我的微信公众号:FunnyBigData 106 | 107 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 108 | -------------------------------------------------------------------------------- /spark-core/Spark-Task-内存管理(on-heap&off-heap).md: -------------------------------------------------------------------------------- 1 | > 本文为 Spark 2.0 源码分析,其他版本可能会有所不同 2 | 3 | 在之前的文章中([Spark 新旧内存管理方案(上)](http://www.jianshu.com/p/2e9eda28e86c)及[Spark 新旧内存管理方案(下)](http://www.jianshu.com/p/bb0bdcb26ccc)),我从粗粒度上对 Spark 内存管理进行了剖析,但我们依然会有类似这样的疑问,在 task 中,shuffle 时使用的内存具体是怎么分配的?是在堆上分配的还是堆外分配的?堆上如何分配、堆外又如何分配? 4 | 5 | 这些问题可以通过剖析 TaskMemoryManager 来解决。TaskMemoryManager 用来管理一个 task 的内存,主要涉及申请内存、释放内存及如何表示统一的表示从堆或堆外申请的一块固定大小的连续的内存 6 | 7 | ### 统一的内存块表示 - MemoryBlock 8 | 9 | 对于堆内存,分配、释放及对象引用关系都由 JVM 进行管理。new 只是返回一个对象引用,而不是该对象在进程地址空间的地址。堆内存的使用严重依赖 JVM 的 GC 器,对于大内存的使用频繁的 GC 经常会对性能造成很大影响。 10 | 11 | Java 提供的 ByteBuffer.allocateDirect 方法可以分配堆外内存,其分配大小受 ```MaxDirectMemorySize``` 配置限制。另一种分配堆外内存的方法就是 Unsafe 的 ```allocateMemory``` 方法,相比前者,它完全脱离了 JVM 限制,与 C 中的 malloc 功能一致。这两个方法还有另一个区别:后者返回的是进程空间的实际内存地址,而前者被 ByteBuffer 进行包装。 12 | 13 | 堆内内存使用简单,但在使用大内存时其 GC 机制容易影响性能;堆外内存相交于堆内存使用复杂,但精确的内存控制使其更高效。在 Spark 中,很多地方会有大数组大内存的需求,高效的内存使用时必须的,因此 Spark 也提供了堆外内存的支持,以优化 Application 运行性能。 14 | 15 | Spark 封装了 ```MemoryLocation``` 来表示一个逻辑内存地址,其定义如下: 16 | 17 | ``` 18 | public class MemoryLocation { 19 | Object obj; 20 | long offset; 21 | 22 | //< 适用于堆内内存 23 | public MemoryLocation(@Nullable Object obj, long offset) { 24 | this.obj = obj; 25 | this.offset = offset; 26 | } 27 | 28 | //< 适用于堆外内存 29 | public MemoryLocation() { 30 | this(null, 0); 31 | } 32 | 33 | ... 34 | } 35 | 36 | ``` 37 | 38 | 以及 MemoryBlock 来表示一块连续的内存,这块内存可以从堆或对外分配,包含以下成员: 39 | 40 | * length:内存块大小 41 | * pageNumber:page id(这块内存又被叫做 page) 42 | * obj:见下文分析 43 | * offset:见下文分析 44 | 45 | ``` 46 | public class MemoryBlock extends MemoryLocation { 47 | 48 | private final long length; 49 | public int pageNumber = -1; 50 | 51 | public MemoryBlock(@Nullable Object obj, long offset, long length) { 52 | super(obj, offset); 53 | this.length = length; 54 | } 55 | 56 | ... 57 | } 58 | ``` 59 | 60 | 接下来我们来看看如何从堆内和堆外申请内存并生成对应的 MemoryBlock 对象。 61 | 62 | #### 申请堆外内存 63 | Spark 封装了 ```UnsafeMemoryAllocator``` 类来分配和释放堆外内存,分配的方法如下: 64 | 65 | ``` 66 | public MemoryBlock allocate(long size) throws OutOfMemoryError { 67 | long address = Platform.allocateMemory(size); 68 | return new MemoryBlock(null, address, size); 69 | } 70 | ``` 71 | 72 | 其中 ```Platform.allocateMemory(size)``` 会调用 ```Unsafe.allocateMemory``` 来从堆外分配一块 size 大小的内存并返回其绝对地址。随后,构造并返回 MemoryBlock 对象,需要注意的是,**该对象的 obj 成员为 ```null```,offset 成员为该绝对地址** 73 | 74 | #### 申请堆内存 75 | Spark 封装了 ```HeapMemoryAllocator``` 类分配和释放堆内存,分配的方法如下: 76 | 77 | ``` 78 | public MemoryBlock allocate(long size) throws OutOfMemoryError { 79 | ... 80 | long[] array = new long[(int) ((size + 7) / 8)]; 81 | return new MemoryBlock(array, Platform.LONG_ARRAY_OFFSET, size); 82 | } 83 | ``` 84 | 85 | 总共分为两步: 86 | 87 | 1. 以8字节对齐的方式申请长度为 ```((size + 7) / 8)``` 的 long 数组,得到 array 88 | 2. 构造 MemoryBlock 对象,**其 obj 成员为 array,offset 成员为 ```Platform.LONG_ARRAY_OFFSET```** 89 | 90 | ## Page table 91 | 在 TaskMemoryManager 有一个如下成员: 92 | 93 | ``` 94 | private final MemoryBlock[] pageTable = new MemoryBlock[8192]; 95 | ``` 96 | 97 | 该成员保存着一个 task 所申请的所有 pages(page 即 MemoryBlock),最多可以有8192个。在 ```TaskMemoryManager#allocatePage(...)``` 中从堆或堆外分配的 page 会被添加到该 pageTable 中。 98 | 99 | ## 对 page 地址进行统一编码 100 | 通过上面的分析我们知道,page 对应的内存可能来自堆或堆外。但这显然不应该由上层操作者来操心,所以 ```TaskMemoryManager``` 提供了只需传入 page 及要访问该 page 上的 offset 就能获得一个 long 型的地址。这样应用者只需操作自该地址起的某一段内存即可,而不用关心这块内存是来自哪。这即是 ```TaskMemoryManager``` 提供的 page 地址统一编码,由 ```TaskMemoryManager#encodePageNumberAndOffset(MemoryBlock page, long offsetInPage): long``` 实现 101 | 102 | ## 参考 103 | 104 | * http://blog.csdn.net/lipeng_bigdata/article/details/50752297 105 | * http://www.jianshu.com/p/34729f9f833c 106 | * https://github.com/ColZer/DigAndBuried/blob/master/spark/spark-memory-manager.md 107 | 108 | --- 109 | 110 | 欢迎关注我的微信公众号:FunnyBigData 111 | 112 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 113 | -------------------------------------------------------------------------------- /spark-core/Spark-Task-的执行流程①---分配-tasks-给-executors.md: -------------------------------------------------------------------------------- 1 | > 本文为 Spark 2.0 版本的源码分析,其他版本可能会有所不同 2 | 3 | TaskScheduler 作为资源调度器的一个重要职责就在: 4 | 5 | * 集群可用资源发生变化(比如有新增的 executor,有 executor lost 等) 6 | * 有新的 task 提交 7 | * 有 task 结束 8 | * 处理 Speculatable task 9 | 10 | 等时机把处于等待状态的 tasks 分配给有空闲资源的 executors,那么这个 “把 task 分配给 executor” 的过程具体是怎样的呢?这就是本文要探讨的内容,将通过以下四小节来进行剖析: 11 | 12 | 1. 打散可用的 executors 13 | 2. 对所有处于等待状态的 taskSet 进行排序 14 | 3. 根据是否有新增的 executor 来决定是否更新各个 taskSet 的可用本地性集合 15 | 4. 结合 taskSets 的排序及本地性集合将 tasks 分配给 executors 16 | 17 | ## 打散可用的 executors 18 | “把 task 分配给 executor” 这一过程是在函数 ```TaskSchedulerImpl#resourceOffers(offers: Seq[WorkerOffer]): Seq[Seq[TaskDescription]]``` 中完成的: 19 | 20 | * 传入参数 ```offers: Seq[WorkerOffer]``` 为集群中所有 activeExecutors 一一对应,一个 WorkerOffer 包含的信息包括:executorId、executorHost及该 executor 空闲的 cores 21 | * 返回值类型为 ```Seq[Seq[TaskDescription]]```,其每一个元素(即```Seq[TaskDescription]```类型)为经过该函数的调度分配给下标相同的 offers 元素对应的 executor 的 tasks 22 | 23 | ```TaskSchedulerImpl#resourceOffers``` 第一阶段做的事可用下图表示: 24 | 25 | 26 | ![](http://upload-images.jianshu.io/upload_images/204749-0d9a0e011193cdf9.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 27 | 28 | 29 | 30 | 在该函数每次被调用之时,通过随机的方式打乱所有 workerOffers(一个 workerOffer 对应一个active executor),之后会根据这打乱后的顺序给 executor 分配 task,这样做就能避免只将 tasks 分配给少数几个 executors 从而达到使集群各节点压力平均的目的。 31 | 32 | 除了打散 workerOffers,还为每个 workerOffer 创建了一个可变的 TaskDescription 数组从而组成了一个二维数组 tasks,用于保存之后的操作中分配给各个 executor 的 tasks。 33 | 34 | ## 对所有处于等待状态的 taskSet 进行排序 35 | 排序的目的是为了让优先级更高的 taskSet 所包含的 task 更优先的被调度执行,所执行的操作是: 36 | 37 | ``` 38 | val sortedTaskSets: ArrayBuffer[TaskSetManager] = rootPool.getSortedTaskSetQueue 39 | ``` 40 | 41 | 其中,```sortedTaskSets``` 是排序后得到的 ```TaskSetManager``` 数组,下标越小表示优先级越高,也就越优先被调度。而 ```rootPool``` 是 ```Pool``` 类型,它是 Standalone 模式下的对列,支持两种调度模式,分别是: 42 | 43 | * SchedulingMode.FIFO:FIFO 模式,先进先出 44 | * SchedulingMode.FAIR:公平模式,会考虑各个对列资源的使用情况 45 | 46 | 更具体的分析,请移步[Pool-Standalone模式下的队列](http://www.jianshu.com/p/f56c3fb989ad),这篇文章对两种调度方式以及如何排序做做了十分详细的说明 47 | 48 | ## 根据是否有新增的 executor 来决定是否更新各个 taskSet 的可用本地性集合 49 | 关于更新 taskSet 的可用本地性集合,这里值进行简单说明,更多内容请移步 [Spark的位置优先: TaskSetManager 的有效 Locality Levels](http://www.jianshu.com/p/05034a9c8cae) 50 | 51 | * 若 taskSet 中有 task 的 partition 是存储在 executor 内存中的且对应 executor alive,那么该 taskSet 的最佳本地性为 ```PROCESS_LOCAL```,可用本地性集合包括 ```PROCESS_LOCAL``` 及所有本地性比 ```PROCESS_LOCAL``` 查的,也就是该集合包括 ```PROCESS_LOCAL, NODE_LOCAL, NO_PREF, RACK_LOCAL, ANY``` 52 | * 若 taskSet 中没有 task 的 partition 是存储在 executor 内存中的,但存在 partition 是存储在某个节点磁盘上的且对应节点 alive ,那么该 taskSet 的最佳本地性为 ```NODE_LOCAL```,可用本地性集合包括 ```NODE_LOCAL``` 及所有本地性比 ```NODE_LOCAL``` 查的,也就是该集合包括 ```NODE_LOCAL, NO_PREF, RACK_LOCAL, ANY``` 53 | * 以此类推,可用本地性集合包含 taskSet 中的 tasks 所拥有的最佳本地性及所有比该本地性差的本地性 54 | 55 | 这个可用本地性集合会在后面的将 task 分配给 executor 起关键作用 56 | 57 | ## 结合 taskSets 的排序及本地性集合将 tasks 分配给 executors 58 | 这一步的实现代码如下: 59 | 60 | ``` 61 | for (taskSet <- sortedTaskSets; maxLocality <- taskSet.myLocalityLevels) { 62 | do { 63 | launchedTask = resourceOfferSingleTaskSet( 64 | taskSet, maxLocality, shuffledOffers, availableCpus, tasks) 65 | } while (launchedTask) 66 | } 67 | ``` 68 | 69 | 含义是根据 sortedTaskSets 的顺序依次遍历其每一个 taskSetManager, 70 | 从该 taskSetManager 的本地性从高到低依次调用 ```TaskSchedulerImpl#resourceOfferSingleTaskSet```,流程如下图: 71 | 72 | 73 | ![](http://upload-images.jianshu.io/upload_images/204749-3f3ad9aad7db6b08.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 74 | 75 | 76 | 77 | --- 78 | 79 | 80 | 以上,就完成了分配 tasks 给 executors 的流程分析,细节比较多,涉及的知识点也比较多,需要扩展阅读文中给出的另几个文章,最后给出一个整体的流程图方便理解 81 | 82 | 83 | ![](http://upload-images.jianshu.io/upload_images/204749-c4f656cde319c120.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 84 | 85 | --- 86 | 87 | 欢迎关注我的微信公众号:FunnyBigData 88 | 89 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 90 | -------------------------------------------------------------------------------- /spark-core/Spark-Task-的执行流程②---创建、分发-Task.md: -------------------------------------------------------------------------------- 1 | > 本文为 Spark 2.0 源码分析笔记,由于源码只包含 standalone 模式下完整的 executor 相关代码,所以本文主要针对 standalone 模式下的 executor 模块,文中内容若不特意说明均为 standalone 模式内容 2 | 3 | ## 创建 task(driver 端) 4 | task 的创建本应该放在[分配 tasks 给 executors](http://www.jianshu.com/p/17a61ff4d65c)一文中进行介绍,但由于创建的过程与分发及之后的反序列化执行关系紧密,我把这一部分内容挪到了本文。 5 | 6 | 创建 task 是在 ```TaskSetManager#resourceOffer(...)``` 中实现的,更准确的说是创建 ```TaskDescription```,task 及依赖的环境都会被转换成 byte buffer,然后与 ```taskId、taskName、execId``` 等一起构造 ```TaskDescription``` 对象,该对象将在之后被序列化并分发给 executor 去执行,主要流程如下: 7 | 8 | 9 | ![](http://upload-images.jianshu.io/upload_images/204749-f2f18156070f63bf.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 10 | 11 | 12 | 从流程图中可以看出,task 依赖了的文件、jar 包、设置的属性及其本身都会被转换成 byte buffer 13 | 14 | ## 分发 task(driver 端) 15 | 分发 task 操作是在 driver 端的 ```CoarseGrainedSchedulerBackend#launchTasks(tasks: Seq[Seq[TaskDescription]])``` 中进行,由于上一步已经创建了 TaskDescription 对象,分发这里要做的事就很简单,如下: 16 | 17 | 18 | ![](http://upload-images.jianshu.io/upload_images/204749-8e0b3a99c427fc2b.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 19 | 20 | 21 | 仅仅是序列化了 ```TaskDescription``` 对象并发送 ```LaunchTask``` 消息给 ```CoarseGrainedExecutorBackend``` 22 | 23 | ## worker 接收并处理 LaunchTask 消息 24 | LaunchTask 消息是由 CoarseGrainedExecutorBackend 接收到的,接收到后的处理流程如下: 25 | 26 | 27 | ![](http://upload-images.jianshu.io/upload_images/204749-209b2328e4380d8d.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 28 | 29 | 30 | 31 | 接收到消息后,CoarseGrainedExecutorBackend 会从消息中反序列化出 TaskDescription 对象并交给 Executor 去执行;Executor 利用 TaskDescription 对象创建 TaskRunner 然后提交到自带的线程池中执行。 32 | 33 | 关于 TaskRunner、线程池以及 task 具体是如何执行的,将会在下一篇文章中详述,本文只关注创建、分发 task 的过程。 34 | 35 | --- 36 | 37 | 欢迎关注我的微信公众号:FunnyBigData 38 | 39 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 40 | -------------------------------------------------------------------------------- /spark-core/Spark-Task-的执行流程③---执行-task.md: -------------------------------------------------------------------------------- 1 | > 本文为 Spark 2.0 源码分析笔记,其他版本可能稍有不同 2 | 3 | [创建、分发 Task](http://www.jianshu.com/p/08c66cbc31d6)一文中我们提到 ```TaskRunner```(继承于 Runnable) 对象最终会被提交到 ```Executor``` 的线程池中去执行,本文就将对该执行过程进行剖析。 4 | 5 | 该执行过程封装在 ```TaskRunner#run()``` 中,搞懂该函数就搞懂了 task 是如何执行的,按照本博客惯例,这里必定要来一张该函数的核心实现: 6 | 7 | 8 | ![](http://upload-images.jianshu.io/upload_images/204749-f0505fcc393c3369.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 9 | 10 | 11 | 需要注意的是,上图的流程都是在 Executor 的线程池中的某条线程中执行的。上图中最复杂和关键的是 ```task.run(...)``` 以及任务结果的处理,也即怎么把各个 partition 计算结果汇报到 driver 端。 12 | 13 | task 结果处理这一块内容将另写一篇文章进行说明,下文主要对 ```task.run(...)``` 进行分析。Task 类共有两种实现: 14 | 15 | * ResultTask:对于 DAG 图中最后一个 Stage(也就是 ResultStage),会生成与该 DAG 图中哦最后一个 RDD (DAG 图中最后边)partition 个数相同的 ResultTask 16 | * ShuffleMapTask:对于非最后的 Stage(也就是 ShuffleMapStage),会生成与该 Stage 最后的 RDD partition 个数相同的 ShuffleMapTask 17 | 18 | 在 ```Task#run(...)``` 方法中最重要的是调用了 ```Task#runTask(context: TaskContext)``` 方法,来分别看看 ResultTask 和 ShuffleMapTask 的实现: 19 | 20 | ## ResultTask#runTask(context: TaskContext) 21 | 22 | ``` 23 | override def runTask(context: TaskContext): U = { 24 | // Deserialize the RDD and the func using the broadcast variables. 25 | val deserializeStartTime = System.currentTimeMillis() 26 | val ser = SparkEnv.get.closureSerializer.newInstance() 27 | //< 反序列化得到 rdd 及 func 28 | val (rdd, func) = ser.deserialize[(RDD[T], (TaskContext, Iterator[T]) => U)]( 29 | ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader) 30 | _executorDeserializeTime = System.currentTimeMillis() - deserializeStartTime 31 | 32 | //< 对 rdd 指定 partition 的迭代器执行 func 函数 33 | func(context, rdd.iterator(partition, context)) 34 | } 35 | ``` 36 | 37 | 实现代码如上,主要做了两件事: 38 | 39 | 1. 反序列化得到 rdd 及 func 40 | 2. 对 rdd 指定 partition 的迭代器执行 func 函数并返回结果 41 | 42 | func 函数是什么呢?我举几个例子就很容易明白: 43 | 44 | * 对于 ```RDD#count()``` 的 ResultTask 这里的 func 真正执行的是 ```def getIteratorSize[T](iterator: Iterator[T]): Long```,即计算该 partition 对应的迭代器的数据条数 45 | * 对于 ```RDD#take(num: Int): Array[T]``` 的 ResultTask 这里的 func 真正执行的是 ```(it: Iterator[T]) => it.take(num).toArray```,即取该 partition 对应的迭代器的前 num 条数据 46 | 47 | 也就是说,func 是对已经计算获得的 RDD 的某个 partition 的迭代器执行在 RDD action 中预定义好的操作,具体的操作根据不同的 action 不同而不同。而这个 partition 对应的迭代器的获取是通过调动 ```RDD#iterator(split: Partition, context: TaskContext): Iterator[T]``` 去获取的,会通过计算或从 cache 或 checkpoint 中获取。 48 | 49 | ## ShuffleMapTask#runTask(context: TaskContext) 50 | 与 ResultTask 对 partition 数据进行计算得到计算结果并汇报给 driver 不同,ShuffleMapTask 的职责是为下游的 RDD 计算出输入数据。更具体的说,ShuffleMapTask 要计算出 partition 数据并通过 shuffle write 写入磁盘(由 BlockManager 来管理)来等待下游的 RDD 通过 shuffle read 读取,其核心流程如下: 51 | 52 | 53 | ![](http://upload-images.jianshu.io/upload_images/204749-2f39dcde143b3e4c.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 54 | 55 | 56 | 共分为四步: 57 | 58 | 1. 从 SparkEnv 中获取 ShuffleManager 对象,当前支持 Hash、Sort Based、Tungsten-sort Based 以及自定义的 Shuffle(关于 shuffle 之后会专门写文章说明) 59 | 2. 从 ShuffleManager 中获取 ShuffleWriter 对象 writer 60 | 3. 得到对应 partition 的迭代器后,通过 writer 将数据写入文件系统中 61 | 4. 停止 writer 并返回结果 62 | 63 | --- 64 | 65 | 参考:《Spark 技术内幕》 66 | 67 | --- 68 | 69 | 欢迎关注我的微信公众号:FunnyBigData 70 | 71 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 72 | -------------------------------------------------------------------------------- /spark-core/Spark-Task-的执行流程④---task-结果的处理.md: -------------------------------------------------------------------------------- 1 | > 本文为 Spark 2.0 源码分析笔记,其他版本可能稍有不同 2 | 3 | [Spark Task 的执行流程③ - 执行 task](http://www.jianshu.com/p/8bb456cb7c77)一文中介绍了 task 是如何执行并返回 task 执行结果的,本文将进一步介绍 task 的结果是怎么处理的。 4 | 5 | ## worker 端的处理 6 | 处理 task 的结果是在 ```TaskRunner#run()``` 中进行的,紧接着 task 执行步骤,结果处理的核心流程如下: 7 | 8 | 9 | ![](http://upload-images.jianshu.io/upload_images/204749-e799dff6b2f4e5e8.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 10 | 11 | 12 | 我们进一步展开上图中浅灰色背景步骤,根据 resultSize(序列化后的 task 结果大小) 大小的不同,共有三种情况: 13 | 14 | * ```resultSize > spark.driver.maxResultSize 配置值(默认1G)```:直接丢弃,若有必要需要修改 ```spark.driver.maxResultSize``` 的值。此时,serializedResult 为序列化的 IndirectTaskResult 对象,driver 之后通过该对象是获得不到结果的 15 | * ```resultSize > maxDirectResultSize 且 resultSize <= spark.driver.maxResultSize 配置值```:maxDirectResultSize 为配置的 ```spark.rpc.message.maxSize``` 与 ```spark.task.maxDirectResultSize``` 更小的值;这种情况下,会将结果存储到 BlockManager 中。此时,serializedResult 为序列化的 IndirectTaskResult 对象,driver 之后可以通过该对象在 BlockManager 系统中拉取结果 16 | * ```resultSize <= maxDirectResultSize```:serializedResult 直接就是 serializedDirectResult 17 | 18 | 在拿到 serializedResult 之后,调用 ```CoarseGrainedExecutorBackend#statusUpdate``` 方法,如下: 19 | 20 | ``` 21 | execBackend.statusUpdate(taskId, TaskState.FINISHED, serializedResult) 22 | ``` 23 | 24 | 该方法会使用 driverRpcEndpointRef 发送一条包含 serializedResult 的 ```StatusUpdate``` 消息给 driver (更具体说是其中的 CoarseGrainedSchedulerBackend 对象) 25 | 26 | ## driver 端的处理 27 | driver 端的 CoarseGrainedSchedulerBackend 在收到 worker 端发送的 ```StatusUpdate``` 消息后,会进行一系列的处理,包括调用 TaskScheduler 方法以做通知,主要流程如下: 28 | 29 | 30 | ![](http://upload-images.jianshu.io/upload_images/204749-95cd467a373614b4.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 31 | 32 | 33 | 其中,需要说明的是 Task 的状态只有为 FINISHED 时才成功,其他值(FAILED, KILLED, LOST)均为失败。 34 | 35 | --- 36 | 37 | 欢迎关注我的微信公众号:FunnyBigData 38 | 39 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 40 | -------------------------------------------------------------------------------- /spark-core/Spark-executor-模块②---AppClient-向-Master-注册-Application.md: -------------------------------------------------------------------------------- 1 | > 本文为 Spark 2.0 源码分析笔记,由于源码只包含 standalone 模式下完整的 executor 相关代码,所以本文主要针对 standalone 模式下的 executor 模块,文中内容若不特意说明均为 standalone 模式内容 2 | 3 | [前一篇文章](http://www.jianshu.com/p/5dab83e94cac)简要介绍了 Spark 执行模块中几个主要的类以及 AppClient 是如何被创建的,这篇文章将详细的介绍 AppClient 向 Master 注册 Application 的过程,将主要从以下几个方面进行说明: 4 | 5 | * 注册 Application 时机 6 | * 注册 Application 的重试机制 7 | * 注册行为细节 8 | 9 | ## 注册 Application 时机 10 | 简单来说,AppClient 向 Master 注册 Application 是在 SparkContext 构造时发生的,也就是 driver 一开始运行就立马向 Master 注册 Application。更具体的步骤可以如下图表示: 11 | 12 | 13 | ![](http://upload-images.jianshu.io/upload_images/204749-b600f0ffb4f63f4f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 14 | 15 | 16 | ## 注册 Application 的重试机制 17 | StandaloneAppClient 中有两个成员,分别是:```private val REGISTRATION_TIMEOUT_SECONDS = 20``` 和 ```private val REGISTRATION_RETRIES = 3```。 其中,```REGISTRATION_RETRIES``` 代表注册 Application 的最大重试次数,为3次;而 ```REGISTRATION_TIMEOUT_SECONDS``` 代表 StandaloneAppClient 在执行注册之后隔多少秒去获取注册结果,具体的流程如下: 18 | 19 | 1. ClientEndpoint 实例通过发送 ```RegisterApplication``` 消息给 Master 来向 Master 注册 Application 20 | 2. 隔 ```REGISTRATION_TIMEOUT_SECONDS``` 秒后检测 registered 标记,若其对应值为 true,则表明注册成功;否则,表明注册失败 21 | * Master 会在注册 Application 后向 AppClient 响应 ```RegisteredApplication``` 消息,AppClient 收到该消息会置 registered 对应值为 true 22 | * 若 Master 没有响应该消息,则 registered 一直为 false) 23 | 3. 若注册成功,注册流程结束;若注册失败: 24 | * 已尝试注册次数小于 ```REGISTRATION_RETRIES```,返回第一步再来一次 25 | * 已尝试注册次数等于 ```REGISTRATION_RETRIES```,结束注册流程,将 Application 标记为 dead,通过回调通知 SchedulerBackend Application dead 26 | 27 | 上面这一小段即时注册 Application 的重试机制,下面再来看看注册的一些细节 28 | 29 | ## 注册行为的细节 30 | 31 | 注册行为可以主要分为以下三步: 32 | 33 | 1. AppClient 发起注册 34 | 2. Master 接收并处理注册消息 35 | 3. AppClient 处理 Master 的注册响应消息 36 | 37 | ### Step1:AppClient 发起注册 38 | AppClient 是通过向 Master 发送 ```RegisterApplication``` 消息进行注册的。该消息定义为一个 case class,其中 ```appDescription: ApplicationDescription``` 成员描述了要注册并启动一个怎么样的 Application(主要包含属性及资源信息),其定义如下: 39 | 40 | ``` 41 | private[spark] case class ApplicationDescription( 42 | name: String, //< Application 的名字 43 | maxCores: Option[Int], //< application 总共能用的最大 cores 数量 44 | memoryPerExecutorMB: Int, //< 每个 executor 分配的内存 45 | command: Command, //< 启动 executor 的 ClassName、所需参数、环境信息等启动一个 Java 进程的所有需要的信息;在 Standalone 模式下,类名就是 CoarseGrainedExecutorBackend 46 | appUiUrl: String, //< Application 的 web ui 的 host:port 47 | eventLogDir: Option[URI] = None, //< Spark事件日志记录的目录。在这个基本目录下,Spark为每个 Application 创建一个子目录。各个应用程序记录日志到相应的目录。常设置为 hdfs 目录以便于 history server 访问来重构 web ui的目录 48 | eventLogCodec: Option[String] = None, 49 | coresPerExecutor: Option[Int] = None, //< 每个 executor 使用的 cores 数量 50 | initialExecutorLimit: Option[Int] = None, 51 | user: String = System.getProperty("user.name", "")) { 52 | 53 | override def toString: String = "ApplicationDescription(" + name + ")" 54 | } 55 | 56 | private[spark] case class Command( 57 | mainClass: String, 58 | arguments: Seq[String], 59 | environment: Map[String, String], 60 | classPathEntries: Seq[String], 61 | libraryPathEntries: Seq[String], 62 | javaOpts: Seq[String]) { 63 | } 64 | ``` 65 | 66 | 除了 Application 的描述,注册时还会带上 ClientEndpoint 对应的 rpcEndpointRef,以便 Master 能通过该 rpcEndpointRef 给自身发送消息。 67 | 68 | 构造该消息实例后,ClientEndpoint 就会通过 master rpcEndpointRef 给 Master 发送该注册消息 69 | 70 | ### Step2:Master 接收并处理注册消息 71 | Master 接收到注册消息后的主要处理流程如下图所示: 72 | 73 | ![](http://upload-images.jianshu.io/upload_images/204749-dd9a570a42529073.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 74 | 75 | 76 | 在向 driver 发送 RegisteredApplication 消息后,其实已经完成了注册流程,从上面的流程图可以看出,只要接收到 AppClient 的注册请求,Master 都能成功注册 Application 并响应消息。这之后的调度都做了什么呢?我们继续跟进 Master#schedule() 方法。 77 | 78 | schedule() 的流程如下: 79 | 80 | 1. 打散(shuffle)所有状态为 ALIVE 的 workders 81 | 2. 对于每一个处于 WAITTING 状态的 driver,都要遍历所有的打散的 alive works 82 | * 如果 worker 的 free memory 和 free cores 都大于等于 driver 要求的值,则通过给该 worker 发送 ```LaunchDriver``` 消息来启动 driver 并把该 driver 从 WAITTING driver 中除名 83 | 3. ```startExecutorsOnWorkers()```:在 workers 上启动 executors(当前,只实现了简单的 FIFO 调度,先满足第一个 app,然后再满足第二个 app,以此类推) 84 | 1. 从 waitingApps 中取出一个 app(app.coresLeft > 0) 85 | 2. 对于该 app,从所有可用的 workers 中筛选出 free memory和 free cores 满足 app executor 需求的 worker,为 usableWorkers 86 | 3. 调用 ```scheduleExecutorsOnWorkers``` 方法来在 usableWorkers 上分配 executors,有两种模式: 87 | * 一种是尽量把一个 app 的 executors 分配到尽可能多的 workers 上 88 | * 另一种是尽量把一个 app 的 executors 分配到尽量少的 workers 上 89 | 4. 上一步得到了要在每个 workers 上使用多少个 cores,这一步就要来分配这些了: 90 | * 调用 ```allocateWorkerResourceToExecutors``` 进行分配: 91 | * 分配一个 worker 的资源给一个或多个 executors 92 | * 调用 ```launchExecutor(worker, exec)``` 启动 executor 93 | * 对应的 WorkerInfo 增加刚分配的 ExecutorDesc 94 | * 给 worker 发送 LaunchExecutor 消息,以要求其启动指定信息的 executor 95 | * 给 driver 发送 ExecutorAdded 消息,以通知其有新的 Executor 添加了 96 | * 置 app 的状态为 RUNNING 97 | 98 | ### Step3:AppClient 处理 Master 的注册响应消息 99 | Master 若成功处理了注册请求,会响应给 AppClient 一个 ```RegisteredApplication``` 消息,AppClient 在接收到该响应消息后,会进行一些简单的操作,主要包括: 100 | 101 | * 设置 appId 102 | * 至 registered 为 true 103 | * 通知 SchedulerBackend 已成功注册 Application 104 | 105 | --- 106 | 107 | 欢迎关注我的微信公众号:FunnyBigData 108 | 109 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 110 | -------------------------------------------------------------------------------- /spark-core/Spark-executor-模块③---启动-executor.md: -------------------------------------------------------------------------------- 1 | > 本文为 Spark 2.0 源码分析笔记,由于源码只包含 standalone 模式下完整的 executor 相关代码,所以本文主要针对 standalone 模式下的 executor 模块,文中内容若不特意说明均为 standalone 模式内容 2 | 3 | 在介绍[AppClient 向 Master 注册 Application](http://www.jianshu.com/p/5175dfd679b8)的过程中,我们知道 Master 在处理 AppClient 的注册消息时,会进行调度,调度的过程中会决定在某个 worker 上启动某个(或某些) executor,这时会向指定的 worker 发送 ```LaunchExecutor``` 消息,本文将对 worker 接收到该消息后如何启动 executor 进行剖析。 4 | 5 | ## worker 启动 executor 6 | worker 接收到 ```LaunchExecutor``` 消息后的处理流程如下图所示,主要有四个步骤,我们仅对最关键的创建 ExecutorRunner 对象的创建与启动进行分析 7 | 8 | 9 | ![](http://upload-images.jianshu.io/upload_images/204749-1734c80ed5e3bf9a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 10 | 11 | 12 | ### ExecutorRunner 对象的创建与启动 13 | ExecutorRunner 是用来管理 executor 进程的,只在 Standalone 模式下有。创建 ExecutorRunner 对象 manager 时,仅对其成员变量做了简单的初始化。关键还是在于 manager 调用的 ```start()``` 方法,该方法实现如下: 14 | 15 | 16 | ![](http://upload-images.jianshu.io/upload_images/204749-a1ef6a54edc20a2f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 17 | 18 | 19 | 那么上图中在 start() 方法中新创建的线程中调用的 ```ExecutorRunner#fetchAndRunExecutor``` 又做了什么呢?该方法主要做了以下事情: 20 | 21 | 1. 结合 ApplicationDescription.command 启动 CoarseGrainedExecutorBackend 进程 22 | 1. CoarseGrainedExecutorBackend 在启动后,会向 driver 发送 RegisterExecutor 消息注册 executor 23 | 2. driver 在接收到 RegisterExecutor 消息后,会将 Executor 的信息保存在本地,并响应 ```RegisteredExecutor``` 消息 24 | 3. 回到 CoarseGrainedExecutorBackend,它在接收到 driver 回应的 ```RegisteredExecutor``` 消息后,会创建一个 Executor。至此,Executor 创建完毕(Executor 在 Mesos、YARN、Standalone 模式下都是相同的,不同的只是资源的分配方式) 25 | 4. driver 端调用 CoarseGrainedSchedulerBackend.DriverEndpoint#makeOffers() 实现在 Executor 上启动 task 26 | 2. 阻塞等待该 CoarseGrainedExecutorBackend 进程退出 27 | 3. 该 CoarseGrainedExecutorBackend 进程退出后,向 worker 发送 ExecutorStateChanged(Executor 状态变更为 EXITED) 消息通知其 Executor 退出 28 | 29 | 其中,在创建、启动或等待 CoarseGrainedExecutorBackend 进程的过程中: 30 | 31 | * 若捕获到 ```InterruptedException``` 类型异常,表明 worker 进程被强制 kill, 则将 Executor 状态置为 KILLED 并调用 killProcess 方法来结束 CoarseGrainedExecutorBackend 进程 32 | * 若捕获到其他类型异常,表明 worker 进程意外退出,则将 Executor 的状态置为 FAILED 并调用 killProcess 方法来结束 CoarseGrainedExecutorBackend 进程 33 | 34 | 至此,我们完成了对 executor 启动过程的分析。 35 | 36 | --- 37 | 38 | 欢迎关注我的微信公众号:FunnyBigData 39 | 40 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 41 | -------------------------------------------------------------------------------- /spark-core/Spark-executor-模块④---Task-的执行流程.md: -------------------------------------------------------------------------------- 1 | > Task 的执行流程相关内容在一年多以前的文章 [Task的调度与执行源码剖析](http://www.jianshu.com/p/9a059ace2f3a) 中已经介绍了很多,但那篇文章内容过长、条理不够清楚并且版本过于久远(本次针对2.0),这里趁分析 executor 模块的机会再写一写 2 | 3 | Task 的执行流程分以下四篇进行介绍: 4 | 5 | 1. [为 task 分配 executor](http://www.jianshu.com/p/17a61ff4d65c) 6 | 2. [创建、分发 Task](http://www.jianshu.com/p/08c66cbc31d6) 7 | 3. [执行 Task](http://www.jianshu.com/p/8bb456cb7c77) 8 | 4. [task 结果的处理](http://www.jianshu.com/p/c204735a6bc3) 9 | 10 | 参考:《Spark技术内幕》 11 | 12 | --- 13 | 14 | 欢迎关注我的微信公众号:FunnyBigData 15 | 16 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 17 | -------------------------------------------------------------------------------- /spark-core/Spark-executor模块①---主要类以及创建-AppClient.md: -------------------------------------------------------------------------------- 1 | > 本文为 Spark 2.0 源码分析笔记,由于源码只包含 standalone 模式下完整的 executor 相关代码,所以本文主要针对 standalone 模式下的 executor 模块,文中内容若不特意说明均为 standalone 模式内容 2 | 3 | 在 executor 模块中,最重要的几个类(或接口、trait)是: 4 | 5 | * AppClient:在 Standalone 模式下的实现是 ```StandaloneAppClient``` 类 6 | * SchedulerBackend:SchedulerBackend 是一个 trait,在 Standalone 模式下的实现是 ```StandaloneSchedulerBackend``` 类 7 | * TaskScheduler:TaskScheduler 也是一个 trait,当前,在所有模式下的实现均为 ```TaskSchedulerImpl``` 类 8 | 9 | 接下来先简要介绍这几个类的作用以及各自主要的成员和方法,这是理解之后内容的基础 10 | 11 | ## StandaloneAppClient(AppClient) 12 | StandaloneAppClient 主要有以下几个作用: 13 | 14 | 1. 向 master 注册 application 15 | 2. 接收并处理来自 master 的各种消息,如 ```RegisteredApplication```、```ApplicationRemoved```、```ExecutorAdded``` 等 16 | 3. 调用 SchedulerBackend 回调接口以通知各种重要的 event,比如:Application 失败、添加了 executor、executor 更新等 17 | 18 | ### 主要成员 19 | 20 | * ```private val REGISTRATION_TIMEOUT_SECONDS = 20```:注册 application 的超时 21 | * ```private val REGISTRATION_RETRIES = 3```:注册 application 的最大重试次数 22 | * ```endpoint: ClientEndpoint```:ClientEndpoint 为 StandaloneAppClient 内部嵌套类,主要用来: 23 | * 通过向 master 发送 ```RegisterApplication``` 消息来注册 application 24 | * 接收来自 master 的消息并处理,消息包括 25 | * ```RegisteredApplication```:application 已成功注册 26 | * ```ApplicationRemoved```:application 已移除 27 | * ```ExecutorAdded```:有新增加的 Executor 28 | * ```ExecutorUpdated```:Executor 发生资源更新 29 | * ```MasterChanged```:master 改变 30 | * 接收来自 StandaloneAppClient 发送的消息并处理,包括: 31 | * ```StopAppClient```:StandaloneAppClient stop 时通知 ClientEndpoint 也进行 stop 并反注册 application 32 | * ```RequestExecutors```:StandaloneAppClient 在注册完 Application 后通过 ClientEndpoint 向 master 为执行 Application 的 tasks 申请资源 33 | * ```KillExecutors```:StandaloneAppClient 通过 ClientEndpoint 向 master 发送消息来 kill executor 34 | 35 | ### 主要方法 36 | * ```def start()```:启动 StandaloneAppClient 37 | * ```def requestTotalExecutors(requestedTotal: Int): Boolean```:为 application 向 master 申请指定总数的 executors 38 | * ```def killExecutors(executorIds: Seq[String]): Boolean```:通过 ClientEndpoint 向 master 发送消息来 kill 一组 executors 39 | 40 | ## SchedulerBackend 41 | SchedulerBackend 在 Standalone 模式下的 SchedulerBackend 的实现是 StandaloneSchedulerBackend,但是从大体的作用上来说,各个模式下的 SchedulerBackend 作用是相同的,主要为: 42 | 43 | 1. 当有新的 task 提交或资源更新时,查找各个节点空闲资源,并确定在哪个 executor 上启动哪个 task 的对应关系,对应的方法是 ```def reviveOffers(): Unit``` 44 | 2. 被 TaskScheduler 调用来 kill task,对应的方法是 ```def killTask(...): Unit``` 45 | 46 | ## TaskScheduler 47 | 低等级的 task 调度接口,当前只有 TaskSchedulerImpl 这一个实现。该接口支持在不同的部署模式下工作。每个 SparkContext(application) 对应唯一的一个 TaskScheduler。 TaskScheduler 从 DAGScheduler 的每一个 stage 获取 tasks,并负责发送到集群去执行这些 tasks,在失败的时候重试,并减轻掉队情况。TaskScheduler 会返回 events 给 DAGScheduler。 48 | 49 | ### 主要方法 50 | * ```def rootPool: Pool```:返回 root 调度对列 51 | * ```def schedulingMode: SchedulingMode```:调度模式 52 | * ```def submitTasks(taskSet: TaskSet)```:提交任务去集群执行 53 | * ```def cancelTasks(stageId: Int, interruptThread: Boolean)```:取消一个 stage 对应的 tasks 54 | * ```def executorHeartbeatReceived(...) ```:接收到 executor 心跳信息 55 | * ```def executorLost(executorId: String, reason: ExecutorLossReason)```:处理 56 | executor lost 57 | 58 | 以上简要的介绍了 AppClient、SchedulerBackend、TaskScheduler 几个接口,其中 SchedulerBackend 和 TaskScheduler 接口实例是在 SparkContext 构造函数中创建的,而 AppClient 实例是在 SchedulerBackend 构造函数中被创建。 59 | 60 | ## AppClient 的创建与启动 61 | AppClient 的创建与启动也比较简单,主要流程如下: 62 | 63 | 1. 在 SparkContext 的构造函数中,调用 ```val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)``` 来通过 master url 来创建相应模式下的 SchedulerBackend 实例 sched 以及 TaskSchedulerImpl 实例 ts(我们假定这里创建的 sched 是 StandaloneScheduler 类型的) 64 | 2. 随后,依然是在 SparkContext 的构造函数中,TaskScheduler 实例 ts 调用其 start 方法,在该 start 方法中会调用 SchedulerBackend 实例 sched 的 start 方法(所以,你也可以从这里知道 TaskScheduler 的实现中是包含 SchedulerBackend 的实例的) 65 | 3. 在 SchedulerBackend 的 start 方法中会创建其嵌套类 ClientEndpoint 对象 66 | 4. 在将 ClientEndpoint 对象注册给 rpcEnv 的过程中 ClientEndpoint 对象会收到 OnStart 消息并处理,处理过程主要就是持有 ApplicationDescription(主要包括name, maxCores, memoryPerExecutorMB, 启动命令行, appUiUrl等) 来向 Master 注册 application 67 | 68 | 再次说明,以上内容若无特别说明均指 Standalone 模式下的。本文简要的分析了几个关键类以及 AppClient 是如何启动的,更详细的剖析会在后面的文章中说明。 69 | 70 | --- 71 | 72 | 欢迎关注我的微信公众号:FunnyBigData 73 | 74 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 75 | -------------------------------------------------------------------------------- /spark-core/Spark-内存管理的前世今生(上).md: -------------------------------------------------------------------------------- 1 | > 欢迎关注我的微信公众号:FunnyBigData 2 | 3 | 作为打着 “内存计算” 旗号出道的 Spark,内存管理是其非常重要的模块。作为使用者,搞清楚 Spark 是如何管理内存的,对我们编码、调试及优化过程会有很大帮助。本文之所以取名为 "Spark 内存管理的前世今生" 是因为在 Spark 1.6 中引入了新的内存管理方案,而在之前一直使用旧方案。 4 | 5 | 刚刚提到自 1.6 版本引入了新的内存管理方案,但并不是说在 1.6 及之后的版本中不能使用旧的方案,而是默认使用新方案。我们可以通过设置 ```spark.memory.userLegacyMode``` 值来选择,该值为 ```false``` 表示使用新方案,```true``` 表示使用旧方案,默认为 ```false```。该值是如何发挥作用的呢?如下: 6 | 7 | ``` 8 | val useLegacyMemoryManager = conf.getBoolean("spark.memory.useLegacyMode", false) 9 | val memoryManager: MemoryManager = 10 | if (useLegacyMemoryManager) { 11 | new StaticMemoryManager(conf, numUsableCores) 12 | } else { 13 | UnifiedMemoryManager(conf, numUsableCores) 14 | } 15 | ``` 16 | 17 | 根据 ```spark.memory.useLegacyMode``` 值的不同,会创建 MemoryManager 不同子类的实例: 18 | 19 | * 值为 ```false```:创建 ```UnifiedMemoryManager``` 类实例,为新的内存管理的实现 20 | * 值为 ```true```:创建 ```StaticMemoryManager```类实例,为旧的内存管理的实现 21 | 22 | 不管是在新方案中还是旧方案中,都根据内存的不同用途,都包含三大块。 23 | 24 | * storage 内存:用于缓存 RDD、展开 partition、存放 Direct Task Result、存放广播变量。在 Spark Streaming receiver 模式中,也用来存放每个 batch 的 blocks 25 | * execution 内存:用于 shuffle、join、sort、aggregation 中的缓存、buffer 26 | 27 | storage 和 execution 内存都通过 MemoryManager 来申请和管理,而另一块内存则不受 MemoryManager 管理,主要有两个作用: 28 | 29 | * 在 spark 运行过程中使用:比如序列化及反序列化使用的内存,各个对象、元数据、临时变量使用的内存,函数调用使用的堆栈等 30 | * 作为误差缓冲:由于 storage 和 execution 中有很多内存的使用是估算的,存在误差。当 storage 或 execution 内存使用超出其最大限制时,有这样一个安全的误差缓冲在可以大大减小 OOM 的概率 31 | 32 | 这块不受 MemoryManager 管理的内存,由系统预留以及 storage 和 execution 安全系数之外的内存组成,这个会在下文中详述。 33 | 34 | 接下来,让我们先来看看 “前世” 35 | 36 | ## 前世 37 | 旧方案的内存结构如下图所示: 38 | 39 | 40 | ![](http://upload-images.jianshu.io/upload_images/204749-f76e9cad68ab24c5.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 41 | 42 | 43 | 让我们结合上图做进一步说明: 44 | 45 | #### execution 内存 46 | execution 最大可用内存为 ```jvm space * spark.storage.memoryFraction * spark.storage.safetyFraction```,默认为 ```jvm space * 0.2 * 0.8```。 47 | 48 | ```spark.shuffle.memoryFraction``` 很大程度上影响了 spill 的频率,如果 spill 过于频繁,可以适当增大 ```spark.shuffle.memoryFraction``` 的值,增加用于 shuffle 的内存,减少Spill的次数。这样一来为了避免内存溢出,可能需要减少 storage 的内存,即减小```spark.storage.memoryFraction``` 的值,这样 RDD cache 的容量减少,在某些场景下可能会对性能造成影响。 49 | 50 | 由于 shuffle 数据的大小是估算出来的(这主要为了减少计算数据大小的时间消耗),会存在误差,当实际使用的内存比估算大的时候,这里 ```spark.shuffle.safetyFraction``` 用来作为一个保险系数,增加一定的误差缓冲,降低实际内存占用超过用户配置值的可能性。所以 execution 真是最大可用的内存为 ```0.2*0.8=0.16```。shuffle 时,一旦 execution 内存使用超过该比例,就会进行 spill。 51 | 52 | #### storage 内存 53 | storage 最大可用内存为 ```jvm space * spark.storage.memoryFraction * spark.storage.safetyFraction```,默认为 ```jvm space * 0.6 * 0.9```。 54 | 55 | 由于在 cache block 时大小也是估算的,所以也需要一个保险系数用来防止误差引起 OOM,即 ```spark.storage.safetyFraction```,所以真实能用来进行 memory cache block 的内存大小的比例为 ```0.6*0.9=0.54```。一旦 storage 使用内存超过该比例,将根据 StorageLevel 决定不缓存 block 还是 OOM 或是存储到磁盘。 56 | 57 | storage 内存中有 ```spark.shuffle.unrollFraction``` 的部分是用来 unroll,即用于 “展开” 一个 partition 的数据,这部分默认为 0.2 58 | 59 | #### 不由 MemoryManager 管理的内存 60 | 系统预留的大小为:```1 - spark.storage.memoryFraction - spark.shuffle.memoryFraction```,默认为 0.2。另一部分是 storage 和 execution 保险系数之外的内存大小,默认为 0.1。 61 | 62 | #### 存在的问题 63 | 旧方案最大的问题是 storage 和 execution 的内存大小都是固定的,不可改变,即使 execution 有大量的空闲内存且 storage 内存不足,storage 也无法使用 execution 的内存,只能进行 spill,反之亦然。所以,在很多情况下存在资源浪费。 64 | 65 | 另外,旧方案中,只有 execution 内存支持 off heap,storage 内存不支持 off heap。 66 | 67 | ## 今生 68 | 上面我们提到旧方案的两个不足之处,在新方案中都得到了解决,即: 69 | 70 | * 新方案 storage 和 execution 内存可以互相借用,当一方内存不足可以向另一方借用内存,提高了整体的资源利用率 71 | * 新方案中 execution 内存和 storage 内存均支持 off heap 72 | 73 | 这两点将在后文中进一步展开,我们先来看看新方案中,默认的内存结构是怎样的?依旧分为三块(这里将 storage 和 execution 内存放在一起讲): 74 | 75 | * 不受 MemoryManager 管理内存,由以下两部分组成: 76 | * 系统预留:大小默认为 ```RESERVED_SYSTEM_MEMORY_BYTES```,即 300M,可以通过设置 ```spark.testing.reservedMemory``` 改变,一般只有测试的时候才会设置该配置,所以我们可以认为系统预留大小为 300M。另外,executor 的最小内存限制为系统预留内存的 1.5 倍,即 450M,若 executor 的总内存大小小于 450M,则会抛出异常 77 | * storage、execution 安全系数外的内存:大小为 ```(heap space - RESERVED_SYSTEM_MEMORY_BYTES)*(1 - spark.memory.fraction)```,默认为 ```(heap space - 300M)* 0.4``` 78 | * storage + execution:storage、execution 内存之和又叫 usableMemory,总大小为 ```(heap space - 300) * spark.memory.fraction```,```spark.memory.fraction``` 默认为 0.6。该值越小,发生 spill 和 block 踢除的频率就越高。其中: 79 | * storage 内存:默认占其中 50%(包含 unroll 部分) 80 | * execution 内存:默认同样占其中 50% 81 | 82 | 由于新方案是 1.6 后默认的内存管理方案,也是目前绝大部分 spark 用户使用的方案,所以我们有必要更深入且详细的展开分析。 83 | 84 | ### 初探统一内存管理类 85 | 86 | 87 | 在最开始我们提到,新方案是由 ```UnifiedMemoryManager``` 实现的,我们先来看看该类的成员及方法,类图如下: 88 | 89 | 90 | ![](http://upload-images.jianshu.io/upload_images/204749-684d6df3955552f6.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 91 | 92 | 93 | 通过这个类图,我想告诉你这几点: 94 | 95 | * UnifiedMemoryManager 具有 4 个 MemoryPool,分别是堆内的 onHeapStorageMemoryPool 和 onHeapExecutionMemoryPool 以及堆外的 offHeapStorageMemoryPool 和 offHeapExecutionMemoryPool(其中,execution 和 storage 使用堆外内存的方式不同,后面会讲到) 96 | * UnifiedMemoryManager 申请、释放 storage、execution、unroll 内存的方法(看起来像废话) 97 | * tungstenMemoryAllocator 会根据不同的 MemoryMode 来生成不同的 MemoryAllocator 98 | * 若 MemoryMode 为 ON_HEAP 为 HeapMemoryAllocator 99 | * 若 MemoryMode 为 OFF_HEAP 则为 UnsafeMemoryAllocator(使用 unsafe api 来申请堆外内存) 100 | 101 | ### 如何申请 storage 内存 102 | 有了上面的这些基础知识,再来看看是怎么申请 storage 内存的。申请 storage 内存是通过调用 103 | 104 | ``` 105 | UnifiedMemoryManager#acquireStorageMemory(blockId: BlockId, 106 | numBytes: Long, 107 | memoryMode: MemoryMode): Boolean 108 | ``` 109 | 110 | 更具体的说法应该是为某个 block(blockId 指定)以那种内存模式(on heap 或 off heap)申请多少字节(numBytes)的 storage 内存,该函数的主要流程如下图: 111 | 112 | 113 | ![](http://upload-images.jianshu.io/upload_images/204749-c8652cb09fdec4dc.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 114 | 115 | 116 | 对于上图,还需要做一些补充来更好理解: 117 | 118 | #### MemoryMode 119 | * 如果 MemoryMode 是 ON_HEAP,那么 executionMemoryPool 为 onHeapExecutionMemoryPool、storageMemoryPool 为 onHeapStorageMemoryPool。maxMemory 为 ```(jvm space - 300M)* spark.memory.fraction```,如果你还记得的话,这在文章最开始的时候有介绍 120 | * 如果 MemoryMode 是 OFF_HEAP,那么 executionMemoryPool 为 offHeapExecutionMemoryPool、storageMemoryPool 为 offHeapMemoryPool。maxMemory 为 maxOffHeapMemory,由 ```spark.memory.offHeap.size``` 指定,由 execution 和 storage 共享 121 | 122 | #### 要向 execution 借用多少? 123 | 计算要向 execution 借用多少内存的代码如下: 124 | 125 | ``` 126 | val memoryBorrowedFromExecution = Math.min(executionPool.memoryFree, numBytes) 127 | ``` 128 | 129 | 为 execution 空闲内存和申请内存 size 的较小值,这说明了两点: 130 | 131 | * 能借用到的内存大小可能是小于申请的内存大小的(当 ```executionPool.memoryFree < numBytes```),更进一步说,成功借用到的内存加上 storage 原本空闲的内存之和有可能还是小于要申请的内存大小 132 | * execution 只可能把自己当前空闲的内存借给 storage,即使在这之前 execution 已经从 storage 借来了大量内存,也不会释放自己已经使用的内存来 “还” 给 storage。execution 这么不讲道理是因为要实现释放 execution 内存来归还给 storage 复杂度太高,难以实现 133 | 134 | 还有一点需要注意的是,借用是发生在相同 MemoryMode 的 storageMemoryPool 和 executionMemoryPool 之间,不能在不同的 MemoryMode 间进行借用 135 | 136 | #### 借到了就万事大吉? 137 | 当 storage 空闲内存不足以分配申请的内存时,从上面的分析我们知道会向 execution 借用,借来后是不是就万事大吉了?当然······不是,前面也提到了即使借到了内存也可能还不够,这也是上图中红色圆框中问号的含义,在我们再进一步跟进到 ```StorageMemoryPool#acquireMemory(blockId: BlockId, numBytes: Long): Boolean``` 中一探究竟,该函数主要流程如下: 138 | 139 | 140 | ![](http://upload-images.jianshu.io/upload_images/204749-ee88eec273a510f0.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 141 | 142 | 143 | 同样,对于上面这个流程图需要做一些说明: 144 | 145 | ##### 计算要释放的内存量 146 | ``` 147 | val numBytesToFree = math.max(0, numAcquireBytes - memoryFree) 148 | ``` 149 | 150 | 如上,要释放的内存大小为再从 execution 借用了内存,使得 storage 空闲内存增大 n(n>=0) 后,还比申请的内存少的那部分内存,若借用后 storage 空闲内存足以满足申请的大小,则 numBytesToFree 为 0,无需进行释放 151 | 152 | ##### 如何释放 storage 内存? 153 | 释放的方式是踢除已缓存的 blocks,实现为 ```evictBlocksToFreeSpace(blockId: Option[BlockId], space: Long, memoryMode: MemoryMode): Long```,有以下几个原则: 154 | 155 | * **只能踢除相同 MemoryMode 的 block** 156 | * **不能踢除属于同一个 RDD 的另一个 block** 157 | 158 | 首先会进行预踢除(所谓预踢除就是计算假设踢除该 block 能释放多少内存),预踢除的具体逻辑是:遍历一个已缓存到内存的 blocks 列表(该列表按照缓存的时间进行排列,约早缓存的在越前面),逐个计算预踢除符合原则的 block 是否满足以下条件之一: 159 | 160 | * 预踢除的累计总大小满足要踢除的大小 161 | * 所有的符合原则的 blocks 都被预踢除 162 | 163 | 若最终预踢除的结果是可以满足要提取的大小,则对预踢除中记录的要踢除的 blocks 进行真正的踢除。具体的方式是:如果从内存中踢除后,还具有其他 StorageLevel 或在其他节点有备份,依然保留该 block 信息;若无,则删除该 block 信息。最终,返回踢除的总大小(可能稍大于要踢除的大小)。 164 | 165 | 若最终预踢除的结果是无法满足要提取的大小,则不进行任何实质性的踢除,直接返回踢除size 为 0。需要再次提醒的是,只能踢除相同 MemoryMode 的 block。 166 | 167 | 以上,结合两幅流程图及相应的说明,相信你已经搞清楚如何申请 storage 内存了。我们再来看看 execution 内存是如何申请的 168 | 169 | ### 如何申请 execution 内存 170 | 我们知道,申请 storage 内存是为了 cache 一个 numBytes 的 block,结果要么是申请成功、要么是申请失败,不存在申请到的内存数比 numBytes 少的情况,这是因为不能将 block 一部分放内存,一部分 spill 到磁盘。但申请 execution 内存则不同,申请 execution 内存是通过调用 171 | 172 | ``` 173 | UnifiedMemoryManager#acquireExecutionMemory(numBytes: Long, 174 | taskAttemptId: Long, 175 | memoryMode: MemoryMode): Long 176 | ``` 177 | 178 | 来实现的,这里的 numBytes 是指至多 numBytes,最终申请的内存数比 numBytes 少也是成功的,比如在 shuffle write 的时候使用的时候,如果申请Å的内存不够,则进行 spill。 179 | 180 | 另一个特点是,申请 execution 时可能会一直阻塞,这是为了能确保每个 task 在进行 spill 之前都能占用至少 1/2N 的 execution pool 内存数(N 为 active tasks 数)。当然,这也不是能完全确保的,比如 tasks 数激增但老的 tasks 还没释放内存就不能满足。 181 | 182 | 接下来,我们来看看如何申请 execution 内存,流程图如下: 183 | 184 | ![](http://upload-images.jianshu.io/upload_images/204749-0a67f7159ee9f02c.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 185 | 186 | 从上图可以看到,整个流程还是挺复杂的。首先,我先对上图中的一些环节进行进一步说明以帮助理解,最后再以简洁的语言来概括一下整个过程。 187 | 188 | #### MemoryMode 189 | 同样,不同的 MemoryMode 的情况是不同的,如下: 190 | 191 | * 如果 MemoryMode 为 ON_HEAP: 192 | * executionMemoryPool 为 onHeapExecutionMemoryPool 193 | * storageMemoryPool 为 onHeapStorageMemoryPool 194 | * storageRegionSize 为 onHeapStorageRegionSize,即 ```(heap space - 300M) * spark.memory.storageFraction``` 195 | * maxMemory 为 maxHeapMemory,即 ```(heap space - 300M)``` 196 | * 如果 MemoryMode 为 OFF_HEAP: 197 | * executionMemoryPool 为 offHeapExecutionMemoryPool 198 | * storageMemoryPool 为 offHeapStorageMemoryPool 199 | * maxMemory 为 maxOffHeapMemory,即 ```spark.memory.offHeap.size``` 200 | * storageRegionSize 为 offHeapStorageRegionSize,即 ```maxOffHeapMemory * spark.memory.storageFraction``` 201 | 202 | 这一小节描述的内容非常重要,因为之后所有的流程都是基于此,看到后面的流程时,还记着会有 ON_HEAP 和 OFF_HEAP 两种情况 203 | 204 | #### maybeGrowExecutionPool(向 storage 借用内存) 205 | 只有当 executionMemoryPool 的空闲内存不足以满足申请的 numBytes 时,该函数才会生效。那这个函数是怎么向 storage 借用内存的呢?流程如下: 206 | 207 | 1. 计算可从 storage 回收的内存 memoryReclaimableFromStorage,为 storage 当前的空闲内存和之前 storage 从 execution 借走的内存中较大的那个 208 | 2. 如果 memoryReclaimableFromStorage 为 0,说明之前 storage 没有从 execution 这边借用过内存并且 storage 自己已经把内存用完了,没有任何内存可以借给 execution,那么本次借用就失败,直接返回;如果 memoryReclaimableFromStorage 大于 0,则进入下一步 209 | 3. 计算本次真正要借用的内存 spaceToReclaim,即 execution 不足的内存(申请的内存减去 execution 的空闲内存)与 memoryReclaimableFromStorage 中的较小值。原则是即使能借更多,也只借够用的就行 210 | 4. 执行借用操作,如果需要 storage 的空闲内存和之前 storage 从 execution 借用的的内存加起来才能满足,则会进行踢除 cached blocks 211 | 212 | 以上就是整个 execution 向 storage 借用内存的过程,与 storage 向 execution 借用最大的不同是:execution 会踢除 storage 已经使用的向 execution 的内存,踢除的流程在文章的前面有描述。这是因为,这本来就是属于 execution 的内存并且通过踢除来实现归还实现上也不复杂 213 | 214 | #### 一个 task 能使用多少 execution 内存? 215 | 也就是流程图中的 maxMemoryPerTask 和 minMemoryPerTask 是如何计算的,如下: 216 | 217 | ``` 218 | val maxPoolSize = computeMaxExecutionPoolSize() 219 | val maxMemoryPerTask = maxPoolSize / numActiveTasks 220 | val minMemoryPerTask = poolSize / (2 * numActiveTasks) 221 | ``` 222 | 223 | maxPoolSize 为从 storage 借用了内存后,executionMemoryPool 的最大可用内存,maxMemoryPerTask 和 minMemoryPerTask 的计算方式也如代码所示。这样做是为了使得每个 task 使用的内存都能维持在 ```1/2*numActiveTasks ~ 1/numActiveTasks``` 范围内,使得在整体上能保持各个 task 资源占用比较均衡并且一定程度上允许需要更多资源的 task 在一定范围内能分配到更多资源,也照顾到了个性化的需求 224 | 225 | #### 最后到底分配多少 execution 内存? 226 | 首先要计算两个值: 227 | 228 | * 最大可以分配多少,即 maxToGrant:是申请的内存量与 ```(maxMemoryPerTask-已为该 task 分配的内存值)``` 中的较小值,如果 ```maxMemoryPerTask < 已为该 task 分配的内存值```,则直接为 0,也就是之前已经给该 task 分配的够多了 229 | * 本次循环真正可以分配多少,即 toGrant:maxToGrant 与当前 executionMemoryPool 空闲内存(注意是借用后)的较小值 230 | 231 | 所以,本次最终能分配的量也就是 toGrant,如果 ```toGrant 加上已经为该 task 分配的内存量之和``` 还小于 minMemoryPerTask 并且 toGrant 小于申请的量,则就会触发阻塞。否则,分配 toGrant 成功,函数返回。 232 | 233 | 阻塞释放的条件有两个,如下: 234 | 235 | * 有 task 释放了内存:更具体的说是有 task 释放了相同 MemoryMode 的 execution 内存,这时空闲的 execution 内存变多了 236 | * 有新 task 申请了内存:同样,更具体的说是有新 task 申请了相同 MemoryMode 的 execution 内存,这时 numActiveTasks 变大了,minMemoryPerTask 则变小了 237 | 238 | 用简短的话描述整个过程如下: 239 | 240 | 1. 申请 execution 内存时,会循环不停的尝试,每次尝试都会看是否需要从 storage 中借用或回收之前借给 storage 的内存(这可能会触发踢除 cached blocks),如果需要则进行借用或回收; 241 | 2. 之后计算本次循环能分配的内存, 242 | * 如果能分配的不够申请的且该 task 累计分配的(包括本次)小于每个 task 应该获得的最小值(1/2*numActiveTasks),则会阻塞,直到有新的 task 申请内存或有 task 释放内存为止,然后进入下一次循环; 243 | * 否则,直接返回本次分配的值 244 | 245 | ## 使用建议 246 | 首先,建议使用新模式,所以接下来的配置建议都是基于新模式的。 247 | 248 | * ```spark.memory.fraction```:如果 application spill 或踢除 block 发生的频率过高(可通过日志观察),可以适当调大该值,这样 execution 和 storage 的总可用内存变大,能有效减少发生 spill 和踢除 block 的频率 249 | * ```spark.memory.storageFraction```:为 storage 占 storage、execution 内存总和的比例。虽然新方案中 storage 和 execution 之间可以发生内存借用,但总的来说,```spark.memory.storageFraction``` 越大,运行过程中,storage 能用的内存就会越多。所以,如果你的 app 是更吃 storage 内存的,把这个值调大一点;如果是更吃 execution 内存的,把这个值调小一点 250 | * ```spark.memory.offHeap.enabled```:堆外内存最大的好处就是可以避免 GC,如果你希望使用堆外内存,将该值置为 true 并设置堆外内存的大小,即设置 ```spark.memory.offHeap.size```,这是必须的 251 | 252 | 另外,需要特别注意的是,堆外内存的大小不会算在 executor memory 中,也就是说加入你设置了 ```--executor memory 10G``` 和 ```spark.memory.offHeap.size=10G```,那总共可以使用 20G 内存,堆内和堆外分别 10G。 253 | 254 | ## 总结&引子 255 | 到这里,已经比较笼统的介绍了 Spark 内存管理的 “前世”,也比较细致的介绍了 “今生”。篇幅比较长,但没有一大段一大段的代码,应该还算比较好懂。如果看到这里,希望你多少能有所收获。 256 | 257 | 然后,请你在大致回顾下这篇文章,有没有觉得缺了点什么?是的,是缺了点东西,所谓 “内存管理” 怎么就没看到具体是怎么分配内存的呢?是怎么使用的堆外内存?storage 和 execution 的堆外内存使用方式会不会不同?execution 和 storage 又是怎么使用堆内内存的呢?以怎么样的数据结构呢? 258 | 259 | 如果你想搞清楚这些问题,关注公众号并回复 “内存管理下”。 260 | 261 | --- 262 | 263 | 欢迎关注我的微信公众号:FunnyBigData 264 | 265 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 266 | -------------------------------------------------------------------------------- /spark-core/Spark-内存管理的前世今生(下).md: -------------------------------------------------------------------------------- 1 | > 欢迎关注我的微信公众号:FunnyBigData 2 | 3 | 在《Spark 内存管理的前世今生(上)》中,我们介绍了 UnifiedMemoryManager 是如何管理内存的。然而,UnifiedMemoryManager 是 MemoryManager 而不是 MemoryAllocator 或 MemoryConsumer,不进行实质上的内存分配和使用,只是负责可以分配多少 storage 或 execution 内存给谁,记录各种元数据信息。 4 | 5 | 这篇文章会关注 storage 的堆内堆外内存到底是在什么样的情况下,以什么样的形式分配以及是怎么使用的。 6 | 7 | 缓存 RDD 是 storage 内存最核心的用途,那我们就来看看缓存 RDD 的 partition 是怎样分配、使用 storage 内存的。 8 | 9 | 可以以非序列化或序列化的形式缓存 RDD,两种情况有所不同,我们先来看看非序列化形式的。 10 | 11 | ## 1: 缓存非序列化 RDD(只支持 ON_HEAP) 12 | 缓存非序列化 RDD 通过调用 13 | 14 | ``` 15 | MemoryStore#putIteratorAsValues[T](blockId: BlockId, 16 | values: Iterator[T], 17 | classTag: ClassTag[T]): Either[PartiallyUnrolledIterator[T], Long] 18 | ``` 19 | 20 | 来缓存一个个 partition 。该函数缓存一个 partition(一个 partition 对应一个 block) 数据至 storage 内存。其中: 21 | 22 | * blockId:缓存到内存后的 block 的 blockId 23 | * values:对象类型的迭代器,对应一个 partition 的数据 24 | 25 | 整个流程还可以细化为以下两个子流程: 26 | 27 | 1. unroll block:展开迭代器 28 | 2. store unrolled to storage memory:将展开后的数据存入 storage 内存 29 | 30 | ### 1-1: unroll block 31 | 一图胜千言,我们先来看看 unroll 的流程 32 | 33 | 34 | ![](http://upload-images.jianshu.io/upload_images/204749-e68065100e830d05.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 35 | 36 | 37 | 我们先对上图中的流程做进一步的说明,然后再简洁的描述下整个过程以加深印象 38 | 39 | #### 1-1-1: 为什么申请初始 unroll 内存不成功还继续往下走? 40 | 初始的用于 unroll 的内存大小由 ```spark.storage.unrollMemoryThreshold``` 控制,默认为 1M。继续往下走主要由两个原因: 41 | 42 | * 由于初始 unroll 大小是可以设置的,如果不小心设置了过大,比如 1G,这时申请这么大的 storage 内存很可能失败,但 block 的真正大小可能远远小于该值;即使该值设置的比较合理,block 也很可能比初始 unroll 大小要小 43 | * 对于 ```MemoryStore#putIteratorAsValues```,即使 block 大小比初始 unroll 大小要大,甚至最终都没能完整的把 values unroll 也是有用的,这个将在后文展开,这里先请关注返回值 ```new PartiallyUnrolledIterator(...)``` 44 | 45 | #### 1-1-2: 关于 vector: SizeTrackingVector 46 | 如流程图中所示,在 partition 对应的 iterator 不断被展开的过程中,每展开获取一个记录,就加到 vector 中,该 vector 为 SizeTrackingVector 类型,是一个只能追加的 buffer(内部通过数组实现),并持续记录自身的估算大小。从这里也可以看出,unroll 过程使用的内存都是 ON_HEAP 的。 47 | 48 | 整个展开过程,说白了就是尽量把更多的 records 塞到这个 vector 中。因为所有展开的 records 都存入了 vector 中,所以从图中可以看出,每当在计算 vector 的估算 size 后,就会与累计已申请的 unroll 内存大小进行比较,如果 vector 的估算 size 更大,说明申请的 unroll 内存不够,就会触发申请更多的 unroll 内存(具体是申请 vector 估算大小的 1.5 倍减去已申请的 unroll 总内存),这: 49 | 50 | * 一是为了接下去的展开操作申请 unroll 内存 51 | * 二也是为了尽量保障向 MemoryManager 申请的 unroll 内存能稍大于真实消耗的 unroll 内存,以避免 OOM(若向 MemoryManager 申请的 unroll 内存小于真实使用的,那么就会导致 MemoryManager 认为有比真实情况下更多的空闲内存,如果使用了这部分不存在的空闲内存就会 OOM) 52 | 53 | 如图所示,要符合一定的条件才 check unroll memory 是否够用,也就是 vector 计算其估算大小并判断是否大于已申请的 unroll memory size。这里是**每展开 16 条记录进行一次检查**,设置这样的间隔是因为每次估算都需要耗费数毫秒。 54 | 55 | #### 1-1-3: 继续还是停止 unroll ? 56 | 每展开一条记录后,都会判断是否还需要、还能够继续展开,当 values 还有未展开的 record 且还有 unroll 内存来展开时就会继续展开,将 record 追加到 vector 中。 57 | 58 | 需要注意的是,只有当 keepUnrolling 为 true 时(不管 values.hasNext 是否为 true)才会进入 ```store unrolled to storage memory``` 流程。这样的逻辑其实有些问题,我们先来看看其实现代码: 59 | 60 | ``` 61 | while (values.hasNext && keepUnrolling) { 62 | vector += values.next() 63 | if (elementsUnrolled % 16 == 0) { 64 | // currentSize 为 vector 的估算大小 65 | val currentSize = vector.estimateSize() 66 | if (currentSize >= memoryThreshold) { 67 | // 申请 size 为 amountToRequest 的估算大小,memoryGrowthFactor 为 1.5 68 | val amountToRequest = (currentSize * memoryGrowthFactor - memoryThreshold).toLong 69 | keepUnrolling = reserveUnrollMemoryForThisTask(blockId, amountToRequest, MemoryMode.ON_HEAP) 70 | } 71 | memoryThreshold += amountToRequest 72 | } 73 | } 74 | elementsUnrolled += 1 75 | } 76 | 77 | if (keepUnrolling) { 78 | // store unrolled to storage memory 79 | } 80 | ``` 81 | 82 | 此时,假设 keepUnrolling 为 true, ```values.hasNext``` 为 true,也就是还有一些记录没有展开(在假设剩余未展开的 records 总大小为 1M),进入循环后,展开一条记录追加到 vector 中后,恰好 ```elementsUnrolled % 16 == 0``` 且 ```currentSize >= memoryThreshold```。根据 ```val amountToRequest = (currentSize * memoryGrowthFactor - memoryThreshold).toLong``` 计算出要为了展开剩余 records 本次要申请的 unroll 内存大小为 amountToRequest,大小为 5M,这时候实际上最大能申请的 unroll 内存大小为 3M,那么申请就失败了,keepUnrolling 为 false,此时进入下一次循环判断就失败了,整个展开过程也就失败了,但事实上剩余能申请的 unroll 内存大小是足以满足剩余的 records 的。 83 | 84 | 一个简单的治标不治本的改进方案是将 memoryGrowthFactor 的值设置的更小(当前为 1.5),该值越小发生上述情况的概率越小,并且,这里的申请内存其实只是做一些数值上的状态更新,并不会发生耗资源或耗时的操作,所以多申请几次并不会带来什么性能下降。 85 | 86 | 回到当前的实现中来,当循环结束,若 keepUnrolling 为 true ,values 一定被全部展开;若 keepUnrolling 为 false(存在展开最后一条 record 后 check 出 vector 估算 size 大于已申请 unroll 总内存并申请失败的情况),则无论 values 有没有被全部展开,都说明能申请到的总 unroll 内存是不足以展开整个 values 的,这就意味着缓存该 partition 至内存失败。 87 | 88 | 需要注意的是,缓存到内存失败并不代表整个缓存动作是失败的,根据 StorageLevel 还可能会缓存到磁盘。 89 | 90 | --- 91 | 92 | ### 1-2: store unrolled to storage memory 93 | 94 | 95 | ![](http://upload-images.jianshu.io/upload_images/204749-aab49ef818fca7ba.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 96 | 97 | #### 1-2-1: 真正的 block 即 DeserializedMemoryEntry 98 | 这一流程说白了就是将 unroll 的总内存占用转化为 storage 的内存占用,事实上真正保存 records 的 vector 中的数组也被移到了 entry 中(引用传递)。entry 是这样被构造的: 99 | 100 | ``` 101 | private case class DeserializedMemoryEntry[T]( 102 | value: Array[T], 103 | size: Long, 104 | classTag: ClassTag[T]) extends MemoryEntry[T] { 105 | val memoryMode: MemoryMode = MemoryMode.ON_HEAP 106 | } 107 | 108 | val arrayValues = vector.toArray 109 | vector = null 110 | val entry = new DeserializedMemoryEntry[T](arrayValues, SizeEstimator.estimate(arrayValues), classTag) 111 | ``` 112 | 113 | entry 的成员 value 为 vector 中保存 records 的数组,entry 的 size 成员为该数组的估算大小。DeserializedMemoryEntry 继承于 MemoryEntry,MemoryEntry 的另一个子类是 SerializedMemoryEntry,对应的是一个序列化的 block。在 MemoryStore 中,以 ```entries: LinkedHashMap[BlockId, MemoryEntry[_]]``` 的形式维护 blockId 及序列化或非序列化的 block 的映射。 114 | 115 | 从这里,你也可以看出,当前缓存非序列化的 RDD 只能使用 ON_HEAP 内存。 116 | 117 | #### 1-2-2: unroll 内存的多退少补 118 | 这之后,再次使用 array[record] 的估算大小与 unroll 总内存进行比较: 119 | 120 | * 若前者较大,则计算要再申请多少 unroll 内存(两者之差)并申请之,申请的结果为 acquireExtra 121 | * 若后者较大,则说明申请了在 unroll 过程中申请了过多的内存,则释放多出来的部分(两者之差)。会出现多出来的情况有两点原因: 122 | * 这次 array[record] 的估算结果更为准确 123 | * 在 unroll 过程中由于每次申请的内存是 ```val amountToRequest = (currentSize * memoryGrowthFactor - memoryThreshold).toLong```,这样的算法是容易导致申请多余实际需要的 124 | 125 | #### 1-2-3: transform unroll to storage 126 | 将 unroll 内存占用转为 storage 内存占用实现如下: 127 | 128 | ``` 129 | def transferUnrollToStorage(amount: Long): Unit = { 130 | // Synchronize so that transfer is atomic 131 | memoryManager.synchronized { 132 | releaseUnrollMemoryForThisTask(MemoryMode.ON_HEAP, amount) 133 | val success = memoryManager.acquireStorageMemory(blockId, amount, MemoryMode.ON_HEAP) 134 | assert(success, "transferring unroll memory to storage memory failed") 135 | } 136 | } 137 | ``` 138 | 139 | 可以看到,这是一个 memoryManager 级别的同步操作,不用担心刚被 release 的 unroll 内存在占用等量的 storage 内存之前会在其他地方被占用。 140 | 141 | 在 ```UnifiedMemoryManager``` 的内存划分中,unroll 内存其实就是 storage 内存,所以上面代码所做的事看起来没什么意义,先让 storage used memory 减去某个值,再加上该值,结果是没变。那为什么还要这么做呢?我想是为了 MemoryStore 和 MemoryManager 的解耦,对于 MemoryStore 来说其并不知道在 MemoryManager 中 unroll 内存就是 storage 内存,如果之后 MemoryManager 不是这样实现了,对 MemoryStore 也不会有影响。 142 | 143 | #### 1-2-4: enoughStorageMemory 及结果 144 | 在这一流程的最后,会根据 enoughStorageMemory 为 true 后 false,返回不同的结果。只有当以上流程中,partition 被完全展开并成功存放到 storage 内存中 enoughStorageMemory 才为 true;即使partition 全部展开,并生成了 entry,如果最终能申请的最多的 storage 内存还是小于 array[record] 的估算 size,整个 cache block to memory 的操作也是失败的,此时的 enoughStorageMemory 为 false。 145 | 146 | 如果最终结果是成功的,返回值为 array[record] 的估算 size。如果是失败的,包括 unroll 失败,将返回 PartiallyUnrolledIterator 对象实例: 147 | 148 | ``` 149 | class PartiallyUnrolledIterator[T]( 150 | memoryStore: MemoryStore, 151 | memoryMode: MemoryMode, 152 | unrollMemory: Long, 153 | private[this] var unrolled: Iterator[T], 154 | rest: Iterator[T]) 155 | extends Iterator[T] 156 | ``` 157 | 158 | 该实例(也是个迭代器)由部分已经展开的迭代器(unrolled)以及剩余未展开的迭代器(rest)组合而成,调用者可根据 StorageLevel 是否还包含 Disk 级别来决定是 close 还是使用该返回值将 block 持久化到磁盘(可以避免部分的 unroll 操作)。 159 | 160 | ## 2: 缓存序列化 RDD(支持 ON_HEAP 和 OFF_HEAP) 161 | 有了上面分析缓存非序列化 RDD 至内存的经验,再来看下面的缓存序列化 RDD 至内存的图会发现有一些相似,也有一些不同。在下面的流程图中,包含了 unroll 过程和 store block to storage memory 过程。为了方便分析,我将整个流程分为三大块: 162 | 163 | * 红框部分:初始化 allocator、bbos、serializationStream 164 | * 灰框部分:展开 values 并 append 到 serializationStream 中 165 | * 篮框部分:store block to storage memory 166 | 167 | 168 | ![](http://upload-images.jianshu.io/upload_images/204749-83153121577110a3.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 169 | 170 | 171 | ### 2-1: 初始化 allocator、bbos、serializationStream 172 | ```allocator: Int => ByteBuffer``` 是一个函数变量,用来分配内存,它是这样被构造的: 173 | 174 | ``` 175 | val allocator = memoryMode match { 176 | case MemoryMode.ON_HEAP => ByteBuffer.allocate _ 177 | case MemoryMode.OFF_HEAP => Platform.allocateDirectBuffer _ 178 | } 179 | ``` 180 | 181 | * 当 MemoryMode 为 ON_HAP 时,allocator 分配的是 HeapByteBuffer 形式的堆上内存 182 | * 当 MemoryMode 为 OFF_HEAP 时,allocator 分配的是 DirectByteBuffer 形式的堆外内存。需要特别注意的是,DirectByteBuffer 本身是堆内的对象,这里的堆外是指其指向的内存是堆外的 183 | 184 | HeapByteBuffer 通过调用 new 分配内存,而 DirectByteBuffer 最终调用 C++ 的 malloc 方法分配,在分配和销毁上 HeapByteBuffer 要比 DirectByteBuffer 稍快。但在网络读写和文件读写方面,DirectByteBuffer 比 HeapByteBuffer 更快(具体原因请自行调研,不是本文重点),这对经常会被网络读写的 block 来说很有意义。 185 | 186 | 另外,HeapByteBuffer 指向的内存受 GC 管理;而 DirectByteBuffer 指向的内存不受 GC 管理,可减小 GC 压力。DirectByteBuffer 指向的内存会在两种情况下会释放: 187 | 188 | * remove 某个 block 时,会通过 DirectByteBuffer 的 cleaner 来释放其指向的内存 189 | * 当 BlockManager stop 时,会 clear 整个 MemoryStore 中的所有 blocks,这时会释放所有的 DirectByteBuffers 及其指向的内存 190 | 191 | 接下来是: 192 | 193 | ``` 194 | val bbos = new ChunkedByteBufferOutputStream(initialMemoryThreshold.toInt, allocator) 195 | ``` 196 | 197 | ChunkedByteBufferOutputStream 包含一个 ```chunks: ArrayBuffer[ByteBuffer]```,该数组中的 ByteBuffer 通过 allocator 创建,用于真正存储 unrolled 数据。再次说明,如果是 ON_HEAP,这里的 ByteBuffer 是 HeapByteBuffer;而如果是 OFF_HEAP,这里的 ByteBuffer 则是 DirectByteBuffer。 198 | 199 | bbos 之后将用于建构构造 ```serializeStream: SerializationStream```,records 将一条条写入 serializeStream,serializeStream 最终会将 records 写入 bbos 的 ```chunks: ArrayBuffer[ByteBuffer]``` 中,一条 record 对应 ByteBuffer 元素。 200 | 201 | ### 2-2: 展开 values 并 append 到 serializationStream 中 202 | 具体展开的流程与 “缓存非序列化 RDD” 类似(```serializationStream.writeObject(values.next())(classTag)``` 也在上一小节进行了说明),最大的区别是在没展开一条 record 都会调用 ```reserveAdditionalMemoryIfNecessary()```,实现如下 203 | 204 | ``` 205 | def reserveAdditionalMemoryIfNecessary(): Unit = { 206 | if (bbos.size > unrollMemoryUsedByThisBlock) { 207 | val amountToRequest = bbos.size - unrollMemoryUsedByThisBlock 208 | keepUnrolling = reserveUnrollMemoryForThisTask(blockId, amountToRequest, memoryMode) 209 | if (keepUnrolling) { 210 | unrollMemoryUsedByThisBlock += amountToRequest 211 | } 212 | } 213 | } 214 | ``` 215 | 216 | 由于是序列化的数据,这里的 bbos.size 是准确值而不是估算值。reserveAdditionalMemoryIfNecessary 说白了就是计算真实已经占用的 unroll 内存(bbos.size)比已经申请的 unrolll 总内存 size 大多少,并申请相应 MemoryMode 的 unroll 内存来使得申请的 unroll 总大小和实际使用的保持一致。如果申请失败,则 keepUnrolling 为 false,那么缓存该非序列化 block 至内存就失败了,将返回 PartiallySerializedBlock 类型对象。 217 | 218 | 在完整展开后,会再调用一次 reserveAdditionalMemoryIfNecessary,以最终确保实际申请的 unroll 内存和实际占用的大小相同。 219 | 220 | ### 2-3: store block to storage memory 221 | 这里将 bbos 中的 ```ArrayBuffer[ByteBuffer]``` 转化为 ChunkedByteBuffer 对象,ChunkedByteBuffer 是只读的物理上是以多块内存组成(即 Array[ByteBuffer])。 222 | 223 | 再以该 ChunkedByteBuffer 对象构造真正的序列化的 block,即 ```entry: SerializedMemoryEntry```,构造时同样会传入 MemoryMode。 224 | 225 | 最后将 entry 加到 ```entries: LinkedHashMap[BlockId, MemoryEntry[_]]``` 中。 226 | 227 | 与 “缓存非序列化 RDD” 相同,如果缓存序列化 block 至内存失败了,根据 StorageLevel 还有机会缓存到磁盘上。 228 | 229 | ## 总结 230 | 上篇文章主要讲解 MemoryManager 是怎样管理内存的,即如何划分内存区域、分配踢除策略、借用策略等,并不涉及真正的内存分配,只做数值上的管理,是处于中心的storage 内存调度 “调度”。而本文则分析了在最重要的缓存非序列化/序列化 RDD 至内存的场景下,storage 内存真正是如何分配使用的,即以什么样的 MemoryMode、什么样的分配逻辑及方式,还介绍了 block 在 memory 中的表现形式等。 231 | 232 | --- 233 | 234 | 关注公众号回复:内存管理上 235 | 查看 Spark 是如何详细划分内存以及内存的分配策略 236 | 237 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 238 | -------------------------------------------------------------------------------- /spark-core/Spark-核心-RDD-剖析(上).md: -------------------------------------------------------------------------------- 1 | 本文将通过描述 Spark RDD 的五大核心要素来描述 RDD,若希望更全面了解 RDD 的知识,请移步 RDD 论文:[RDD:基于内存的集群计算容错抽象](http://shiyanjun.cn/archives/744.html) 2 | 3 | Spark 的五大核心要素包括: 4 | 5 | * partition 6 | * partitioner 7 | * compute func 8 | * dependency 9 | * preferredLocation 10 | 11 | 下面一一来介绍 12 | 13 | ##(一): partition 14 | ###partition 个数怎么定 15 | RDD 由若干个 partition 组成,共有三种生成方式: 16 | 17 | * 从 Scala 集合中创建,通过调用 ```SparkContext#makeRDD``` 或 ```SparkContext#parallelize``` 18 | * 加载外部数据来创建 RDD,例如从 HDFS 文件、mysql 数据库读取数据等 19 | * 由其他 RDD 执行 transform 操作转换而来 20 | 21 | 那么,在使用上述方法生成 RDD 的时候,会为 RDD 生成多少个 partition 呢?一般来说,加载 Scala 集合或外部数据来创建 RDD 时,是可以指定 partition 个数的,若指定了具体值,那么 partition 的个数就等于该值,比如: 22 | 23 | ``` 24 | val rdd1 = sc.makeRDD( scalaSeqData, 3 ) //< 指定 partition 数为3 25 | val rdd2 = sc.textFile( hdfsFilePath, 10 ) //< 指定 partition 数为10 26 | ``` 27 | 28 | 若没有指定具体的 partition 数时的 partition 数为多少呢? 29 | 30 | * 对于从 Scala 集合中转换而来的 RDD:默认的 partition 数为 defaultParallelism,该值在不同的部署模式下不同: 31 | * Local 模式:本机 cpu cores 的数量 32 | * Mesos 模式:8 33 | * Yarn:max(2, 所有 executors 的 cpu cores 个数总和) 34 | * 对于从外部数据加载而来的 RDD:默认的 partition 数为 ```min(defaultParallelism, 2)``` 35 | * 对于执行转换操作而得到的 RDD:视具体操作而定,如 map 得到的 RDD 的 partition 数与 父 RDD 相同;union 得到的 RDD 的 partition 数为父 RDDs 的 partition 数之和... 36 | 37 | --- 38 | 39 | ### partition 的定义 40 | 我们常说,partition 是 RDD 的数据单位,代表了一个分区的数据。但这里千万不要搞错了,partition 是逻辑概念,是代表了一个分片的数据,而不是包含或持有一个分片的数据。 41 | 42 | 真正直接持有数据的是各个 partition 对应的迭代器,要再次注意的是,partition 对应的迭代器访问数据时也不是把整个分区的数据一股脑加载持有,而是像常见的迭代器一样一条条处理。举个例子,我们把 HDFS 上10G 的文件加载到 RDD 做处理时,并不会消耗10G 的空间,如果没有 shuffle 操作(shuffle 操作会持有较多数据在内存),那么这个操作的内存消耗是非常小的,因为在每个 task 中都是一条条处理处理的,在某一时刻只会持有一条数据。这也是初学者常有的理解误区,一定要注意 Spark 是基于内存的计算,但不会傻到什么时候都把所有数据全放到内存。 43 | 44 | 让我们来看看 Partition 的定义帮助理解: 45 | 46 | ``` 47 | trait Partition extends Serializable { 48 | def index: Int 49 | 50 | override def hashCode(): Int = index 51 | } 52 | ``` 53 | 54 | 在 trait Partition 中仅包含返回其索引的 index 方法。很多具体的 RDD 也会有自己实现的 partition,比如: 55 | 56 | **KafkaRDDPartition 提供了获取 partition 所包含的 kafka msg 条数的方法** 57 | 58 | ``` 59 | class KafkaRDDPartition( 60 | val index: Int, 61 | val topic: String, 62 | val partition: Int, 63 | val fromOffset: Long, 64 | val untilOffset: Long, 65 | val host: String, 66 | val port: Int 67 | ) extends Partition { 68 | /** Number of messages this partition refers to */ 69 | def count(): Long = untilOffset - fromOffset 70 | } 71 | ``` 72 | 73 | **UnionRDD 的 partition 类 UnionPartition 提供了获取依赖的父 partition 及获取优先位置的方法** 74 | 75 | ``` 76 | private[spark] class UnionPartition[T: ClassTag]( 77 | idx: Int, 78 | @transient private val rdd: RDD[T], 79 | val parentRddIndex: Int, 80 | @transient private val parentRddPartitionIndex: Int) 81 | extends Partition { 82 | 83 | var parentPartition: Partition = rdd.partitions(parentRddPartitionIndex) 84 | 85 | def preferredLocations(): Seq[String] = rdd.preferredLocations(parentPartition) 86 | 87 | override val index: Int = idx 88 | } 89 | ``` 90 | 91 | ###partition 与 iterator 方法 92 | RDD 的 ```def iterator(split: Partition, context: TaskContext): Iterator[T]``` 方法用来获取 split 指定的 Partition 对应的数据的迭代器,有了这个迭代器就能一条一条取出数据来按 compute chain 来执行一个个transform 操作。iterator 的实现如下: 93 | 94 | ``` 95 | final def iterator(split: Partition, context: TaskContext): Iterator[T] = { 96 | if (storageLevel != StorageLevel.NONE) { 97 | SparkEnv.get.cacheManager.getOrCompute(this, split, context, storageLevel) 98 | } else { 99 | computeOrReadCheckpoint(split, context) 100 | } 101 | } 102 | ``` 103 | 104 | def 前加了 final 说明该函数是不能被子类重写的,其先判断 RDD 的 storageLevel 是否为 NONE,若不是,则尝试从缓存中读取,读取不到则通过计算来获取该 Partition 对应的数据的迭代器;若是,尝试从 checkpoint 中获取 Partition 对应数据的迭代器,若 checkpoint 不存在则通过计算来获取。 105 | 106 | 刚刚介绍了如果从 cache 或者 checkpoint 无法获得 Partition 对应的数据的迭代器,则需要通过计算来获取,这将会调用到 ```def compute(split: Partition, context: TaskContext): Iterator[T]``` 方法,各个 RDD 最大的不同也体现在该方法中。后文会详细介绍该方法 107 | 108 | ##(二): partitioner 109 | partitioner 即分区器,说白了就是决定 RDD 的每一条消息应该分到哪个分区。但只有 k, v 类型的 RDD 才能有 partitioner(当然,非 key, value 类型的 RDD 的 partitioner 为 None。 110 | 111 | partitioner 为 None 的 RDD 的 partition 的数据要么对应数据源的某一段数据,要么来自对父 RDDs 的 partitions 的处理结果。 112 | 113 | 我们先来看看 Partitioner 的定义及注释说明: 114 | 115 | ``` 116 | abstract class Partitioner extends Serializable { 117 | //< 返回 partition 数量 118 | def numPartitions: Int 119 | //< 返回 key 应该属于哪个 partition 120 | def getPartition(key: Any): Int 121 | } 122 | ``` 123 | 124 | Partitioner 共有两种实现,分别是 HashPartitioner 和 RangePartitioner 125 | 126 | ###HashPartitioner 127 | 先来看 HashPartitioner 的实现(省去部分代码): 128 | 129 | ``` 130 | class HashPartitioner(partitions: Int) extends Partitioner { 131 | require(partitions >= 0, s"Number of partitions ($partitions) cannot be negative.") 132 | 133 | def numPartitions: Int = partitions 134 | 135 | def getPartition(key: Any): Int = key match { 136 | case null => 0 137 | case _ => Utils.nonNegativeMod(key.hashCode, numPartitions) 138 | } 139 | 140 | ... 141 | } 142 | 143 | // x 对 mod 求于,若结果为正,则返回该结果;若结果为负,返回结果加上 mod 144 | def nonNegativeMod(x: Int, mod: Int): Int = { 145 | val rawMod = x % mod 146 | rawMod + (if (rawMod < 0) mod else 0) 147 | } 148 | ``` 149 | 150 | ```numPartitions``` 直接返回主构造函数中传入的 partitions 参数,之前在有本书里看到说 Partitioner 不仅决定了一条 record 应该属于哪个 partition,还决定了 partition 的数量,其实这句话的后半段的有误的,Partitioner 并不能决定一个 RDD 的 partition 数,Partitioner 方法返回的 partition 数是直接返回外部传入的值。 151 | 152 | ```getPartition``` 方法也不复杂,主要做了: 153 | 154 | 1. 为参数 key 计算一个 hash 值 155 | 2. 若该哈希值对 partition 个数取余结果为正,则该结果即该 key 归属的 partition index;否则,以该结果加上 partition 个数为 partition index 156 | 157 | 从上面的分析来看,当 key, value 类型的 RDD 的 key 的 hash 值分布不均匀时,会导致各个 partition 的数据量不均匀,极端情况下一个 partition 会持有整个 RDD 的数据而其他 partition 则不包含任何数据,这显然不是我们希望看到的,这时就需要 RangePartitioner 出马了。 158 | 159 | ###RangePartitioner 160 | 上文也提到了,HashPartitioner 可能会导致各个 partition 数据量相差很大的情况。这时,初衷为使各个 partition 数据分布尽量均匀的 RangePartitioner 便有了用武之地。 161 | 162 | RangePartitioner 将一个范围内的数据映射到 partition,这样两个 partition 之间要么是一个 partition 的数据都比另外一个大,或者小。RangePartitioner采用水塘抽样算法,比 HashPartitioner 耗时,具体可见:[Spark分区器HashPartitioner和RangePartitioner代码详解](http://www.iteblog.com/archives/1522) 163 | 164 | --- 165 | 166 | 欢迎关注我的微信公众号:FunnyBigData 167 | 168 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 169 | -------------------------------------------------------------------------------- /spark-core/Spark-核心-RDD-剖析(下).md: -------------------------------------------------------------------------------- 1 | 上文[Spark 核心 RDD 剖析(上)](http://www.jianshu.com/p/207607888767)介绍了 RDD 两个重要要素:partition 和 partitioner。这篇文章将介绍剩余的部分,即 compute func、dependency、preferedLocation 2 | 3 | ##compute func 4 | 在前一篇文章中提到,当调用 ```RDD#iterator``` 方法无法从缓存或 checkpoint 中获取指定 partition 的迭代器时,就需要调用 ```compute``` 方法来获取,该方法声明如下: 5 | 6 | ``` 7 | def compute(split: Partition, context: TaskContext): Iterator[T] 8 | ``` 9 | 10 | 每个具体的 RDD 都必须实现自己的 compute 函数。从上面的分析我们可以联想到,任何一个 RDD 的任意一个 partition 都首先是通过 compute 函数计算出的,之后才能进行 cache 或 checkpoint。接下来我们来对几个常用 transformation 操作对应的 RDD 的 compute 进行分析 11 | 12 | ###map 13 | 首先来看下 map 的实现: 14 | 15 | ``` 16 | def map[U: ClassTag](f: T => U): RDD[U] = withScope { 17 | val cleanF = sc.clean(f) 18 | new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF)) 19 | } 20 | ``` 21 | 22 | 我们调用 map 时,会传入匿名函数 ```f: T => U```,该函数将一个类型 T 实例转换成一个类型 U 的实例。在 map 函数中,将该函数进一步封装成 ```(context, pid, iter) => iter.map(cleanF)``` 的函数,该函数以迭代器作为参数,对迭代出的每一个元素执行 f 函数,然后以该封装后的函数作为参数来构造 MapPartitionsRDD,接下来看看 ```MapPartitionsRDD#compute``` 是怎么实现的: 23 | 24 | ``` 25 | override def compute(split: Partition, context: TaskContext): Iterator[U] = 26 | f(context, split.index, firstParent[T].iterator(split, context)) 27 | ``` 28 | 29 | 上面代码中的 firstParent 是指本 RDD 的依赖 ```dependencies: Seq[Dependency[_]]``` 中的第一个,MapPartitionsRDD 的依赖中只有一个父 RDD。而 MapPartitionsRDD 的 partition 与其唯一的父 RDD partition 是一一对应的,所以其 compute 方法可以描述为:对父 RDD partition 中的每一个元素执行传入 map 的方法得到自身的 partition 及迭代器 30 | 31 | ###groupByKey 32 | 与 map、union 不同,groupByKey 是一个会产生宽依赖的 transform,其最终生成的 RDD 是 ShuffledRDD,来看看其 compute 实现: 33 | 34 | ``` 35 | override def compute(split: Partition, context: TaskContext): Iterator[(K, C)] = { 36 | val dep = dependencies.head.asInstanceOf[ShuffleDependency[K, V, C]] 37 | SparkEnv.get.shuffleManager.getReader(dep.shuffleHandle, split.index, split.index + 1, context) 38 | .read() 39 | .asInstanceOf[Iterator[(K, C)]] 40 | } 41 | ``` 42 | 43 | 可以看到,ShuffledRDD 的 compute 使用 ShuffleManager 来获取一个 reader,该 reader 将从本地或远程 BlockManager 拉取 map output 的 file 数据,每个 reduce task 拉取一个 partition 数据。 44 | 45 | 对于其他生成 ShuffledRDD 的 transform 的 compute 操作也是如此,比如 reduceByKey,join 等 46 | 47 | ##dependency 48 | RDD 依赖是一个 Seq 类型:```dependencies_ : Seq[Dependency[_]]```,因为一个 RDD 可以有多个父 RDD。共有两种依赖: 49 | 50 | * 窄依赖:父 RDD 的 partition 至多被一个子 RDD partition 依赖 51 | * 宽依赖:父 RDD 的 partition 被多个子 RDD partitions 依赖 52 | 53 | 窄依赖共有两种实现,一种是一对一的依赖,即 OneToOneDependency: 54 | 55 | ``` 56 | @DeveloperApi 57 | class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) { 58 | override def getParents(partitionId: Int): List[Int] = List(partitionId) 59 | } 60 | ``` 61 | 62 | 从其 getParents 方法可以看出 OneToOneDependency 的依赖关系下,子 RDD 的 partition 仅依赖于唯一 parent RDD 的相同 index 的 partition。另一种窄依赖的实现是 RangeDependency,它仅仅被 UnionRDD 使用,UnionRDD 把多个 RDD 合成一个 RDD,这些 RDD 是被拼接而成,其 getParents 实现如下: 63 | 64 | ``` 65 | override def getParents(partitionId: Int): List[Int] = { 66 | if (partitionId >= outStart && partitionId < outStart + length) { 67 | List(partitionId - outStart + inStart) 68 | } else { 69 | Nil 70 | } 71 | } 72 | ``` 73 | 74 | 宽依赖只有一种实现,即 ShuffleDependency,宽依赖支持两种 Shuffle Manager,即 ```HashShuffleManager``` 和 ```SortShuffleManager```,Shuffle 相关内容以后会专门写文章介绍 75 | 76 | ##preferedLocation 77 | preferedLocation 即 RDD 每个 partition 对应的优先位置,每个 partition 对应一个```Seq[String]```,表示一组优先节点的 host。 78 | 79 | 要注意的是,并不是每个 RDD 都有 preferedLocation,比如从 Scala 集合中创建的 RDD 就没有,而从 HDFS 读取的 RDD 就有,其 partition 对应的优先位置及对应的 block 所在的各个节点。 80 | 81 | --- 82 | 83 | 欢迎关注我的微信公众号:FunnyBigData 84 | 85 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 86 | -------------------------------------------------------------------------------- /spark-core/Spark的位置优先--TaskSetManager-的有效-Locality-Levels.md: -------------------------------------------------------------------------------- 1 | > based on spark-1.5.1 standalone mode 2 | 3 | 在Spark Application Web UI的 Stages tag 上,我们可以看到这个的表格,描述的是某个 stage 的 tasks 的一些信息,其中 Locality Level 一栏的值可以有 ```PROCESS_LOCAL、NODE_LOCAL、NO_PREF、RACK_LOCAL、ANY``` 几个值。这篇文章将从这几个值入手,从源码角度分析 TaskSetManager 的 Locality Levels 4 | 5 | 6 | ![](http://upload-images.jianshu.io/upload_images/204749-e71313dc210a07ea.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 7 | 这几个值在图中代表 task 的计算节点和 task 的输入数据的节点位置关系 8 | 9 | * ```PROCESS_LOCAL```: 数据在同一个 JVM 中,即同一个 executor 上。这是最佳数据 locality。 10 | * ```NODE_LOCAL```: 数据在同一个节点上。比如数据在同一个节点的另一个 executor上;或在 HDFS 上,恰好有 block 在同一个节点上。速度比 PROCESS_LOCAL 稍慢,因为数据需要在不同进程之间传递或从文件中读取 11 | * ```NO_PREF```: 数据从哪里访问都一样快,不需要位置优先 12 | * ```RACK_LOCAL```: 数据在同一机架的不同节点上。需要通过网络传输数据及文件 IO,比 NODE_LOCAL 慢 13 | * ```ANY```: 数据在非同一机架的网络上,速度最慢 14 | 15 | 我们在上图中看到的其实是结果,即某个 task 计算节点与其输入数据的位置关系,下面将要挖掘Spark 的调度系统如何产生这个结果,这一过程涉及 RDD、DAGScheduler、TaskScheduler,搞懂了这一过程也就基本搞懂了 Spark 的 PreferredLocations(位置优先策略) 16 | 17 | ##RDD 的 PreferredLocations 18 | 我们知道,根据输入数据源的不同,RDD 可能具有不同的优先位置,通过 RDD 的以下方法可以返回指定 partition 的最优先位置: 19 | 20 | ``` 21 | protected def getPreferredLocations(split: Partition): Seq[String] 22 | ``` 23 | 24 | 返回类型为 ```Seq[String]```,其实对应的是 ```Seq[TaskLocation]```,在返回前都会执行 ```TaskLocation#toString``` 方法。TaskLocation 是一个 trait,共有以三种实现,分别代表数据存储在不同的位置: 25 | 26 | ``` 27 | /** 28 | * 代表数据存储在 executor 的内存中,也就是这个 partition 被 cache到内存了 29 | */ 30 | private [spark] 31 | case class ExecutorCacheTaskLocation(override val host: String, executorId: String) 32 | extends TaskLocation { 33 | override def toString: String = s"${TaskLocation.executorLocationTag}${host}_$executorId" 34 | } 35 | 36 | /** 37 | * 代表数据存储在 host 这个节点的磁盘上 38 | */ 39 | private [spark] case class HostTaskLocation(override val host: String) extends TaskLocation { 40 | override def toString: String = host 41 | } 42 | 43 | /** 44 | * 代表数据存储在 hdfs 上 45 | */ 46 | private [spark] case class HDFSCacheTaskLocation(override val host: String) extends TaskLocation { 47 | override def toString: String = TaskLocation.inMemoryLocationTag + host 48 | } 49 | ``` 50 | 51 | * ExecutorCacheTaskLocation: 代表 partition 数据已经被 cache 到内存,比如 KafkaRDD 会将 partitions 都 cache 到内存,其 toString 方法返回的格式如 ```executor_$host_$executorId``` 52 | * HostTaskLocation:代表 partition 数据存储在某个节点的磁盘上(且不在 hdfs 上),其 toString 方法直接返回 host 53 | * HDFSCacheTaskLocation:代表 partition 数据存储在 hdfs 上,比如从 hdfs 上加载而来的 HadoopRDD 的 partition,其 toString 方法返回的格式如 ```hdfs_cache_$host``` 54 | 55 | 这样,我们就知道不同的 RDD 会有不同的优先位置,并且存储在不同位置的优先位置的字符串的格式是不同的,这在之后 TaskSetManager 计算 tasks 的最优本地性起了关键作用。 56 | 57 | ##DAGScheduler 生成 taskSet 58 | DAGScheduler 通过调用 submitStage 来提交一个 stage 对应的 tasks,submitStage 会调用submitMissingTasks,submitMissingTasks 会以下代码来确定每个需要计算的 task 的preferredLocations,这里调用到了 RDD#getPreferredLocs,getPreferredLocs返回的 partition 的优先位置,就是这个 partition 对应的 task 的优先位置 59 | 60 | ``` 61 | val taskIdToLocations = try { 62 | stage match { 63 | case s: ShuffleMapStage => 64 | partitionsToCompute.map { id => (id, getPreferredLocs(stage.rdd, id))}.toMap 65 | case s: ResultStage => 66 | val job = s.resultOfJob.get 67 | partitionsToCompute.map { id => 68 | val p = job.partitions(id) 69 | (id, getPreferredLocs(stage.rdd, p)) 70 | }.toMap 71 | } 72 | } catch { 73 | ... 74 | } 75 | ``` 76 | 77 | 这段调用返回的 ```taskIdToLocations: Seq[ taskId -> Seq[hosts] ]``` 会在submitMissingTasks生成要提交给 TaskScheduler 调度的 taskSet: Seq[Task[_]]时用到,如下,注意看注释: 78 | 79 | ``` 80 | val tasks: Seq[Task[_]] = try { 81 | stage match { 82 | case stage: ShuffleMapStage => 83 | partitionsToCompute.map { id => 84 | val locs = taskIdToLocations(id) 85 | val part = stage.rdd.partitions(id) 86 | //< 使用上述获得的 task 对应的优先位置,即 locs 来构造ShuffleMapTask 87 | new ShuffleMapTask(stage.id, stage.latestInfo.attemptId, 88 | taskBinary, part, locs, stage.internalAccumulators) 89 | } 90 | 91 | case stage: ResultStage => 92 | val job = stage.resultOfJob.get 93 | partitionsToCompute.map { id => 94 | val p: Int = job.partitions(id) 95 | val part = stage.rdd.partitions(p) 96 | val locs = taskIdToLocations(id) 97 | //< 使用上述获得的 task 对应的优先位置,即 locs 来构造ResultTask 98 | new ResultTask(stage.id, stage.latestInfo.attemptId, 99 | taskBinary, part, locs, id, stage.internalAccumulators) 100 | } 101 | } 102 | } catch { 103 | ... 104 | } 105 | ``` 106 | 107 | 简而言之,在 DAGScheduler 为 stage 创建要提交给 TaskScheduler 调度执行的 taskSet 时,**对于 taskSet 中的每一个 task,其优先位置与其对应的 partition 对应的优先位置一致** 108 | 109 | ## 构造 TaskSetManager,确定 locality levels 110 | 在 DAGScheduler 向 TaskScheduler 提交了 taskSet 之后,TaskSchedulerImpl 会为每个 taskSet 创建一个 TaskSetManager 对象,该对象包含taskSet 所有 tasks,并管理这些 tasks 的执行,其中就包括计算 taskSetManager 中的 tasks 都有哪些locality levels,以便在调度和延迟调度 tasks 时发挥作用。 111 | 112 | 在构造 TaskSetManager 对象时,会调用```var myLocalityLevels = computeValidLocalityLevels()```来确定locality levels 113 | 114 | ``` 115 | private def computeValidLocalityLevels(): Array[TaskLocality.TaskLocality] = { 116 | import TaskLocality.{PROCESS_LOCAL, NODE_LOCAL, NO_PREF, RACK_LOCAL, ANY} 117 | val levels = new ArrayBuffer[TaskLocality.TaskLocality] 118 | if (!pendingTasksForExecutor.isEmpty && getLocalityWait(PROCESS_LOCAL) != 0 && 119 | pendingTasksForExecutor.keySet.exists(sched.isExecutorAlive(_))) { 120 | levels += PROCESS_LOCAL 121 | } 122 | if (!pendingTasksForHost.isEmpty && getLocalityWait(NODE_LOCAL) != 0 && 123 | pendingTasksForHost.keySet.exists(sched.hasExecutorsAliveOnHost(_))) { 124 | levels += NODE_LOCAL 125 | } 126 | if (!pendingTasksWithNoPrefs.isEmpty) { 127 | levels += NO_PREF 128 | } 129 | if (!pendingTasksForRack.isEmpty && getLocalityWait(RACK_LOCAL) != 0 && 130 | pendingTasksForRack.keySet.exists(sched.hasHostAliveOnRack(_))) { 131 | levels += RACK_LOCAL 132 | } 133 | levels += ANY 134 | logDebug("Valid locality levels for " + taskSet + ": " + levels.mkString(", ")) 135 | levels.toArray 136 | } 137 | ``` 138 | 139 | 这个函数是在解决4个问题: 140 | 141 | 1. taskSetManager 的 locality levels是否包含 ```PROCESS_LOCAL``` 142 | 2. taskSetManager 的 locality levels是否包含 ```NODE_LOCAL``` 143 | 3. taskSetManager 的 locality levels是否包含 ```NO_PREF``` 144 | 4. taskSetManager 的 locality levels是否包含 ```RACK_LOCAL``` 145 | 146 | 让我们来各个击破 147 | 148 | ###taskSetManager 的 locality levels是否包含 ```PROCESS_LOCAL``` 149 | 关键代码: 150 | 151 | ``` 152 | if (!pendingTasksForExecutor.isEmpty && getLocalityWait(PROCESS_LOCAL) != 0 && 153 | pendingTasksForExecutor.keySet.exists(sched.isExecutorAlive(_))) { 154 | levels += PROCESS_LOCAL 155 | } 156 | ``` 157 | 158 | 真正关键的其实是这段代码,其他两个判断都很简单 159 | 160 | ``` 161 | pendingTasksForExecutor.keySet.exists(sched.isExecutorAlive(_)) 162 | ``` 163 | 164 | 要搞懂这段代码,首先要搞明白下面两个问题 165 | 166 | 1. pendingTasksForExecutor是怎么来的,什么含义? 167 | 2. sched.isExecutorAlive(_)干了什么? 168 | 169 | ####pendingTasksForExecutor是怎么来的,什么含义? 170 | pendingTasksForExecutor 在 TaskSetManager 构造函数中被创建,如下 171 | ```private val pendingTasksForExecutor = new HashMap[String, ArrayBuffer[Int]]```其中,key 为executoroId,value 为task index 数组。在 TaskSetManager 的构造函数中如下调用 172 | 173 | ``` 174 | for (i <- (0 until numTasks).reverse) { 175 | addPendingTask(i) 176 | } 177 | ``` 178 | 179 | 这段调用为 taskSetManager 中的优先位置类型为 ```ExecutorCacheTaskLocation```(这里通过 toString 返回的格式进行匹配) 的 tasks 调用 addPendingTask,addPendingTask 获取 task 的优先位置,即一个 ```Seq[String]```;再获得这组优先位置对应的 executors,从来反过来获得了 executor 对应 partition 缓存在其上内存的 tasks,即pendingTasksForExecutor 180 | 181 | 简单的说,**pendingTasksForExecutor保存着当前可用的 executor 对应的 partition 缓存在在其上内存中的 tasks 的映射关系** 182 | 183 | ####sched.isExecutorAlive(_)干了什么? 184 | sched.isExecutorAlive的实现为: 185 | 186 | ``` 187 | def TaskSchedulerImpl#isExecutorAlive(execId: String): Boolean = synchronized { 188 | activeExecutorIds.contains(execId) 189 | } 190 | ``` 191 | 192 | ```activeExecutorIds: HashSet[String]```保存集群当前所有可用的 executor id(这里对 executor 的 free cores 个数并没有要求,可为0),每当 DAGScheduler 提交 taskSet 会触发 TaskScheduler 调用 resourceOffers 方法,该方法会更新当前可用的 executors 至 activeExecutorIds;当有 executor lost 的时候,TaskSchedulerImpl 也会调用 removeExecutor 来将 lost 的executor 从 activeExecutorIds 中去除 193 | 194 | 所有**isExecutorAlive就是判断参数中的 executor id 当前是否 active** 195 | 196 | --- 197 | 198 | 结合以上两段代码的分析,可以知道这行代码```pendingTasksForExecutor.keySet.exists(sched.isExecutorAlive(_))```的含义: **taskSetManager 的所有对应 partition 数据缓存在 executor 内存中的 tasks 对应的所有 executor,是否有任一 active,若有则返回 true;否则返回 false** 199 | 200 | 这样,也就知道了如何去判断一个 taskSetManager 对象的 locality levels 是否包含 PROCESS_LOCAL 201 | 202 | ###taskSetManager 的 locality levels是否包含 ```NODE_LOCAL``` 203 | 有了上面对 PROCESS_LOCAL 的详细分析,这里对是否包含 NODE_LOCAL 只做简要分析。最关键代码 204 | 205 | ```pendingTasksForHost.keySet.exists(sched.hasExecutorsAliveOnHost(_))```,其中 206 | 207 | * pendingTasksForHost: ```HashMap[String, ArrayBuffer[Int]]```类型,key 为 host,value 为 preferredLocations 包含该 host 的 tasks indexs 数组 208 | * sched.hasExecutorsAliveOnHost(_): 209 | 源码如下,其中executorsByHost为 ```HashMap[String, HashSet[String]]``` 类型,key 为 host,value 为该 host 上的 active executors 210 | 211 | ``` 212 | def hasExecutorsAliveOnHost(host: String): Boolean = synchronized { 213 | executorsByHost.contains(host) 214 | } 215 | ``` 216 | 217 | 这样,也就知道如何判断 taskSetManager 的 locality levels:taskSetManager 的所有 tasks 对应的所有 hosts,是否有任一是 tasks 的优先位置 hosts,若有返回 true;否则返回 false 218 | 219 | ###taskSetManager 的 locality levels是否包含 ```RACK_LOCAL``` 220 | 关键代码:```pendingTasksForRack.keySet.exists(sched.hasHostAliveOnRack(_))```,其中 221 | 222 | * pendingTasksForRack:```HashMap[String, ArrayBuffer[Int]]```类型,key为 rack,value 为优先位置所在的 host 属于该机架的 tasks 223 | * sched.hasHostAliveOnRack(_):源码如下,其中```hostsByRack: HashMap[String, HashSet[String]]```的 key 为 rack,value 为该 rack 上所有作为 taskSetManager 优先位置的 hosts 224 | 225 | ``` 226 | def hasHostAliveOnRack(rack: String): Boolean = synchronized { 227 | hostsByRack.contains(rack) 228 | } 229 | ``` 230 | 231 | 所以,判断 taskSetManager 的 locality levels 是否包含```RACK_LOCAL```的规则为:taskSetManager 的所有 tasks 的优先位置 host 所在的所有 racks 与当前 active executors 所在的机架是否有交集,若有则返回 true,否则返回 false 232 | 233 | ###taskSetManager 的 locality levels是否包含 ```NO_PREF``` 234 | 关键代码如下: 235 | 236 | ``` 237 | if (!pendingTasksWithNoPrefs.isEmpty) { 238 | levels += NO_PREF 239 | } 240 | ``` 241 | 242 | 如果一个 RDD 的某些 partitions 没有优先位置(如是以内存集合作为数据源且 executors 和 driver不在同一个节点),那么这个 RDD action 产生的 taskSetManagers 的 locality levels 就包含 NO_PREF 243 | 244 | 对于所有的 taskSetManager 均包含 ANY 245 | 246 | --- 247 | 248 | 欢迎关注我的微信公众号:FunnyBigData 249 | 250 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 251 | -------------------------------------------------------------------------------- /spark-core/[Spark源码剖析]-DAGScheduler划分stage.md: -------------------------------------------------------------------------------- 1 | # 划分stage源码剖析 2 | 3 | >本文基于Spark 1.3.1 4 | 5 | 先上一些stage相关的知识点: 6 | 7 | 1. DAGScheduler将Job分解成具有前后依赖关系的多个stage 8 | 2. DAGScheduler是根据ShuffleDependency划分stage的 9 | 3. stage分为ShuffleMapStage和ResultStage;一个Job中包含一个ResultStage及多个ShuffleMapStage 10 | 4. 一个stage包含多个tasks,task的个数即该stage的finalRDD的partition数 11 | 5. 一个stage中的task完全相同,ShuffleMapStage包含的都是ShuffleMapTask;ResultStage包含的都是ResultTask 12 | 13 | 14 | 下图为整个划分stage的函数调用关系图 15 | ![DAGScheduler划分stage函数调用关系.png](http://upload-images.jianshu.io/upload_images/204749-c24af52fc674f8d3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 16 | 17 | 18 | 在DAGScheduler内部通过post一个JobSubmitted事件来触发Job的提交 19 | 20 | ``` 21 | DAGScheduler.eventProcessLoop.post( JobSubmitted(...) ) 22 | DAGScheduler.handleJobSubmitted 23 | ``` 24 | 25 | 既然这两个方法都是DAGScheduler内部的实现,为什么不直接调用函数而要这样“多此一举”呢?我猜想这是为了保证整个系统事件模型的完整性。 26 | 27 | DAGScheduler.handleJobSubmitted部分源码及如下 28 | 29 | ``` 30 | private[scheduler] def handleJobSubmitted(jobId: Int, 31 | finalRDD: RDD[_], 32 | func: (TaskContext, Iterator[_]) => _, 33 | partitions: Array[Int], 34 | allowLocal: Boolean, 35 | callSite: CallSite, 36 | listener: JobListener, 37 | properties: Properties) { 38 | var finalStage: ResultStage = null 39 | try { 40 | //< 创建finalStage可能会抛出一个异常, 比如, jobs是基于一个HadoopRDD的但这个HadoopRDD已被删除 41 | finalStage = newResultStage(finalRDD, partitions.size, jobId, callSite) 42 | } catch { 43 | case e: Exception => 44 | return 45 | } 46 | 47 | //< 此处省略n行代码 48 | } 49 | ``` 50 | 51 | 该函数通过调用newResultStage函数来创建唯一的ResultStage,也就是finalStage。调用newResultStage时,传入了finalRDD、partitions.size等参数。 52 | 53 | 跟进到```DAGScheduler.newResultStage``` 54 | 55 | ``` 56 | private def newResultStage( 57 | rdd: RDD[_], 58 | numTasks: Int, 59 | jobId: Int, 60 | callSite: CallSite): ResultStage = { 61 | val (parentStages: List[Stage], id: Int) = getParentStagesAndId(rdd, jobId) 62 | val stage: ResultStage = new ResultStage(id, rdd, numTasks, parentStages, jobId, callSite) 63 | 64 | stageIdToStage(id) = stage 65 | updateJobIdStageIdMaps(jobId, stage) 66 | stage 67 | } 68 | ``` 69 | 70 | DAGScheduler.newResultStage首先调用```val (parentStages: List[Stage], id: Int) = getParentStagesAndId(rdd, jobId)```,这个调用看起来像是要先确定好该ResultStage依赖的父stages, 71 | 72 | ***问题1:那么是直接父stage呢?还是父stage及间接依赖的所有父stage呢?***记住这个问题,继续往下看。 73 | 74 | 跟进到```DAGScheduler.getParentStagesAndId```: 75 | 76 | ``` 77 | private def getParentStagesAndId(rdd: RDD[_], jobId: Int): (List[Stage], Int) = { 78 | val parentStages = getParentStages(rdd, jobId) 79 | val id = nextStageId.getAndIncrement() //< 这个调用确定了每个stage的id,划分stage时,会从右到左,因为是递归调用,其实越左的stage创建时,越早调到?try 80 | (parentStages, id) 81 | } 82 | ``` 83 | 84 | 该函数调用```getParentStages```获得parentStages,之后获取一个递增的id,连同刚获得的parentStages一同返回,并在newResultStage中,将id作为ResultStage的id。那么, 85 | ***问题2:stage id是父stage的大还是子stage的大?***。继续跟进源码,所有提问均会在后面解答。 86 | 87 | 跟到```getParentStages```里 88 | 89 | ``` 90 | //< 这个函数的实现方式比较巧妙 91 | private def getParentStages(rdd: RDD[_], jobId: Int): List[Stage] = { 92 | //< 通过vist一级一级vist得到的父stage 93 | val parents = new HashSet[Stage] 94 | //< 已经visted的rdd 95 | val visited = new HashSet[RDD[_]] 96 | val waitingForVisit = new Stack[RDD[_]] 97 | def visit(r: RDD[_]) { 98 | if (!visited(r)) { 99 | visited += r 100 | 101 | for (dep <- r.dependencies) { 102 | dep match { 103 | //< 若为宽依赖,调用getShuffleMapStage 104 | case shufDep: ShuffleDependency[_, _, _] => 105 | parents += getShuffleMapStage(shufDep, jobId) 106 | case _ => 107 | //< 若为窄依赖,将该依赖中的rdd加入到待vist栈,以保证能一级一级往上vist,直至遍历整个DAG图 108 | waitingForVisit.push(dep.rdd) 109 | } 110 | } 111 | } 112 | } 113 | waitingForVisit.push(rdd) 114 | while (waitingForVisit.nonEmpty) { 115 | visit(waitingForVisit.pop()) 116 | } 117 | parents.toList 118 | } 119 | ``` 120 | 121 | 122 | 123 | 函数getParentStages中,遍历整个RDD依赖图的finalRDD的List[dependency] (关于RDD及依赖,可参考[举例说明Spark RDD的分区、依赖](http://www.jianshu.com/p/6b9e4001723d),若遇到ShuffleDependency(即宽依赖),则调用```getShuffleMapStage(shufDep, jobId)```返回一个```ShuffleMapStage```类型对象,添加到父stage列表中。若为NarrowDenpendency,则将该NarrowDenpendency包含的RDD加入到待visit队列中,之后继续遍历待visit队列中的RDD,直到遇到ShuffleDependency或无依赖的RDD。 124 | 125 | 函数```getParentStages```的职责说白了就是:以参数rdd为起点,一级一级遍历依赖,碰到窄依赖就继续往前遍历,碰到宽依赖就调用```getShuffleMapStage(shufDep, jobId)```。这里需要特别注意的是,```getParentStages```以rdd为起点遍历RDD依赖并不会遍历整个RDD依赖图,而是一级一级遍历直到所有“遍历路线”都碰到了宽依赖就停止。剩下的事,在遍历的过程中交给```getShuffleMapStage```。 126 | 127 | 那么,让我来看看函数```getShuffleMapStage```的实现: 128 | 129 | ``` 130 | private def getShuffleMapStage( 131 | shuffleDep: ShuffleDependency[_, _, _], 132 | jobId: Int): ShuffleMapStage = { 133 | shuffleToMapStage.get(shuffleDep.shuffleId) match { 134 | case Some(stage) => stage 135 | case None => 136 | // We are going to register ancestor shuffle dependencies 137 | registerShuffleDependencies(shuffleDep, jobId) 138 | 139 | //< 然后创建新的ShuffleMapStage 140 | val stage = newOrUsedShuffleStage(shuffleDep, jobId) 141 | shuffleToMapStage(shuffleDep.shuffleId) = stage 142 | 143 | stage 144 | } 145 | } 146 | ``` 147 | 148 | 在划分stage的过程中,由于每次```shuffleDep.shuffleId```都不同且都是第一次出现,显然```shuffleToMapStage.get(shuffleDep.shuffleId)```会match到None,便会调用```newOrUsedShuffleStage```。来看看它的实现: 149 | 150 | ``` 151 | private def registerShuffleDependencies(shuffleDep: ShuffleDependency[_, _, _], jobId: Int) { 152 | val parentsWithNoMapStage = getAncestorShuffleDependencies(shuffleDep.rdd) 153 | while (parentsWithNoMapStage.nonEmpty) { 154 | //< 出栈的其实是shuffleDep的前一个宽依赖,且shuffleToMapStage不包含以该出栈宽依赖id为key的元素 155 | val currentShufDep = parentsWithNoMapStage.pop() 156 | //< 创建新的ShuffleMapStage 157 | val stage = newOrUsedShuffleStage(currentShufDep, jobId) 158 | //< 将新创建的ShuffleMapStage加入到shuffleId -> ShuffleMapStage映射关系中 159 | shuffleToMapStage(currentShufDep.shuffleId) = stage 160 | } 161 | } 162 | ``` 163 | 164 | 函数```registerShuffleDependencies```首先调用```getAncestorShuffleDependencies```,这个函数遍历参数rdd的List[dependency],若遇到ShuffleDependency,加入到```parents: Stack[ShuffleDependency[_, _, _]]```中;若遇到窄依赖,则遍历该窄依赖对应rdd的父一层依赖,知道遇到宽依赖为止。实现与```getParentStages```基本一致,不同的是这里是将宽依赖加入到parents中并返回。 165 | 166 | registerShuffleDependencies拿到各个“依赖路线”最近的所有宽依赖后。对每个宽依赖调用```newOrUsedShuffleStage```,该函数用来创建新ShuffleMapStage或获得已经存在的ShuffleMapStage。来看它的实现: 167 | 168 | ``` 169 | private def newOrUsedShuffleStage( 170 | shuffleDep: ShuffleDependency[_, _, _], 171 | jobId: Int): ShuffleMapStage = { 172 | val rdd = shuffleDep.rdd 173 | val numTasks = rdd.partitions.size 174 | val stage = newShuffleMapStage(rdd, numTasks, shuffleDep, jobId, rdd.creationSite) 175 | //< 若该shuffleDep.shulleId对应的, stage已经在MapOutputTracker中存在,那么可用的输出的数量及位置将从MapOutputTracker恢复 176 | if (mapOutputTracker.containsShuffle(shuffleDep.shuffleId)) { 177 | val serLocs = mapOutputTracker.getSerializedMapOutputStatuses(shuffleDep.shuffleId) 178 | val locs = MapOutputTracker.deserializeMapStatuses(serLocs) 179 | for (i <- 0 until locs.size) { 180 | stage.outputLocs(i) = Option(locs(i)).toList // locs(i) will be null if missing 181 | } 182 | stage.numAvailableOutputs = locs.count(_ != null) 183 | } else { 184 | // Kind of ugly: need to register RDDs with the cache and map output tracker here 185 | // since we can't do it in the RDD constructor because # of partitions is unknown 186 | logInfo("Registering RDD " + rdd.id + " (" + rdd.getCreationSite + ")") 187 | //< 否则使用shuffleDep.shuffleId, rdd.partitions.size在mapOutputTracker中注册,这会在shuffle阶段reducer从shuffleMap端fetch数据起作用 188 | mapOutputTracker.registerShuffle(shuffleDep.shuffleId, rdd.partitions.size) 189 | } 190 | stage 191 | } 192 | ``` 193 | 194 | 函数```newOrUsedShuffleStage```首先调用```newShuffleMapStage```来创建新的ShuffleMapStage,来看下```newShuffleMapStage```的实现: 195 | 196 | ``` 197 | private def newShuffleMapStage( 198 | rdd: RDD[_], 199 | numTasks: Int, 200 | shuffleDep: ShuffleDependency[_, _, _], 201 | jobId: Int, 202 | callSite: CallSite): ShuffleMapStage = { 203 | val (parentStages: List[Stage], id: Int) = getParentStagesAndId(rdd, jobId) 204 | val stage: ShuffleMapStage = new ShuffleMapStage(id, rdd, numTasks, parentStages, 205 | jobId, callSite, shuffleDep) 206 | 207 | stageIdToStage(id) = stage 208 | updateJobIdStageIdMaps(jobId, stage) 209 | stage 210 | } 211 | ``` 212 | 213 | 结合文章开始处的函数调用关系图,可以看到```newShuffleMapStage```竟然又调用```getParentStagesAndId```来获取它的parentStages。那么,文章开头处的整个函数调用流程又会继续走一遍,不同的是起点rdd不是原来的finalRDD而是变成了这里的宽依赖的rdd。 214 | 215 | 静下心来,仔细看几遍上文提到的源码及注释,其实每一次的如上图所示的递归调用,其实就只做了两件事: 216 | 1. 遍历起点RDD的依赖列表,若遇到窄依赖,则继续遍历该窄依赖的父List[RDD]的依赖,直到碰到宽依赖;若碰到宽依赖(不管是起点RDD的宽依赖还是遍历多级依赖碰到的宽依赖),则以宽依赖RDD为起点再次重复上述过程。直到到达RDD依赖图的最左端,也就是遍历到了没有依赖的RDD,则进入2 217 | 2. 达到RDD依赖图的最左端,即递归调用也到了最深得层数,```getParentStagesAndId中```,```getParentStages```第一次返回(第一次返回为空,因为最初的stage没有父stage),```val id = nextStageId.getAndIncrement()```也是第一次被调用,获得第一个stage的id,为0(注意,这个时候还没有创建第一个stage)。这之后,便调用 218 | 219 | ``` 220 | val stage: ShuffleMapStage = new ShuffleMapStage(id, rdd, numTasks, parentStages, jobId, callSite, shuffleDep) 221 | ``` 222 | 223 | 创建第一个ShuffleMapStage。至此,这一层递归调用结束,返回到上一层递归中,这一层创建的所有的ShuffleMapStage会作为下一层stage的父List[stage]用来构造上一层的stages。上一层递归调用返回后,上一层创建的stage又将作为上上层的parent List[stage]来构造上上层的stages。依次类推,直到最后的ResultStage也被创建出来为止。整个stage的划分完成。 224 | 225 | 有一个需要注意的点是,无论对于ShuffleMapStage还是ResultStage来说,task的个数即该stage的finalRDD的partition的个数,仔细查看下上文中的```newResultStage```和```newShuffleMapStage```函数可以搞明白这点,不再赘述。 226 | 227 | 最后,解答下上文中的两个问题: 228 | ***问题1:***每个stage都有一个val parents: List[Stage]成员,保存的是其直接依赖的父stages;其直接父stages又有自身依赖的父stages,以此类推,构成了整个DAG图 229 | 230 | ***问题2:***父stage的id比子stage的id小,DAG图中,越左边的stage,id越小。 231 | 232 | --- 233 | 234 | 欢迎关注我的微信公众号:FunnyBigData 235 | 236 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 237 | -------------------------------------------------------------------------------- /spark-core/[Spark源码剖析]-DAGScheduler提交stage.md: -------------------------------------------------------------------------------- 1 | DAGScheduler通过调用submitStage来提交stage,实现如下: 2 | 3 | ``` 4 |   private def submitStage(stage: Stage) { 5 |     val jobId = activeJobForStage(stage) 6 |     if (jobId.isDefined) { 7 |       logDebug("submitStage(" + stage + ")") 8 |       if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) { 9 |         //< 获取该stage未提交的父stages,并按stage id从小到大排序 10 |         val missing = getMissingParentStages(stage).sortBy(_.id) 11 |         logDebug("missing: " + missing) 12 |         if (missing.isEmpty) { 13 |           logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents") 14 |           //< 若无未提交的父stage, 则提交该stage对应的tasks 15 |           submitMissingTasks(stage, jobId.get) 16 |         } else { 17 |           //< 若存在未提交的父stage, 依次提交所有父stage (若父stage也存在未提交的父stage, 则提交之, 依次类推); 并把该stage添加到等待stage队列中 18 |           for (parent <- missing) { 19 |             submitStage(parent) 20 |           } 21 |           waitingStages += stage 22 |         } 23 |       } 24 |     } else { 25 |       abortStage(stage, "No active job for stage " + stage.id) 26 |     } 27 |   } 28 | ``` 29 | 30 | submitStage先调用```getMissingParentStages```来获取参数stageX(这里为了区分,取名为stageX)是否有未提交的父stages,若有,则依次递归(按stage id从小到大排列,也就是stage是从后往前提交的)提交父stages,并将stageX加入到```waitingStages: HashSet[Stage]```中。对于要依次提交的父stage,也是如此。 31 | 32 | ```getMissingParentStages```与[DAGScheduler划分stage](http://blog.csdn.net/bigbigdata/article/details/47293263)中介绍的```getParentStages```有点像,但不同的是不再需要划分stage,并对每个stage的状态做了判断,源码及注释如下: 33 | 34 | ``` 35 | //< 以参数stage为起点,向前遍历所有stage,判断stage是否为未提交,若使则加入missing中 36 |   private def getMissingParentStages(stage: Stage): List[Stage] = { 37 |     //< 未提交的stage 38 |     val missing = new HashSet[Stage] 39 |     //< 存储已经被访问到得RDD 40 |     val visited = new HashSet[RDD[_]] 41 | 42 |     val waitingForVisit = new Stack[RDD[_]] 43 |     def visit(rdd: RDD[_]) { 44 |       if (!visited(rdd)) { 45 |         visited += rdd 46 |         if (getCacheLocs(rdd).contains(Nil)) { 47 |           for (dep <- rdd.dependencies) { 48 |             dep match { 49 |               //< 若为宽依赖,生成新的stage 50 |               case shufDep: ShuffleDependency[_, _, _] => 51 |                 //< 这里调用getShuffleMapStage不像在getParentStages时需要划分stage,而是直接根据shufDep.shuffleId获取对应的ShuffleMapStage 52 |                 val mapStage = getShuffleMapStage(shufDep, stage.jobId) 53 |                 if (!mapStage.isAvailable) { 54 |                   // 若stage得状态为available则为未提交stage 55 |                   missing += mapStage 56 |                 } 57 |               //< 若为窄依赖,那就属于同一个stage。并将依赖的RDD放入waitingForVisit中,以能够在下面的while中继续向上visit,直至遍历了整个DAG图 58 |               case narrowDep: NarrowDependency[_] => 59 |                 waitingForVisit.push(narrowDep.rdd) 60 |             } 61 |           } 62 |         } 63 |       } 64 |     } 65 |     waitingForVisit.push(stage.rdd) 66 |     while (waitingForVisit.nonEmpty) { 67 |       visit(waitingForVisit.pop()) 68 |     } 69 |     missing.toList 70 |   } 71 | ``` 72 | 73 | 上面提到,若stageX存在未提交的父stages,则先提交父stages;那么,如果stageX没有未提交的父stage呢(比如,包含从HDFS读取数据生成HadoopRDD的那个stage是没有父stage的)? 74 | 75 | 这时会调用```submitMissingTasks(stage, jobId.get)```,参数就是stageX及其对应的jobId.get。这个函数便是我们时常在其他文章或书籍中看到的将stage与taskSet对应起来,然后DAGScheduler将taskSet提交给TaskScheduler去执行的实施者。这个函数的实现比较长,下面分段说明。 76 | 77 | ###Step1: 得到RDD中需要计算的partition 78 | 对于Shuffle类型的stage,需要判断stage中是否缓存了该结果;对于Result类型的Final Stage,则判断计算Job中该partition是否已经计算完成。这么做(没有直接提交全部tasks)的原因是,stage中某个task执行失败其他执行成功的时候就需要找出这个失败的task对应要计算的partition而不是要计算所有partition 79 | 80 | ``` 81 |   private def submitMissingTasks(stage: Stage, jobId: Int) { 82 |     stage.pendingTasks.clear() 83 | 84 |     //< 首先得到RDD中需要计算的partition 85 |     //< 对于Shuffle类型的stage,需要判断stage中是否缓存了该结果; 86 |     //< 对于Result类型的Final Stage,则判断计算Job中该partition是否已经计算完成 87 |     //< 这么做的原因是,stage中某个task执行失败其他执行成功地时候就需要找出这个失败的task对应要计算的partition而不是要计算所有partition 88 |     val partitionsToCompute: Seq[Int] = { 89 |       stage match { 90 |         case stage: ShuffleMapStage => 91 |           (0 until stage.numPartitions).filter(id => stage.outputLocs(id).isEmpty) 92 |         case stage: ResultStage => 93 |           val job = stage.resultOfJob.get 94 |           (0 until job.numPartitions).filter(id => !job.finished(id)) 95 |       } 96 |     } 97 | ``` 98 | 99 | ###Step2: 序列化task的binary 100 | Executor可以通过广播变量得到它。每个task运行的时候首先会反序列化 101 | 102 | ``` 103 | var taskBinary: Broadcast[Array[Byte]] = null 104 |     try { 105 |       // For ShuffleMapTask, serialize and broadcast (rdd, shuffleDep). 106 |       // For ResultTask, serialize and broadcast (rdd, func). 107 |       val taskBinaryBytes: Array[Byte] = stage match { 108 |         case stage: ShuffleMapStage => 109 |           //< 对于ShuffleMapTask,将rdd及其依赖关系序列化;在Executor执行task之前会反序列化 110 |           closureSerializer.serialize((stage.rdd, stage.shuffleDep): AnyRef).array() 111 |           //< 对于ResultTask,对rdd及要在每个partition上执行的func 112 |         case stage: ResultStage => 113 |           closureSerializer.serialize((stage.rdd, stage.resultOfJob.get.func): AnyRef).array() 114 |       } 115 | 116 |       //< 将序列化好的信息广播给所有的executor 117 |       taskBinary = sc.broadcast(taskBinaryBytes) 118 |     } catch { 119 |       // In the case of a failure during serialization, abort the stage. 120 |       case e: NotSerializableException => 121 |         abortStage(stage, "Task not serializable: " + e.toString) 122 |         runningStages -= stage 123 | 124 |         // Abort execution 125 |         return 126 |       case NonFatal(e) => 127 |         abortStage(stage, s"Task serialization failed: $e\n${e.getStackTraceString}") 128 |         runningStages -= stage 129 |         return 130 |     } 131 | ``` 132 | 133 | ###Step3: 为每个需要计算的partiton生成一个task 134 | ShuffleMapStage对应的task全是ShuffleMapTask; ResultStage对应的全是ResultTask。task继承Serializable,要确保task是可序列化的。 135 | 136 | ``` 137 | val tasks: Seq[Task[_]] = stage match { 138 |       case stage: ShuffleMapStage => 139 |         partitionsToCompute.map { id => 140 |           val locs = getPreferredLocs(stage.rdd, id) 141 |           //< RDD对应的partition 142 |           val part = stage.rdd.partitions(id) 143 |           new ShuffleMapTask(stage.id, taskBinary, part, locs) 144 |         } 145 | 146 |       case stage: ResultStage => 147 |         val job = stage.resultOfJob.get 148 |         //< id为输出分区索引,表示reducerID 149 |         partitionsToCompute.map { id => 150 |           val p: Int = job.partitions(id) 151 |           val part = stage.rdd.partitions(p) 152 |           val locs = getPreferredLocs(stage.rdd, p) 153 |           new ResultTask(stage.id, taskBinary, part, locs, id) 154 |         } 155 |     } 156 | ``` 157 | 158 | ###Step4: 提交tasks 159 | 先用tasks来初始化一个TaskSet对象,再调用TaskScheduler.submitTasks提交 160 | 161 | ``` 162 | stage.pendingTasks ++= tasks 163 |       logDebug("New pending tasks: " + stage.pendingTasks) 164 |       //< 提交TaskSet至TaskScheduler 165 |       taskScheduler.submitTasks( 166 |         new TaskSet(tasks.toArray, stage.id, stage.newAttemptId(), stage.jobId, properties)) 167 |       //< 记录stage提交task的时间 168 |       stage.latestInfo.submissionTime = Some(clock.getTimeMillis()) 169 |     } else { 170 | ``` 171 | 172 | 以上,介绍了提交stage和提交tasks的实现。本文若有纰漏,请批评指正。 173 | 174 | --- 175 | 176 | 欢迎关注我的微信公众号:FunnyBigData 177 | 178 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 179 | -------------------------------------------------------------------------------- /spark-core/[Spark源码剖析]-JobWaiter.md: -------------------------------------------------------------------------------- 1 | ###职责 2 |  * 等待DAGScheduler job完成,一个JobWaiter对象与一个job唯一一一对应 3 |  * 一旦task完成,将该task结果填充到```SparkContext.runJob```创建的results数组中 4 | 5 | ###构造函数 6 | 7 | ``` 8 | private[spark] class JobWaiter[T]( 9 |     dagScheduler: DAGScheduler, 10 |     val jobId: Int, 11 |     totalTasks: Int, 12 |     resultHandler: (Int, T) => Unit) 13 |   extends JobListener {...} 14 | ``` 15 | 16 | 在SparkContext.runJob中,通过 17 | 18 | ``` 19 | val results = new Array[U](partitions.size) 20 | runJob[T, U](rdd, func, partitions, allowLocal, (index, res) => results(index) = res) 21 | ``` 22 | 来创建容纳job结果的数据,数组的每个元素对应与之下标相等的partition的计算结果;并将结果处理函数```(index, res) => results(index) = res```作为参数传入runJob,以使在runJob内部的创建的JobWaiter对象能够在得知taskSucceeded之后,将该task的结果填充到results中 23 | 24 | ###重要成员及方法 25 | ``` 26 | private var finishedTasks = 0 27 | ``` 28 | 已经完成的task个数 29 | 30 | --- 31 | 32 | ``` 33 | private var jobResult: JobResult = if (jobFinished) JobSucceeded else null 34 | ``` 35 | 如果job完成,jobResult为job的执行结果。对于0个task的job,直接设置job执行结果为JobSucceeded。 36 | 37 | --- 38 | 39 | ``` 40 |   def cancel() { 41 |      42 |     dagScheduler.cancelJob(jobId) 43 |   } 44 | ``` 45 | 发送一个信号来取消job。该取消操作本身会被异步执行。在TaskScheduler取消所有属于该job的tasks后,该job会以一个Spark异常结束。 46 | 47 | --- 48 | 49 | ``` 50 | override def taskSucceeded(index: Int, result: Any): Unit = synchronized { ... } 51 | ``` 52 | 53 | * 讲该task结果,即参数result,填充到SparkContext.runJob中建立的```val results = new Array[U](partitions.size)```中 54 | * ```finishedTasks += 1```,判断finishedTasks是否与totalTasks相等,若相等,则```_jobFinished = true jobResult = JobSucceeded``` 55 |        56 | ***问:***什么情况下会 taskSucceeded 方法会被调用? 57 | ***答:***DAGScheduler收到```completion @ CompletionEvent```事件后,会调用```dagScheduler.handleTaskCompletion(completion)```,该函数会最终调用```job.listener.taskSucceeded(rt.outputId, event.result)```,job.listener为trait JobListener对象,具体实现为JobWaiter 58 | 59 | ---  60 | 61 | ```def awaitResult(): JobResult = synchronized { ... }``` 62 | 等待job结束,并返回jobResult 63 | 64 | --- 65 | 66 | 欢迎关注我的微信公众号:FunnyBigData 67 | 68 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 69 | -------------------------------------------------------------------------------- /spark-core/[Spark源码剖析]Pool-Standalone模式下的队列.md: -------------------------------------------------------------------------------- 1 | #Pool-Spark Standalone模式下的队列 2 | 3 | ```org.apache.spark.scheduler.Pool```是 Spark Standalone 模式下的队列。从其重要成员及成员函数来剖析这个在 TaskScheduler 调度中起关键作用的类。 4 | 5 | ##成员 6 | 下图展示了 Pool 的所有成员及一些简要说明 7 | 8 | 9 | ![](http://upload-images.jianshu.io/upload_images/204749-fd1634c486cdc591.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 10 | 11 | 其中,```taskSetSchedulingAlgorithm```的类型由```schedulingMode```决定,下文会对```FairSchedulingAlgorithm```和```FIFOSchedulingAlgorithm```做详细分析 12 | 13 | ``` 14 | var taskSetSchedulingAlgorithm: SchedulingAlgorithm = { 15 | schedulingMode match { 16 | case SchedulingMode.FAIR => 17 | new FairSchedulingAlgorithm() 18 | case SchedulingMode.FIFO => 19 | new FIFOSchedulingAlgorithm() 20 | } 21 | } 22 | ``` 23 | 24 | ##成员函数 25 | 先来看看如何向一个 Pool 中添加 TaskSetManager 或 Pool,说明都写在注释中。 26 | 27 | ``` 28 | override def addSchedulable(schedulable: Schedulable) { 29 | // 0) { 142 | false 143 | } else { 144 | //< 若以上比较都相等,则比较 s1和 s2的名字 145 | s1.name < s2.name 146 | } 147 | } 148 | } 149 | ``` 150 | 151 | FairSchedulingAlgorithm的比较规则以在上面代码的注释中说明 152 | 153 | ##PS 154 | Pool 的成员stageId 初始值为-1,但搜遍整个 Spark 源码也没有找到哪里有对该值的重新赋值。这个 stageId 的具体含义及如何发挥作用还没有完全搞明白,若哪位朋友知道,麻烦告知,多谢 155 | 156 | --- 157 | 158 | 欢迎关注我的微信公众号:FunnyBigData 159 | 160 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 161 | -------------------------------------------------------------------------------- /spark-core/[Spark源码剖析]Spark-延迟调度策略.md: -------------------------------------------------------------------------------- 1 | 本文旨在说明 Spark 的延迟调度及其是如何工作的 2 | 3 | ##什么是延迟调度 4 | 在 Spark 中,若 task 与其输入数据在同一个 jvm 中,我们称 task 的本地性为 ```PROCESS_LOCAL```,这种本地性(locality level)是最优的,避免了网络传输及文件 IO,是最快的;其次是 task 与输入数据在同一节点上的 ```NODE_LOCAL```,数据在哪都一样的 ```NO_PREF```,数据与 task 在同一机架不同节点的 ```RACK_LOCAL``` 及最糟糕的不在同一机架的 ```ANY```。 5 | 6 | 本地性越好,对于 task 来说,花在网络传输及文件 IO 的时间越少,整个 task 执行耗时也就更少。而对于很多 task 来说,执行 task 的时间往往会比网络传输/文件 IO 的耗时要短的多。所以 Spark 希望尽量以更优的本地性启动 task。延迟调度就是为此而存在的。 7 | 8 | 在[Spark的位置优先(1): TaskSetManager 的有效 Locality Levels](http://www.jianshu.com/p/05034a9c8cae)这篇文章中,我们可以知道,假设一个 task 的最优本地性为 N,那么该 task 同时也具有其他所有本地性比 N 差的本地性。 9 | 10 | 假设调度器上一次以 locality level(本地性) M 为某个 taskSetManager 启动 task 失败,则说明该 taskSetManager 中包含本地性 M 的 tasks 的本地性 M 对应的所有节点均没有空闲资源。此时,只要当期时间与上一次以 M 为 taskSetManager 启动 task 时间差小于配置的值,调度器仍然会以 locality level M 来为 taskSetManager 启动 task 11 | 12 | ##延时调度如何工作 13 | 14 | 函数```TaskSetManager#getAllowedLocalityLevel```是实现延时调度最关键的地方,用来返回**当前该 taskSetManager 中未执行的 tasks 的最高可能 locality level**。以下为其实现 15 | 16 | ``` 17 | /** 18 | * Get the level we can launch tasks according to delay scheduling, based on current wait time. 19 | */ 20 | private def getAllowedLocalityLevel(curTime: Long): TaskLocality.TaskLocality = { 21 | // Remove the scheduled or finished tasks lazily 22 | def tasksNeedToBeScheduledFrom(pendingTaskIds: ArrayBuffer[Int]): Boolean = { 23 | var indexOffset = pendingTaskIds.size 24 | while (indexOffset > 0) { 25 | indexOffset -= 1 26 | val index = pendingTaskIds(indexOffset) 27 | if (copiesRunning(index) == 0 && !successful(index)) { 28 | return true 29 | } else { 30 | pendingTaskIds.remove(indexOffset) 31 | } 32 | } 33 | false 34 | } 35 | // Walk through the list of tasks that can be scheduled at each location and returns true 36 | // if there are any tasks that still need to be scheduled. Lazily cleans up tasks that have 37 | // already been scheduled. 38 | def moreTasksToRunIn(pendingTasks: HashMap[String, ArrayBuffer[Int]]): Boolean = { 39 | val emptyKeys = new ArrayBuffer[String] 40 | val hasTasks = pendingTasks.exists { 41 | case (id: String, tasks: ArrayBuffer[Int]) => 42 | if (tasksNeedToBeScheduledFrom(tasks)) { 43 | true 44 | } else { 45 | emptyKeys += id 46 | false 47 | } 48 | } 49 | // The key could be executorId, host or rackId 50 | emptyKeys.foreach(id => pendingTasks.remove(id)) 51 | hasTasks 52 | } 53 | 54 | while (currentLocalityIndex < myLocalityLevels.length - 1) { 55 | val moreTasks = myLocalityLevels(currentLocalityIndex) match { 56 | case TaskLocality.PROCESS_LOCAL => moreTasksToRunIn(pendingTasksForExecutor) 57 | case TaskLocality.NODE_LOCAL => moreTasksToRunIn(pendingTasksForHost) 58 | case TaskLocality.NO_PREF => pendingTasksWithNoPrefs.nonEmpty 59 | case TaskLocality.RACK_LOCAL => moreTasksToRunIn(pendingTasksForRack) 60 | } 61 | if (!moreTasks) { 62 | // This is a performance optimization: if there are no more tasks that can 63 | // be scheduled at a particular locality level, there is no point in waiting 64 | // for the locality wait timeout (SPARK-4939). 65 | lastLaunchTime = curTime 66 | logDebug(s"No tasks for locality level ${myLocalityLevels(currentLocalityIndex)}, " + 67 | s"so moving to locality level ${myLocalityLevels(currentLocalityIndex + 1)}") 68 | currentLocalityIndex += 1 69 | } else if (curTime - lastLaunchTime >= localityWaits(currentLocalityIndex)) { 70 | // Jump to the next locality level, and reset lastLaunchTime so that the next locality 71 | // wait timer doesn't immediately expire 72 | lastLaunchTime += localityWaits(currentLocalityIndex) 73 | currentLocalityIndex += 1 74 | logDebug(s"Moving to ${myLocalityLevels(currentLocalityIndex)} after waiting for " + 75 | s"${localityWaits(currentLocalityIndex)}ms") 76 | } else { 77 | return myLocalityLevels(currentLocalityIndex) 78 | } 79 | } 80 | myLocalityLevels(currentLocalityIndex) 81 | } 82 | ``` 83 | 84 | 代码有点小长,好在并不复杂,一些关键注释在以上源码中都有注明。 85 | 86 | 循环条件为```while (currentLocalityIndex < myLocalityLevels.length - 1) ```, 87 | 其中```myLocalityLevels: Array[TaskLocality.TaskLocality]```是当前 TaskSetManager 的所有 tasks 所包含的本地性(locality)集合,本地性越高的 locality level 在 myLocalityLevels 中的下标越小(具体请参见http://www.jianshu.com/p/05034a9c8cae) 88 | 89 | currentLocalityIndex 是 getAllowedLocalityLevel 前一次返回的 locality level 在 myLocalityLevels 中的索引(下标),若 getAllowedLocalityLevel 是第一次被调用,则 currentLocalityIndex 为0 90 | 91 | 整个循环体都在做这几个事情: 92 | 93 | 1. 判断 ```myLocalityLevels(currentLocalityIndex)``` 这个级别的本地性对应的待执行 tasks 集合中是否还有待执行的 task 94 | 2. 若无;则将 ```currentLocalityIndex += 1``` 进行下一次循环,即将 locality level 降低一级回到第1步 95 | 3. 若有,且当前时间与上次getAllowedLocalityLevel返回 ```myLocalityLevels(currentLocalityIndex)``` 时间间隔小于 ```myLocalityLevels(currentLocalityIndex)``` 对应的延迟时间(通过```spark.locality.wait.process或spark.locality.wait.node或spark.locality.wait.rack```配置),则 currentLocalityIndex 不变,返回myLocalityLevels(currentLocalityIndex)。这里是延迟调度的关键,只要当前时间与上一次以某个 locality level 启动 task 的时间只差小于配置的值,不管上次是否成功启动了 task,这一次仍然以上次的 locality level 来启动 task。说的更明白一些:比如上次以 localtyX 为 taskSetManager 启动 task 失败,说明taskSetManager 中 tasks 对应 localityX 的节点均没有空闲资源来启动 task,但 Spark 此时仍然会以 localityX 来为 taskSetManager 启动 task。为什么要这样做?一般来说,task 执行耗时相对于网络传输/文件IO 要小得多,调度器多等待1 2秒可能就可以以更好的本地性执行 task,避免了更耗时的网络传输或文件IO,task 整体执行时间会降低 96 | 4. 若有,且当前时间与上次getAllowedLocalityLevel返回 ```myLocalityLevels(currentLocalityIndex)``` 时间间隔大于 ```myLocalityLevels(currentLocalityIndex)``` 对应的延迟时间,则将 ```currentLocalityIndex += 1``` 进行下一次循环,即将 locality level 降低一级回到第1步 97 | 98 | --- 99 | 100 | 下面为帮助理解代码的部分说明 101 | 102 | ###判断是否还有当前 locality level 的 task 需要执行 103 | 104 | ``` 105 | val moreTasks = myLocalityLevels(currentLocalityIndex) match { 106 | case TaskLocality.PROCESS_LOCAL => moreTasksToRunIn(pendingTasksForExecutor) 107 | case TaskLocality.NODE_LOCAL => moreTasksToRunIn(pendingTasksForHost) 108 | case TaskLocality.NO_PREF => pendingTasksWithNoPrefs.nonEmpty 109 | case TaskLocality.RACK_LOCAL => moreTasksToRunIn(pendingTasksForRack) 110 | } 111 | ``` 112 | moreTasksToRunIn就不进行过多解释了,主要作用有两点: 113 | 114 | 1. 对于不同等级的 locality level 的 tasks 列表,将已经成功执行的或正在执行的该 locality level 的 task 从对应的列表中移除 115 | 2. 判断对应的 locality level 的 task 是否还要等待执行的,若有则返回 true,否则返回 false 116 | 117 | 以 ```myLocalityLevels(currentLocalityIndex)``` 等于 ```PROCESS_LOCAL``` 为例,这一段代码用来判断该 taskSetManager 中的 tasks 是否还有 task 的 locality levels 包含 ```PROCESS_LOCAL``` 118 | 119 | ###if (!moreTasks) 120 | 若!moreTasks,则对currentLocalityIndex加1,即 locality level 变低一级,再次循环。 121 | 122 | 根据 http://www.jianshu.com/p/05034a9c8cae 的分析我们知道,若一个 task 存在于某个 locality level 为 level1 待执行 tasks 集合中,那么该 task 也一定存在于所有 locality level 低于 level1 的待执行 tasks 集合。 123 | 124 | 从另一个角度看,对于每个 task,总是尝试以最高的 locality level 去启动,若启动失败且下次以该 locality 启动时间与上次以该 locality level 启动时间超过配置的值,则将 locality level 降低一级来尝试启动 task 125 | 126 | --- 127 | 128 | 欢迎关注我的微信公众号:FunnyBigData 129 | 130 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 131 | -------------------------------------------------------------------------------- /spark-core/[Spark源码剖析]Task的调度与执行源码剖析.md: -------------------------------------------------------------------------------- 1 | > 本文基于Spark 1.3.1,Standalone模式 2 | 3 | 一个Spark Application分为stage级别和task级别的调度,stage级别的调度已经用[DAGScheduler划分stage]和[DAGScheduler提交stage]两片文章进行源码层面的说明,本文将从源码层面剖析task是如何被调度和执行的。 4 | 5 | ##函数调用流程 6 | 先给出task调度的总体函数调用流程,并说明每个关键函数是干嘛的。这样一开始就在心里有个大概的流程图,便于之后的理解。 7 | 8 | ``` 9 | //< DAGScheduler调用该taskScheduler.submitTasks提交一个stage对应的taskSet,一个taskSet包含多个task 10 | TaskSchedulerImpl.submitTasks(taskSet: TaskSet) 11 | //< TaskScheduler(实际上是TaskSchedulerImpl)为DAGScheduler提交的每个taskSet创建一个对应的TaskSetManager对象,TaskSetManager用于调度同一个taskSet中的task 12 | val manager = TaskSchedulerImpl.createTaskSetManager(taskSet, maxTaskFailures) 13 | //< 将新创建的manager加入到调度树中,调度树由SchedulableBulider维护。有FIFO、Fair两种实现 14 | SchedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties) 15 | //< 触发调用CoarseGrainedSchedulerBackend.reviveOffers(),它将通过发送事件触发makeOffers方法调用 16 | CoarseGrainedSchedulerBackend.reviveOffers() 17 | //< 此处为发送ReviveOffers事件 18 | driverEndpoint.send(ReviveOffers) 19 | //< 此处为接收事件并处理 20 | CoarseGrainedSchedulerBackend.receive 21 | CoarseGrainedSchedulerBackend.makeOffers 22 | //< 查找各个节点空闲资源(这里是cores),并返回要在哪些节点上启动哪些tasks的对应关系,用Seq[Seq[TaskDescription]]表示 23 | TaskSchedulerImpl.resourceOffers 24 | //< 启动对应task 25 | CoarseGrainedSchedulerBackend.launchTasks 26 | executorData.executorEndpoint.send(LaunchTask(new SerializableBuffer(serializedTask))) 27 | ``` 28 | 29 | 看了上述流程可能不那么明白,没关系,不明白才要往下看。 30 | 31 | ##TaskSchedulerImpl.submitTasks(...) 32 | 33 | 在Spark 1.3.1版本中,TaskSchedulerImpl是TaskScheduler的唯一实现。submitTasks函数主要作用如下源码及注释所示: 34 | 35 | 1. 为taskSet创建对应的TaskSetManager对象。TaskManager的主要功能在于对Task的细粒度调度,比如 36 | * 决定在某个executor上是否启动及启动哪个task 37 | * 为了达到Locality aware,将Task的调度做相应的延迟 38 | * 当一个Task失败的时候,在约定的失败次数之内时,将Task重新提交 39 | * 处理拖后腿的task 40 | 2. 调用SchedulerBackend.makeOffers进入下一步 41 | 42 | ``` 43 | override def submitTasks(taskSet: TaskSet) { 44 | val tasks = taskSet.tasks 45 | logInfo("Adding task set " + taskSet.id + " with " + tasks.length + " tasks") 46 | this.synchronized { 47 | //< 为stage对应的taskSet创建TaskSetManager对象 48 | val manager = createTaskSetManager(taskSet, maxTaskFailures) 49 | //< 建立taskset与TaskSetManager的对应关系 50 | activeTaskSets(taskSet.id) = manager 51 | 52 | //< TaskSetManager会被放入调度池(Pool)当中。 53 | schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties) 54 | 55 | //< 设置定时器,若task还没启动,则一直输出未分配到资源报警(输出警告日志) 56 | if (!isLocal && !hasReceivedTask) { 57 | starvationTimer.scheduleAtFixedRate(new TimerTask() { 58 | override def run() { 59 | if (!hasLaunchedTask) { 60 | logWarning("Initial job has not accepted any resources; " + 61 | "check your cluster UI to ensure that workers are registered " + 62 | "and have sufficient resources") 63 | } else { 64 | this.cancel() 65 | } 66 | } 67 | }, STARVATION_TIMEOUT_MS, STARVATION_TIMEOUT_MS) 68 | } 69 | hasReceivedTask = true 70 | } 71 | 72 | //< 将处触发调用SchedulerBackend.makeOffers来为tasks分配资源,调度任务 73 | backend.reviveOffers() 74 | } 75 | ``` 76 | 77 | ## 基于事件模型的调用 78 | 79 | 下面源码及注释展示了CoarseGrainedSchedulerBackend是如何通过事件模型来进一步调用的。其中ReviveOffers事件有两种触发模式: 80 | 81 | 1. 周期性触发的,默认1秒一次 82 | 2. reviveOffers被TaskSchedulerImpl.reviveOffers()调用 83 | 84 | ``` 85 | override def reviveOffers() { 86 | driverEndpoint.send(ReviveOffers) 87 | } 88 | 89 | override def receive: PartialFunction[Any, Unit] = { 90 | //< 此处省略n行代码 91 | 92 | case ReviveOffers => 93 | makeOffers() 94 | 95 | //< 此处省略n行代码 96 | } 97 | ``` 98 | 99 | ## CoarseGrainedSchedulerBackend.makeOffers() 100 | 该函数非常重要,它将集群的资源以Offer的方式发给上层的TaskSchedulerImpl。TaskSchedulerImpl调用scheduler.resourceOffers获得要被执行的Seq[TaskDescription],然后将得到的Seq[TaskDescription]交给CoarseGrainedSchedulerBackend分发到各个executor上执行 101 | 102 | ``` 103 | def makeOffers() { 104 | launchTasks(scheduler.resourceOffers(executorDataMap.map { case (id, executorData) => 105 | new WorkerOffer(id, executorData.executorHost, executorData.freeCores) 106 | }.toSeq)) 107 | } 108 | ``` 109 | 110 | 为便于理解makeOffers调用及间接调用的各个流程,将该函数实现分为三个step来分析,这需要对源码的表现形式做一点点改动,但并不会有任何影响。 111 | 112 | ###Step1: val seq = executorDataMap.map { case (id, executorData) => new WorkerOffer(id, executorData.executorHost, executorData.freeCores) }.toSeq 113 | 114 | executorDataMap是HashMap[String, ExecutorData]类型,在该HashMap中key为executor id,value为ExecutorData类型(包含executor的host,RPC信息,TotalCores,FreeCores信息) 115 | 116 | ``` 117 | //< 代表一个executor上的可用资源(这里仅可用cores) 118 | private[spark] 119 | case class WorkerOffer(executorId: String, host: String, cores: Int) 120 | ``` 121 | 122 | 123 | 124 | 这段代码,返回HashMap[executorId, WorkerOffer]。每个WorkerOffer包含executor的id,host及其上可用cores信息。 125 | 126 | ###Step2: val taskDescs = scheduler.resourceOffers( seq ) 127 | 拿到集群里的executor及其对应WorkerOffer后,就要开始第二个步骤,即找出要在哪些Worker上启动哪些task。这个过程比较长,也比较复杂。让我来一层层拨开迷雾。 128 | 129 | 我把```val taskDescs = scheduler.resourceOffers( seq )```即```TaskSchedulerImpl.resourceOffers(offers: Seq[WorkerOffer])```,返回的是Seq[Seq[TaskDescription]] 类型,来看看其实现: 130 | 131 | ``` 132 | def resourceOffers(offers: Seq[WorkerOffer]): Seq[Seq[TaskDescription]] = synchronized { 133 | //< 标记每个slave为alive并记录它们的hostname 134 | var newExecAvail = false 135 | //< 此处省略更新executor,host,rack信息代码;这里会根据是否有新的executor更新newExecAvail的值 136 | 137 | //< 为了负载均衡,打乱offers顺序,Random.shuffle用于将一个集合中的元素打乱 138 | val shuffledOffers = Random.shuffle(offers) 139 | //< 事先创建好用于存放要在各个worker上launch的 List[workerId, ArrayBuffer[TaskDescription]]。 140 | //< 由于task要使用的cores并不一定为1,所以每个worker上要launch得task并不一定等于可用的cores数 141 | val tasks = shuffledOffers.map(o => new ArrayBuffer[TaskDescription](o.cores)) 142 | //< 每个executor上可用的cores 143 | val availableCpus = shuffledOffers.map(o => o.cores).toArray 144 | 145 | //< 返回排序过的TaskSet队列,有FIFO及Fair两种排序规则,默认为FIFO,可通过配置修改 146 | val sortedTaskSets = rootPool.getSortedTaskSetQueue 147 | for (taskSet <- sortedTaskSets) { 148 | //< 如果有新的executor added,更新TaskSetManager可用的executor 149 | if (newExecAvail) { 150 | taskSet.executorAdded() 151 | } 152 | } 153 | 154 | var launchedTask = false 155 | //< 依次取出排序过的taskSet列表中的taskSet; 156 | //< 对于每个taskSet,取出其tasks覆盖的所有locality,从高到低依次遍历每个等级的locality; 157 | //< 取出了taskSet及本次要处理的locality后,根据该taskSet及locality遍历所有可用的worker,找出可以在各个worker上启动的task,加到tasks:Seq[Seq[TaskDescription]]中 158 | for (taskSet <- sortedTaskSets; maxLocality <- taskSet.myLocalityLevels) { 159 | do { 160 | //< 获取tasks,tasks代表要在哪些worker上启动哪些tasks 161 | launchedTask = resourceOfferSingleTaskSet( 162 | taskSet, maxLocality, shuffledOffers, availableCpus, tasks) 163 | } while (launchedTask) 164 | } 165 | 166 | if (tasks.size > 0) { 167 | hasLaunchedTask = true 168 | } 169 | return tasks 170 | } 171 | ``` 172 | 173 | 结合代码,概括起来说,Step2又可以分为4个SubStep: 174 | 175 | * 【SubStep1】: executor, host, rack等信息更新 176 | * 【SubStep2】: 随机打乱workers。目的是为了分配tasks能负载均衡,分配tasks时,是从打乱的workers的序列的0下标开始判断是否能在worker上启动task的 177 | * 【SubStep3】: RootPool对它包含的所有的TaskSetManagers进行排序并返回已排序的TaskSetManager数组。这里涉及到RootPool概念及如何排序,将会在下文展开说明 178 | * 【SubStep4】: 对于RootPool返回的排序后的ArrayBuffer[TaskSetManager]中的每一个TaskSetManager,取出其包含的tasks包含的所有locality。根据locality从高到低,对于每个locality,遍历所有worker,结合延迟调度机制,判断TaskSetManager的哪些tasks可以在哪些workers上启动。这里比较需要进一步说明的是“延迟调度机制”及如何判断某个TaskSetManager里的tasks是否有可以在某个worker上启动 179 | 180 | 下面,就对SubStep3及SubStep4进行展开说明 181 | 182 | ####【SubStep3】 183 | SubStep3的职责是"RootPool对它包含的所有的TaskSetManagers进行排序并返回已排序的TaskSetManager数组"。那么什么是RootPool呢?每个Spark Application包含唯一一个TaskScheduler对象,该TaskScheduler对象包含唯一一个RootPool,Spark Application包含的所有Job的所有stage对应的所有未完成的TaskSetManager都会保存在RootPool中,完成后从RootPool中remove。RootPool为```org.apache.spark.scheduler.Pool```类型,称作调度池。Pool的概念与YARN中队列的概念比较类似,一个队列可以包含子队列,相对的一个Pool可以包含子Pool;YARN队列的叶子节点即提交到该队列的Application,Pool的叶子节点即分配到该Pool的TaskSetManager。Pool根据调度模式的不同,分为FIFO及Fair。FIFO模式下只有一层Pool,不同于YARN的队列可以n多层,Pool的Fair调度模式下,只能有三层:RootPool,RootPool的子Pools,子Pools的叶子节点(即TaskSetManager)。 184 | 185 | 不同的调度模式添加叶子节点的实现是一样的,如下: 186 | 187 | ``` 188 | override def addSchedulable(schedulable: Schedulable) { 189 | require(schedulable != null) 190 | //< 当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素 191 | schedulableQueue.add(schedulable) 192 | schedulableNameToSchedulable.put(schedulable.name, schedulable) 193 | schedulable.parent = this 194 | } 195 | ``` 196 | 197 | Schedulable类型的参数schedulable包含成员```val parent: Pool```,即父Pool,所以在添加TaskSetManager到Pool的时候就指定了父Pool。对于FIFO,所有的TaskSetManager的父Pool都是RootPool;对于Fair,TaskSetManager的父Pool即RootPool的某个子Pool。 198 | 199 | 不同的模式,除了Pool的层级结构不同,对它包含的TaskSetManagers进行排序时使用的算法也不同。FIFO对应FIFOSchedulingAlgorithm类,Fair对应FairSchedulingAlgorithm()类 200 | 201 | ``` 202 | var taskSetSchedulingAlgorithm: SchedulingAlgorithm = { 203 | schedulingMode match { 204 | case SchedulingMode.FAIR => 205 | new FairSchedulingAlgorithm() 206 | case SchedulingMode.FIFO => 207 | new FIFOSchedulingAlgorithm() 208 | } 209 | } 210 | ``` 211 | 212 | 当Pool.getSortedTaskSetQueue被调用时,就会用到该排序类,如下: 213 | 214 | ``` 215 | //< 利用排序算法taskSetSchedulingAlgorithm先对以本pool作为父pool的子pools做排序,再对排序后的pool中的每个TaskSetManager排序; 216 | //< 得到最终排好序的 ArrayBuffer[TaskSetManager] 217 | override def getSortedTaskSetQueue: ArrayBuffer[TaskSetManager] = { 218 | var sortedTaskSetQueue = new ArrayBuffer[TaskSetManager] 219 | val sortedSchedulableQueue = 220 | schedulableQueue.toSeq.sortWith(taskSetSchedulingAlgorithm.comparator) 221 | //< FIFO不会调到这里,直接走到下面的return 222 | for (schedulable <- sortedSchedulableQueue) { 223 | sortedTaskSetQueue ++= schedulable.getSortedTaskSetQueue 224 | } 225 | sortedTaskSetQueue 226 | } 227 | ``` 228 | 229 | FIFO排序类中的比较函数的实现很简单: 230 | 1. Schedulable A和Schedulable B的优先级,优先级值越小,优先级越高 231 | 2. A优先级与B优先级相同,若A对应stage id越小,优先级越高 232 | 233 | ``` 234 | private[spark] class FIFOSchedulingAlgorithm extends SchedulingAlgorithm { 235 | override def comparator(s1: Schedulable, s2: Schedulable): Boolean = { 236 | val priority1 = s1.priority 237 | val priority2 = s2.priority 238 | var res = math.signum(priority1 - priority2) 239 | if (res == 0) { 240 | val stageId1 = s1.stageId 241 | val stageId2 = s2.stageId 242 | res = math.signum(stageId1 - stageId2) 243 | } 244 | if (res < 0) { 245 | true 246 | } else { 247 | false 248 | } 249 | } 250 | } 251 | ``` 252 | 253 | Pool及TaskSetManager都继承于Schedulable,来看下它的定义: 254 | 255 | ``` 256 | private[spark] trait Schedulable { 257 | var parent: Pool 258 | // child queues 259 | def schedulableQueue: ConcurrentLinkedQueue[Schedulable] 260 | def schedulingMode: SchedulingMode 261 | def weight: Int 262 | def minShare: Int 263 | def runningTasks: Int 264 | def priority: Int 265 | def stageId: Int 266 | def name: String 267 | 268 | //< 省略若干代码 269 | } 270 | ``` 271 | 272 | 可以看到,Schedulable包含weight(权重)、priority(优先级)、minShare(最小共享量)等属性。其中: 273 | * weight:权重,默认是1,设置为2的话,就会比其他调度池获得2x多的资源,如果设置为-1000,该调度池一有任务就会马上运行 274 | * minShare:最小共享核心数,默认是0,在权重相同的情况下,minShare大的,可以获得更多的资源 275 | 276 | 对于Fair调度模式下的比较,实现如下: 277 | 278 | ``` 279 | 280 | private[spark] class FairSchedulingAlgorithm extends SchedulingAlgorithm { 281 | override def comparator(s1: Schedulable, s2: Schedulable): Boolean = { 282 | val minShare1 = s1.minShare 283 | val minShare2 = s2.minShare 284 | val runningTasks1 = s1.runningTasks 285 | val runningTasks2 = s2.runningTasks 286 | val s1Needy = runningTasks1 < minShare1 287 | val s2Needy = runningTasks2 < minShare2 288 | val minShareRatio1 = runningTasks1.toDouble / math.max(minShare1, 1.0).toDouble 289 | val minShareRatio2 = runningTasks2.toDouble / math.max(minShare2, 1.0).toDouble 290 | val taskToWeightRatio1 = runningTasks1.toDouble / s1.weight.toDouble 291 | val taskToWeightRatio2 = runningTasks2.toDouble / s2.weight.toDouble 292 | var compare:Int = 0 293 | 294 | if (s1Needy && !s2Needy) { 295 | return true 296 | } else if (!s1Needy && s2Needy) { 297 | return false 298 | } else if (s1Needy && s2Needy) { 299 | compare = minShareRatio1.compareTo(minShareRatio2) 300 | } else { 301 | compare = taskToWeightRatio1.compareTo(taskToWeightRatio2) 302 | } 303 | 304 | if (compare < 0) { 305 | true 306 | } else if (compare > 0) { 307 | false 308 | } else { 309 | s1.name < s2.name 310 | } 311 | } 312 | } 313 | ``` 314 | 315 | 结合以上代码,我们可以比较容易看出Fair调度模式的比较逻辑: 316 | 1. 正在运行的task个数小于最小共享核心数的要比不小于的优先级高 317 | 2. 若两者正在运行的task个数都小于最小共享核心数,则比较minShare使用率的值,即```runningTasks.toDouble / math.max(minShare, 1.0).toDouble```,越小则优先级越高 318 | 3. 若minShare使用率相同,则比较权重使用率,即```runningTasks.toDouble / s.weight.toDouble```,越小则优先级越高 319 | 4. 如果权重使用率还相同,则比较两者的名字 320 | 321 | 对于Fair调度模式,需要先对RootPool的各个子Pool进行排序,再对子Pool中的TaskSetManagers进行排序,使用的算法都是```FairSchedulingAlgorithm.FairSchedulingAlgorithm```。 322 | 323 | 到这里,应该说清楚了整个SubStep3的流程。 324 | 325 | ####SubStep4 326 | SubStep4说白了就是已经知道了哪些worker上由多少可用cores了,然后要决定要在哪些worker上启动哪些tasks: 327 | 328 | ``` 329 | //< 事先创建好用于存放要在各个worker上launch的 List[workerId, ArrayBuffer[TaskDescription]]。 330 | //< 由于task要使用的cores并不一定为1,所以每个worker上要launch得task并不一定等于可用的cores数 331 | val tasks = shuffledOffers.map(o => new ArrayBuffer[TaskDescription](o.cores)) 332 | 333 | var launchedTask = false 334 | for (taskSet <- sortedTaskSets; maxLocality <- taskSet.myLocalityLevels) { 335 | do { 336 | //< 获取tasks,tasks代表要在哪些worker上启动哪些tasks 337 | launchedTask = resourceOfferSingleTaskSet( 338 | taskSet, maxLocality, shuffledOffers, availableCpus, tasks) 339 | } while (launchedTask) 340 | } 341 | ``` 342 | 343 | 从for循环可以看到,该过程对排好序的taskSet数组的每一个元素,从locality优先级从高到低(taskSet.myLocalityLevels返回该taskSet包含的所有task包含的locality,按locality从高到低排列,PROCESS_LOCAL最高)取出locality,以取出的taskSet和locality调用```TaskSchedulerImpl.resourceOfferSingleTaskSet```,来看下它的实现(为方便阅读及理解,删去一些代码): 344 | 345 | ``` 346 | private def resourceOfferSingleTaskSet( 347 | taskSet: TaskSetManager, 348 | maxLocality: TaskLocality, 349 | shuffledOffers: Seq[WorkerOffer], 350 | availableCpus: Array[Int], 351 | tasks: Seq[ArrayBuffer[TaskDescription]]) : Boolean = { 352 | var launchedTask = false 353 | 354 | //< 获取每个worker上要执行的tasks序列 355 | for (i <- 0 until shuffledOffers.size) { 356 | val execId = shuffledOffers(i).executorId 357 | val host = shuffledOffers(i).host 358 | if (availableCpus(i) >= CPUS_PER_TASK) { 359 | try { 360 | for (task <- taskSet.resourceOffer(execId, host, maxLocality)) { 361 | //< 将获得要在index为i的worker上执行的task,添加到tasks(i)中;这样就知道了要在哪个worker上执行哪些tasks了 362 | tasks(i) += task 363 | 364 | availableCpus(i) -= CPUS_PER_TASK 365 | assert(availableCpus(i) >= 0) 366 | launchedTask = true 367 | } 368 | } catch { 369 | case e: TaskNotSerializableException => 370 | return launchedTask 371 | } 372 | } 373 | } 374 | return launchedTask 375 | } 376 | ``` 377 | resourceOfferSingleTaskSet拿到worker可用cores,taskSet和locality后 378 | 1. 遍历每个worker的可用cores,如果可用cores大于task需要的cores数(即CPUS_PER_TASK),进入2 379 | 2. 调用```taskSet.resourceOffer(execId, host, maxLocality)```获取可在指定executor上启动的task,若返回非空,把返回的task加到最终的```tasks: Seq[ArrayBuffer[TaskDescription]]```中,该结构保存要在哪些worker上启动哪些tasks 380 | 3. 减少2中分配了task的worker的可用cores及更新其他信息 381 | 382 | 从以上的分析中可以看出,要在某个executor上启动哪个task最终的实现在```TaskSetManager.resourceOffer```中,由于该函数比较长,我将函数分过几个过程来分析 383 | 384 | 首先来看第一段: 385 | 386 | ``` 387 | //< 如果资源是有locality特征的 388 | if (maxLocality != TaskLocality.NO_PREF) { 389 | //< 获取当前taskSet允许执行的locality。getAllowedLocalityLevel随时间变化而变化 390 | allowedLocality = getAllowedLocalityLevel(curTime) 391 | //< 如果允许的locality级别低于maxLocality,则使用maxLocality覆盖允许的locality 392 | if (allowedLocality > maxLocality) { 393 | // We're not allowed to search for farther-away tasks 394 | //< 临时将允许的locality级别降低到资源允许的最高locality级别 395 | allowedLocality = maxLocality 396 | } 397 | } 398 | ``` 399 | 400 | 要判断task能否在worker上启动,除了空闲资源是否达到task要求外,还需要判断本地性,即locality。locality从高到低共分为PROCESS_LOCAL, NODE_LOCAL,RACK_LOCAL及ANY。若taskSet带有locality属性,则通过getAllowedLocalityLevel函数获得该taskSet能容忍的最低界别locality。 401 | 402 | getAllowedLocalityLevel中: 403 | 1. 如果taskset刚刚被提交,taskScheduler开始第一轮对taskset中的task开始提交,那么当时currentLocalityIndex为0,直接返回可用的最好的本地性;如果是在以后的提交过程中,那么如果当前的等待时间超过了一个级别,就向后跳一个级别 404 | 2. getAllowedLocalityLevel方法返回的是当前这次调度中,能够容忍的最差的本地性级别,在后续步骤的搜索中就只搜索本地性比这个级别好的情况 405 | 3. 随着时间的推移,撇开maxLocality配置不谈,对于本地性的容忍程度越来越大。 406 | 407 | 继续返回```TaskSetManager.resourceOffer```中,获得taskSet能容忍的最差locality后,与maxLocality比较去较差的locality作为最终的 408 | 能容忍的最差locality。 409 | 410 | 进入第二段: 411 | 412 | ``` 413 | dequeueTask(execId, host, allowedLocality) match { 414 | case Some((index, taskLocality, speculative)) => { 415 | //< 进行各种信息更新操作 416 | 417 | addRunningTask(taskId) 418 | 419 | // We used to log the time it takes to serialize the task, but task size is already 420 | // a good proxy to task serialization time. 421 | // val timeTaken = clock.getTime() - startTime 422 | val taskName = s"task ${info.id} in stage ${taskSet.id}" 423 | sched.dagScheduler.taskStarted(task, info) 424 | return Some(new TaskDescription(taskId = taskId, attemptNumber = attemptNum, execId, 425 | taskName, index, serializedTask)) 426 | } 427 | case _ => 428 | } 429 | ``` 430 | 431 | 可以看到,第二段首先调用了函数```dequeueTask```,如果返回不为空,说明为指定的worker分配了task;这之后,进行各种信息更新,将taskId加入到runningTask中,并通知DAGScheduler,最后返回taskDescription。来看看```dequeueTask```的实现: 432 | 433 | ``` 434 | private def dequeueTask(execId: String, host: String, maxLocality: TaskLocality.Value) 435 | : Option[(Int, TaskLocality.Value, Boolean)] = 436 | { 437 | //< dequeueTaskFromList: 该方法获取list中一个可以launch的task,同时清除扫描过的已经执行的task。其实它从第二次开始首先扫描的一定是已经运行完成的task,因此是延迟清除 438 | // 同一个Executor,通过execId来查找相应的等待的task 439 | for (index <- dequeueTaskFromList(execId, getPendingTasksForExecutor(execId))) { 440 | return Some((index, TaskLocality.PROCESS_LOCAL, false)) 441 | } 442 | 443 | // 通过主机名找到相应的Task,不过比之前的多了一步判断 444 | if (TaskLocality.isAllowed(maxLocality, TaskLocality.NODE_LOCAL)) { 445 | for (index <- dequeueTaskFromList(execId, getPendingTasksForHost(host))) { 446 | return Some((index, TaskLocality.NODE_LOCAL, false)) 447 | } 448 | } 449 | 450 | 451 | if (TaskLocality.isAllowed(maxLocality, TaskLocality.NO_PREF)) { 452 | // Look for noPref tasks after NODE_LOCAL for minimize cross-rack traffic 453 | for (index <- dequeueTaskFromList(execId, pendingTasksWithNoPrefs)) { 454 | return Some((index, TaskLocality.PROCESS_LOCAL, false)) 455 | } 456 | } 457 | 458 | // 通过Rack的名称查找Task 459 | if (TaskLocality.isAllowed(maxLocality, TaskLocality.RACK_LOCAL)) { 460 | for { 461 | rack <- sched.getRackForHost(host) 462 | index <- dequeueTaskFromList(execId, getPendingTasksForRack(rack)) 463 | } { 464 | return Some((index, TaskLocality.RACK_LOCAL, false)) 465 | } 466 | } 467 | 468 | // 查找那些preferredLocations为空的,不指定在哪里执行的Task来执行 469 | if (TaskLocality.isAllowed(maxLocality, TaskLocality.ANY)) { 470 | for (index <- dequeueTaskFromList(execId, allPendingTasks)) { 471 | return Some((index, TaskLocality.ANY, false)) 472 | } 473 | } 474 | 475 | // find a speculative task if all others tasks have been scheduled 476 | // 最后没办法了,拖的时间太长了,只能启动推测执行了 477 | dequeueSpeculativeTask(execId, host, maxLocality).map { 478 | case (taskIndex, allowedLocality) => (taskIndex, allowedLocality, true)} 479 | } 480 | ``` 481 | 482 | 从该实现可以看出,不管之前获得的能容忍的最差locality(即allowedLocality)有多低,每次```dequeueTask```都是以PROCESS_LOCAL->...->allowedLocality顺序来判断是否可以以该locality启动task,而并不是必须以allowedLocality启动task。这也增大了启动task的机会。 483 | 484 | 到这里应该大致说清楚了Step2中的各个流程。 485 | 486 | 487 | 488 | 489 | ###Step3: launchTasks( taskDescs ) 490 | 得到要在哪些worker上启动哪些task后,将调用launchTasks来启动各个task,实现如下: 491 | 492 | ``` 493 | def launchTasks(tasks: Seq[Seq[TaskDescription]]) { 494 | for (task <- tasks.flatten) { 495 | val ser = SparkEnv.get.closureSerializer.newInstance() 496 | //< 序列化task 497 | val serializedTask = ser.serialize(task) 498 | //< 若序列化后的task的size大于等于Akka可用空间 499 | if (serializedTask.limit >= akkaFrameSize - AkkaUtils.reservedSizeBytes) { 500 | val taskSetId = scheduler.taskIdToTaskSetId(task.taskId) 501 | scheduler.activeTaskSets.get(taskSetId).foreach { taskSet => 502 | try { 503 | var msg = "Serialized task %s:%d was %d bytes, which exceeds max allowed: " + 504 | "spark.akka.frameSize (%d bytes) - reserved (%d bytes). Consider increasing " + 505 | "spark.akka.frameSize or using broadcast variables for large values." 506 | msg = msg.format(task.taskId, task.index, serializedTask.limit, akkaFrameSize, AkkaUtils.reservedSizeBytes) 507 | //< 中止taskSet,标记为已完成;同时将该taskSet的状态置为isZombie(Zombie:僵尸) 508 | taskSet.abort(msg) 509 | } catch { 510 | case e: Exception => logError("Exception in error callback", e) 511 | } 512 | } 513 | } 514 | else { 515 | //< 若序列化后的task的size小于Akka可用空间,减去对应executor上的可用cores数并向对应的executor发送启动task消息 516 | val executorData = executorDataMap(task.executorId) 517 | executorData.freeCores -= scheduler.CPUS_PER_TASK 518 | executorData.executorEndpoint.send(LaunchTask(new SerializableBuffer(serializedTask))) 519 | } 520 | } 521 | } 522 | ``` 523 | 524 | 逻辑比较简单,先对task进行序列化,若序列化后的task的size大于等于akka可用空间大小,则taskSet标记为已完成并置为Zombie状态;若序列化后的task的size小于akka可用空间大小,则通过发送消息给对应executor启动task 525 | 526 | --- 527 | 528 | 欢迎关注我的微信公众号:FunnyBigData 529 | 530 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 531 | -------------------------------------------------------------------------------- /spark-core/[图解Spark]-一张图搞懂DAGScheduler.md: -------------------------------------------------------------------------------- 1 | 以下为我调试spark1.3.1源码对整个DAGScheduler工作流程画的详细流程图,以做备忘也希望对你有所帮助 2 | 3 | 若需要高清版,点击 https://pan.baidu.com/s/1pKEbPph 4 | 5 | ![](http://upload-images.jianshu.io/upload_images/204749-bc68193cc3ef7f47.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 6 | 7 | 8 | 9 | --- 10 | 11 | 欢迎关注我的微信公众号:FunnyBigData 12 | 13 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 14 | -------------------------------------------------------------------------------- /spark-core/[源码剖析]Spark读取配置.md: -------------------------------------------------------------------------------- 1 | # Spark读取配置 2 | 3 | 我们知道,有一些配置可以在多个地方配置。以配置executor的memory为例,有以下三种方式: 4 | 1. spark-submit的```--executor-memory```选项 5 | 2. spark-defaults.conf的```spark.executor.memory```配置 6 | 3. spark-env.sh的```SPARK_EXECUTOR_MEMORY```配置 7 | 8 | 同一个配置可以在多处设置,这显然会造成迷惑,不知道spark为什么到现在还保留这样的逻辑。 9 | 如果我分别在这三处对executor的memory设置了不同的值,最终在Application中生效的是哪个? 10 | 11 | 处理这一问题的类是```SparkSubmitArguments```。在其构造函数中就完成了从 『spark-submit --选项』、『spark-defaults.conf』、『spark-env.sh』中读取配置,并根据策略决定使用哪个配置。下面分几步来分析这个重要的构造函数。 12 | 13 | ##Step0:读取spark-env.sh配置并写入环境变量中 14 | SparkSubmitArguments的参数列表包含一个```env: Map[String, String] = sys.env```参数。该参数包含一些系统环境变量的值和从spark-env.sh中读取的配置值,如图是我一个demo中env值的部分截图 15 | 16 | 17 | ![](http://upload-images.jianshu.io/upload_images/204749-73203a9b36f86c6a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 18 | 19 | 这一步之所以叫做Step0,是因为env的值在构造SparkSubmitArguments对象之前就确认,即```spark-env.sh```在构造SparkSubmitArguments对象前就读取并将配置存入env中。 20 | 21 | 22 | ##Step1:创建各配置成员并赋空值 23 | 这一步比较简单,定义了所有要从『spark-submit --选项』、『spark-defaults.conf』、『spark-env.sh』中读取的配置,并赋空值。下面的代码展示了其中一部分 : 24 | 25 | ``` 26 | var master: String = null 27 | var deployMode: String = null 28 | var executorMemory: String = null 29 | var executorCores: String = null 30 | var totalExecutorCores: String = null 31 | var propertiesFile: String = null 32 | var driverMemory: String = null 33 | var driverExtraClassPath: String = null 34 | var driverExtraLibraryPath: String = null 35 | var driverExtraJavaOptions: String = null 36 | var queue: String = null 37 | var numExecutors: String = null 38 | var files: String = null 39 | var archives: String = null 40 | var mainClass: String = null 41 | var primaryResource: String = null 42 | var name: String = null 43 | var childArgs: ArrayBuffer[String] = new ArrayBuffer[String]() 44 | var jars: String = null 45 | var packages: String = null 46 | var repositories: String = null 47 | var ivyRepoPath: String = null 48 | var packagesExclusions: String = null 49 | var verbose: Boolean = false 50 | 51 | ... 52 | ``` 53 | 54 | ##Step2:调用父类parse方法解析 spark-submit --选项 55 | 56 | ``` 57 | try { 58 | parse(args.toList) 59 | } catch { 60 | case e: IllegalArgumentException => SparkSubmit.printErrorAndExit(e.getMessage()) 61 | } 62 | ``` 63 | 64 | 这里调用父类的```SparkSubmitOptionParser#parse(List args)```。parse函数查找args中设置的--选项和值并解析为name和value,如```--master yarn-client```会被解析为值为```--master```的name和值为```yarn-client```的value。这之后调用```SparkSubmitArguments#handle(MASTER, "yarn-client")```进行处理。 65 | 66 | 来看看handle函数干了什么: 67 | 68 | ``` 69 | 70 | /** Fill in values by parsing user options. */ 71 | override protected def handle(opt: String, value: String): Boolean = { 72 | opt match { 73 | case NAME => 74 | name = value 75 | 76 | case MASTER => 77 | master = value 78 | 79 | case CLASS => 80 | mainClass = value 81 | 82 | case DEPLOY_MODE => 83 | if (value != "client" && value != "cluster") { 84 | SparkSubmit.printErrorAndExit("--deploy-mode must be either \"client\" or \"cluster\"") 85 | } 86 | deployMode = value 87 | 88 | case NUM_EXECUTORS => 89 | numExecutors = value 90 | 91 | case TOTAL_EXECUTOR_CORES => 92 | totalExecutorCores = value 93 | 94 | case EXECUTOR_CORES => 95 | executorCores = value 96 | 97 | case EXECUTOR_MEMORY => 98 | executorMemory = value 99 | 100 | case DRIVER_MEMORY => 101 | driverMemory = value 102 | 103 | case DRIVER_CORES => 104 | driverCores = value 105 | 106 | case DRIVER_CLASS_PATH => 107 | driverExtraClassPath = value 108 | 109 | ... 110 | 111 | case _ => 112 | throw new IllegalArgumentException(s"Unexpected argument '$opt'.") 113 | } 114 | true 115 | } 116 | 117 | ``` 118 | 119 | 这个函数也很简单,根据参数opt及value,设置各个成员的值。接上例,parse中调用```handle("--master", "yarn-client")```后,在handle函数中,master成员将被赋值为```yarn-client```。 120 | 121 | 注意,case MASTER中的MASTER的值在```SparkSubmitOptionParser```定义为```--master```,MASTER与其他值定义如下: 122 | 123 | ``` 124 | protected final String MASTER = "--master"; 125 | 126 | protected final String CLASS = "--class"; 127 | protected final String CONF = "--conf"; 128 | protected final String DEPLOY_MODE = "--deploy-mode"; 129 | protected final String DRIVER_CLASS_PATH = "--driver-class-path"; 130 | protected final String DRIVER_CORES = "--driver-cores"; 131 | protected final String DRIVER_JAVA_OPTIONS = "--driver-java-options"; 132 | protected final String DRIVER_LIBRARY_PATH = "--driver-library-path"; 133 | protected final String DRIVER_MEMORY = "--driver-memory"; 134 | protected final String EXECUTOR_MEMORY = "--executor-memory"; 135 | protected final String FILES = "--files"; 136 | protected final String JARS = "--jars"; 137 | protected final String KILL_SUBMISSION = "--kill"; 138 | protected final String NAME = "--name"; 139 | protected final String PACKAGES = "--packages"; 140 | protected final String PACKAGES_EXCLUDE = "--exclude-packages"; 141 | protected final String PROPERTIES_FILE = "--properties-file"; 142 | protected final String PROXY_USER = "--proxy-user"; 143 | protected final String PY_FILES = "--py-files"; 144 | protected final String REPOSITORIES = "--repositories"; 145 | protected final String STATUS = "--status"; 146 | protected final String TOTAL_EXECUTOR_CORES = "--total-executor-cores"; 147 | 148 | ... 149 | ``` 150 | 151 | 总结来说,parse函数解析了spark-submit中的--选项,并根据解析出的name和value给SparkSubmitArguments的各个成员(例如master、deployMode、executorMemory等)设置值。 152 | 153 | ##Step3:mergeDefaultSparkProperties加载spark-defaults.conf中配置 154 | 155 | Step3读取spark-defaults.conf中的配置文件并存入sparkProperties中,sparkProperties将在下一步中发挥作用 156 | 157 | ``` 158 | //< 保存从spark-defaults.conf读取的配置 159 | val sparkProperties: HashMap[String, String] = new HashMap[String, String]() 160 | 161 | //< 获取配置文件路径,若在spark-env.sh中设置SPARK_CONF_DIR,则以该值为准;否则为 $SPARK_HOME/conf/spark-defaults.conf 162 | def getDefaultPropertiesFile(env: Map[String, String] = sys.env): String = { 163 | env.get("SPARK_CONF_DIR") 164 | .orElse(env.get("SPARK_HOME").map { t => s"$t${File.separator}conf" }) 165 | .map { t => new File(s"$t${File.separator}spark-defaults.conf")} 166 | .filter(_.isFile) 167 | .map(_.getAbsolutePath) 168 | .orNull 169 | } 170 | 171 | //< 读取spark-defaults.conf配置并存入sparkProperties中 172 | private def mergeDefaultSparkProperties(): Unit = { 173 | // Use common defaults file, if not specified by user 174 | propertiesFile = Option(propertiesFile).getOrElse(Utils.getDefaultPropertiesFile(env)) 175 | // Honor --conf before the defaults file 176 | defaultSparkProperties.foreach { case (k, v) => 177 | if (!sparkProperties.contains(k)) { 178 | sparkProperties(k) = v 179 | } 180 | } 181 | } 182 | ``` 183 | 184 | ##Step4:loadEnvironmentArguments确认每个配置成员最终值 185 | 先来看看代码(由于篇幅太长,省略了一部分) 186 | 187 | ``` 188 | private def loadEnvironmentArguments(): Unit = { 189 | master = Option(master) 190 | .orElse(sparkProperties.get("spark.master")) 191 | .orElse(env.get("MASTER")) 192 | .orNull 193 | driverExtraClassPath = Option(driverExtraClassPath) 194 | .orElse(sparkProperties.get("spark.driver.extraClassPath")) 195 | .orNull 196 | driverExtraJavaOptions = Option(driverExtraJavaOptions) 197 | .orElse(sparkProperties.get("spark.driver.extraJavaOptions")) 198 | .orNull 199 | driverExtraLibraryPath = Option(driverExtraLibraryPath) 200 | .orElse(sparkProperties.get("spark.driver.extraLibraryPath")) 201 | .orNull 202 | driverMemory = Option(driverMemory) 203 | .orElse(sparkProperties.get("spark.driver.memory")) 204 | .orElse(env.get("SPARK_DRIVER_MEMORY")) 205 | .orNull 206 | 207 | ... 208 | 209 | keytab = Option(keytab).orElse(sparkProperties.get("spark.yarn.keytab")).orNull 210 | principal = Option(principal).orElse(sparkProperties.get("spark.yarn.principal")).orNull 211 | 212 | // Try to set main class from JAR if no --class argument is given 213 | if (mainClass == null && !isPython && !isR && primaryResource != null) { 214 | val uri = new URI(primaryResource) 215 | val uriScheme = uri.getScheme() 216 | 217 | uriScheme match { 218 | case "file" => 219 | try { 220 | val jar = new JarFile(uri.getPath) 221 | // Note that this might still return null if no main-class is set; we catch that later 222 | mainClass = jar.getManifest.getMainAttributes.getValue("Main-Class") 223 | } catch { 224 | case e: Exception => 225 | SparkSubmit.printErrorAndExit(s"Cannot load main class from JAR $primaryResource") 226 | } 227 | case _ => 228 | SparkSubmit.printErrorAndExit( 229 | s"Cannot load main class from JAR $primaryResource with URI $uriScheme. " + 230 | "Please specify a class through --class.") 231 | } 232 | } 233 | 234 | // Global defaults. These should be keep to minimum to avoid confusing behavior. 235 | master = Option(master).getOrElse("local[*]") 236 | 237 | // In YARN mode, app name can be set via SPARK_YARN_APP_NAME (see SPARK-5222) 238 | if (master.startsWith("yarn")) { 239 | name = Option(name).orElse(env.get("SPARK_YARN_APP_NAME")).orNull 240 | } 241 | 242 | // Set name from main class if not given 243 | name = Option(name).orElse(Option(mainClass)).orNull 244 | if (name == null && primaryResource != null) { 245 | name = Utils.stripDirectory(primaryResource) 246 | } 247 | 248 | // Action should be SUBMIT unless otherwise specified 249 | action = Option(action).getOrElse(SUBMIT) 250 | } 251 | ``` 252 | 253 | 我们单独以确定master值的那部分代码来说明,相关代码如下 254 | 255 | ``` 256 | master = Option(master) 257 | .orElse(sparkProperties.get("spark.master")) 258 | .orElse(env.get("MASTER")) 259 | .orNull 260 | 261 | // Global defaults. These should be keep to minimum to avoid confusing behavior. 262 | master = Option(master).getOrElse("local[*]") 263 | ``` 264 | 265 | 确定master的值的步骤如下: 266 | 1. **Option(master)**:若master值不为null,则以master为准;否则进入2。若master不为空,从上文的分析我们可以知道是从解析spark-submit --master选项得到的值 267 | 2. **.orElse(sparkProperties.get("spark.master"))**:若sparkProperties.get("spark.master")范围非null则以该返回值为准;否则进入3。从Step3中可以知道sparkProperties中的值都是从spark-defaults.conf中读取 268 | 3. **.orElse(env.get("MASTER"))**:若env.get("MASTER")返回非null,则以该返回值为准;否则进入4。env中的值从spark-env.sh读取而来 269 | 4. 若以上三处均为设置master,则取默认值local[*] 270 | 271 | 查看其余配置成员的值的决定过程也和master一致,稍有不同的是并不是所有配置都能在spark-defaults.conf、spark-env.sh和spark-submit选项中设置。但优先级还是一致的。 272 | 273 | 由此,我们可以得出结论,对于spark配置。若一个配置在多处设置,则优先级如下: 274 | ***spark-submit --选项 > spark-defaults.conf配置 > spark-env.sh配置 > 默认值*** 275 | 276 | 最后,附上流程图 277 | 278 | 279 | ![](http://upload-images.jianshu.io/upload_images/204749-f69827e92149a1a2.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 280 | 281 | --- 282 | 283 | 欢迎关注我的微信公众号:FunnyBigData 284 | 285 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 286 | -------------------------------------------------------------------------------- /spark-core/【源码剖析】--Spark-新旧内存管理方案(上).md: -------------------------------------------------------------------------------- 1 | Spark 作为一个以擅长内存计算为优势的计算引擎,内存管理方案是其非常重要的模块。作为使用者的我们,搞清楚 Spark 是如何管理内存的,对我们编码、调试及优化过程会有很大帮助。本文之所以取名为 "Spark 新旧内存管理方案剖析" 是因为在 Spark 1.6 中引入了新的内存管理方案,加之当前很多公司还在使用 1.6 以前的版本,所以本文会对这两种方案进行剖析。 2 | 3 | 刚刚提到自 1.6 版本引入了新的内存管理方案,但并不是说在 1.6 版本中不能使用旧的方案,而是默认使用新方案。我们可以通过设置 ```spark.memory.userLegacyMode``` 值来选择,该值为 ```false``` 表示使用新方案,```true``` 表示使用旧方案,默认为 ```false```。该值是如何发挥作用的呢?看了下面的代码就明白了: 4 | 5 | ``` 6 | val useLegacyMemoryManager = conf.getBoolean("spark.memory.useLegacyMode", false) 7 | val memoryManager: MemoryManager = 8 | if (useLegacyMemoryManager) { 9 | new StaticMemoryManager(conf, numUsableCores) 10 | } else { 11 | UnifiedMemoryManager(conf, numUsableCores) 12 | } 13 | ``` 14 | 15 | 根据 ```spark.memory.useLegacyMode``` 值的不同,会创建 MemoryManager 不同子类的实例: 16 | 17 | * 值为 ```false```:创建 ```UnifiedMemoryManager``` 类实例,该类为新的内存管理模块的实现 18 | * 值为 ```true```:创建 ```StaticMemoryManager```类实例,该类为旧的内存管理模块的实现 19 | 20 | MemoryManager 是用于管理内存的虚基类,声明了一些方法来管理用于 execution 、 storage 的内存和其他内存: 21 | 22 | * execution 内存:用于 shuffles,如joins、sorts 和 aggregations,避免频繁的 IO 而需要内存 buffer 23 | * storage 内存:用于 caching RDD,缓存 broadcast 数据及缓存 task results 24 | * 其他内存:在下文中说明 25 | 26 | 先来看看 MemoryManager 重要的成员和方法: 27 | 28 | ![](http://upload-images.jianshu.io/upload_images/204749-9496b23f293470f1.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 29 | 30 | 31 | 接下来,来看看 MemoryManager 的两种实现 32 | ##StaticMemoryManager 33 | 当 ```spark.memory.userLegacyMode``` 为 ```true``` 时,在 SparkEnv 中是这样实例化 StaticMemoryManager: 34 | 35 | ``` 36 | new StaticMemoryManager(conf, numUsableCores) 37 | ``` 38 | 39 | 调用的是 StaticMemoryManager 辅助构造函数,如下: 40 | 41 | ``` 42 | def this(conf: SparkConf, numCores: Int) { 43 | this( 44 | conf, 45 | StaticMemoryManager.getMaxExecutionMemory(conf), 46 | StaticMemoryManager.getMaxStorageMemory(conf), 47 | numCores) 48 | } 49 | ``` 50 | 51 | 继而调用主构造函数,如下: 52 | 53 | ``` 54 | private[spark] class StaticMemoryManager( 55 | conf: SparkConf, 56 | maxOnHeapExecutionMemory: Long, 57 | override val maxStorageMemory: Long, 58 | numCores: Int) 59 | extends MemoryManager( 60 | conf, 61 | numCores, 62 | maxStorageMemory, 63 | maxOnHeapExecutionMemory) 64 | ``` 65 | 66 | 这样我们就可以推导出,对于 StaticMemoryManager,其用于 storage 的内存大小等于 ```StaticMemoryManager.getMaxStorageMemory(conf)```;用于 execution 的内存大小等于 ```StaticMemoryManager.getMaxExecutionMemory(conf)```,下面进一步看看这两个方法的实现 67 | 68 | ###StaticMemoryManager.getMaxExecutionMemory(conf) 69 | 实现如下: 70 | 71 | ``` 72 | private def getMaxExecutionMemory(conf: SparkConf): Long = { 73 | val systemMaxMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory) 74 | val memoryFraction = conf.getDouble("spark.shuffle.memoryFraction", 0.2) 75 | val safetyFraction = conf.getDouble("spark.shuffle.safetyFraction", 0.8) 76 | (systemMaxMemory * memoryFraction * safetyFraction).toLong 77 | } 78 | ``` 79 | 80 | 若设置了 ```spark.testing.memory``` 则以该配置的值作为 systemMaxMemory,否则使用 JVM 最大内存作为 systemMaxMemory。```spark.testing.memory``` 仅用于测试,一般不设置,所以这里我们认为 systemMaxMemory 的值就是 executor 的最大可用内存。 81 | 82 | ```spark.shuffle.memoryFraction```:shuffle 期间用于 aggregation 和 cogroups 的内存占 executor 运行时内存的百分比,用小数表示。在任何时候,用于 shuffle 的内存总 size 不得超过这个限制,超出部分会 spill 到磁盘。如果经常 spill,考虑调大 ```spark.storage.memoryFraction``` 83 | 84 | ```spark.shuffle.safetyFraction```:为防止 OOM,不能把 systemMaxMemory * ```spark.shuffle.memoryFraction``` 全用了,需要有个安全百分比 85 | 86 | 所以最终用于 execution 的内存量为:```executor 最大可用内存``` * ```spark.shuffle.memoryFraction``` * ```spark.shuffle.safetyFraction```,默认为 ```executor 最大可用内存``` * ```0.16``` 87 | 88 | 需要特别注意的是,即使用于 execution 的内存不够用了,但同时 executor 还有其他空余内存,也不能给 execution 用 89 | 90 | ###StaticMemoryManager.getMaxStorageMemory(conf) 91 | 实现如下: 92 | 93 | ``` 94 | private def getMaxStorageMemory(conf: SparkConf): Long = { 95 | val systemMaxMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory) 96 | val memoryFraction = conf.getDouble("spark.storage.memoryFraction", 0.6) 97 | val safetyFraction = conf.getDouble("spark.storage.safetyFraction", 0.9) 98 | (systemMaxMemory * memoryFraction * safetyFraction).toLong 99 | } 100 | ``` 101 | 102 | 分析过程与 ```getMaxExecutionMemory``` 一致,我们得出这样的结论,用于storage 的内存量为: ```executor 最大可用内存``` * ```spark.storage.memoryFraction``` * ```spark.storage.safetyFraction```,默认为 ```executor 最大可用内存``` * ```0.54``` 103 | 104 | ```spark.storage.memoryFraction```:用于做 memory cache 的内存占 executor 最大可用内存的百分比,该值不应大于老生代 105 | 106 | ```spark.storage.safetyFraction```:防止 OOM 的安全比例,由 ```spark.storage.safetyFraction``` 控制,默认为0.9。在 storage 中,有一部分内存是给 unroll 使用的,unroll 即反序列化 block,该部分占比由 ```spark.storage.unrollFraction``` 控制,默认为0.2 107 | 108 | ###others 109 | 从上面的分析我们可以看到,storage 和 execution 总共使用了 80% 的内存,那剩余 20% 去哪了?这部分内存被系统保留了,用来存储运行中产生的对象 110 | 111 | 所以,各部分内存占比可由下图表示: 112 | 113 | ![](http://upload-images.jianshu.io/upload_images/204749-d203d3231c07a488.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 114 | 115 | 116 | 经过上面的描述,我们搞明白了旧的内存管理方案是如何划分内存的,也就可以根据我们实际的 app 来调整各个部分的比例。同时,我们可以明显的看到这种内存管理方式的缺陷,即 execution 和 storage 两部分内存固定死,不能共享,即使在一方内存不够用而另一方内存空闲的情况下。这样的方式经常会造成内存浪费,所以有必要引入支持共享,能更好利用内存的方案,UnifiedMemoryManager 就应运而生了 117 | 118 | --- 119 | 120 | 欢迎关注我的微信公众号:FunnyBigData 121 | 122 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 123 | -------------------------------------------------------------------------------- /spark-core/【源码剖析】--Spark-新旧内存管理方案(下).md: -------------------------------------------------------------------------------- 1 | 上一篇文章[【源码剖析】- Spark 新旧内存管理方案(上)](http://www.jianshu.com/p/2e9eda28e86c)介绍了旧的内存管理方案以及其实现类 StaticMemoryManager 是如何工作的,本文将通过介绍 UnifiedMemoryManager 来介绍新内存管理方案(以下统称为新方案)。 2 | 3 | ##内存总体分布 4 | ###系统预留 5 | 在新方案中,内存依然分为三块,分别是系统预留、用于 storage、用于 execution。其中系统预留大小如下: 6 | 7 | ``` 8 | val reservedMemory = conf.getLong("spark.testing.reservedMemory", 9 | if (conf.contains("spark.testing")) 0 else RESERVED_SYSTEM_MEMORY_BYTES) 10 | ``` 11 | 12 | 生产环境中使用一般不会设置 ```spark.testing.reservedMemory``` 和 ```spark.testing```,所以我们认为系统预留空间大小置为 ```RESERVED_SYSTEM_MEMORY_BYTES```,即 300M。 13 | 14 | ###execution 和 storage 部分总大小 15 | 上一小节这段代码是 UnifiedMemoryManager#getMaxMemory 的一个片段,该方法返回 execution 和 storage 可以共用的总空间,让我们来看看这个方法的具体实现: 16 | 17 | ``` 18 | private def getMaxMemory(conf: SparkConf): Long = { 19 | //< 生产环境中一般不会设置 spark.testing.memory,所以这里认为 systemMemory 大小为 Jvm 最大可用内存 20 | val systemMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory) 21 | //< 系统预留 300M 22 | val reservedMemory = conf.getLong("spark.testing.reservedMemory", 23 | if (conf.contains("spark.testing")) 0 else RESERVED_SYSTEM_MEMORY_BYTES) 24 | val minSystemMemory = reservedMemory * 1.5 25 | //< 如果 systemMemory 小于450M,则抛异常 26 | if (systemMemory < minSystemMemory) { 27 | throw new IllegalArgumentException(s"System memory $systemMemory must " + 28 | s"be at least $minSystemMemory. Please use a larger heap size.") 29 | } 30 | val usableMemory = systemMemory - reservedMemory 31 | val memoryFraction = conf.getDouble("spark.memory.fraction", 0.75) 32 | //< 最终 execution 和 storage 的可用内存之和为 (JVM最大可用内存 - 系统预留内存) * spark.memory.fraction 33 | (usableMemory * memoryFraction).toLong 34 | } 35 | ``` 36 | 37 | 从以上代码及注释我们可以看出,最终 execution 和 storage 的可用内存之和为 ```(JVM最大可用内存 - 系统预留内存) * spark.memory.fraction```,默认为```(JVM 最大可用内存 - 300M)* 0.75```。举个例子,如果你为 execution 设置了2G 内存,那么 execution 和 storage 可用的总内存为 (2048-300)*0.75=1311 38 | 39 | ###execution 和 storage 部分默认大小 40 | 上一小节搞清了用于 execution 和 storage 的内存之和 maxMemory,那么用于 execution 和 storage 的内存分别为多少呢?看下面三段代码: 41 | 42 | **object UnifiedMemoryManager 的 apply 方法用来构造类 UnifiedMemoryManager 的实例** 43 | 44 | ``` 45 | def apply(conf: SparkConf, numCores: Int): UnifiedMemoryManager = { 46 | val maxMemory = getMaxMemory(conf) 47 | new UnifiedMemoryManager( 48 | conf, 49 | maxMemory = maxMemory, 50 | storageRegionSize = 51 | (maxMemory * conf.getDouble("spark.memory.storageFraction", 0.5)).toLong, 52 | numCores = numCores) 53 | } 54 | ``` 55 | 56 | 这段代码确定在构造 ```UnifiedMemoryManager``` 时: 57 | 58 | * maxMemory 即 execution 和 storage 能共用的内存总和为 ```getMaxMemory(conf)```,即 ```(JVM最大可用内存 - 系统预留内存) * spark.memory.fraction``` 59 | * storageRegionSize 为 ```maxMemory * conf.getDouble("spark.memory.storageFraction", 0.5)```,在没有设置 ```spark.memory.storageFraction``` 的情况下为一半的 maxMemory 60 | 61 | 那么 storageRegionSize 是干嘛用的呢?继续看 ```UnifiedMemoryManager``` 和 ```MemoryManager``` 构造函数: 62 | 63 | ``` 64 | private[spark] class UnifiedMemoryManager private[memory] ( 65 | conf: SparkConf, 66 | val maxMemory: Long, 67 | storageRegionSize: Long, 68 | numCores: Int) 69 | extends MemoryManager( 70 | conf, 71 | numCores, 72 | storageRegionSize, 73 | maxMemory - storageRegionSize) 74 | 75 | private[spark] abstract class MemoryManager( 76 | conf: SparkConf, 77 | numCores: Int, 78 | storageMemory: Long, 79 | onHeapExecutionMemory: Long) extends Logging 80 | ``` 81 | 82 | 我们不难发现: 83 | 84 | * storageRegionSize 就是 storageMemory,大小为 ```maxMemory * conf.getDouble("spark.memory.storageFraction", 0.5)```,默认为 ```maxMemory * 0.5``` 85 | * execution 的大小为 ```maxMemory - storageRegionSize```,默认为 ```maxMemory * 0.5```,即默认情况下 storageMemory 和 execution 能用的内存相同,各占一半 86 | 87 | ##互相借用内存 88 | 新方案与旧方案最大的不同是:旧方案中 execution 和 storage 可用的内存是固定死的,即使一方内存不够用而另一方有大把空闲内存,空闲一方也无法将结存借给不足一方,这样降造成严重的内存浪费。而新方案解决了这一点,execution 和 storage 之间的内存可以互相借用,大大提供内存利用率,也更好的满足了不同资源侧重的计算的需求 89 | 90 | 下面便来介绍新方案中内存是如何互相借用的 91 | 92 | ###acquireStorageMemory 93 | 先来看看 storage 从 execution 借用内存是如何在分配 storage 内存中发挥作用的 94 | 95 | 96 | ![](http://upload-images.jianshu.io/upload_images/204749-b7f79774fd5e3ac2.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 97 | 98 | 99 | 这一过程对应的实现是 ```UnifiedMemoryManager#acquireStorageMemory```,上面的流程图应该说明了是如何 storage 内存及在 storage 内存不足时是如何向 execution 借用内存的 100 | 101 | ###acquireExecutionMemory 102 | 该方法是给 execution 给指定 task 分配内存的实现,当 execution pool 内存不足时,会从 storage pool 中借。该方法在某些情况下可能会阻塞直到有足够空闲内存。 103 | 104 | 在该方法内部定义了两个函数: 105 | 106 | * maybeGrowExecutionPool:会释放storage中保存的数据,减小storage部分内存大小,从而增大Execution部分 107 | * computeMaxExecutionPoolSize:计算在 storage 释放内存借给 execution 后,execution 部分的内存大小 108 | 109 | 在定义了这两个方法后,直接调用 ```ExecutionMemoryPool#acquireMemory``` 方法,acquireMemory方法会一直处理该 task 的请求,直到分配到足够内存或系统判断无法满足该请求为止。acquireMemory 方法内部有一个死循环,循环内部逻辑如下: 110 | 111 | 112 | ![](http://upload-images.jianshu.io/upload_images/204749-5ce0b929fe4415c6.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 113 | 114 | 115 | 从上面的流程图中,我们可以知道当 execution pool 要为某个 task 分配内存并且内存不足时,会从 storage pool 中借用内存,能借用的最大 size 为 ```storage 的空闲内存+之前 storage 从 execution 借走的内存```。这与 storage 从 execution 借用内存不同,storage 只能从 execution 借走空闲的内存,不能借走 execution 中已在使用的从 storage 借来的内存,源码中的解释是如果要这么做实现太过复杂,暂时不支持。 116 | 117 | 以上过程分析的是memoryMode 为 ON_HEAP 的情况,如果是 OFF_HEAP,则直接从 offHeapExecution 内存池中分配,本文重点不在此,故不展开分析。 118 | 119 | --- 120 | 121 | 欢迎关注我的微信公众号:FunnyBigData 122 | 123 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 124 | -------------------------------------------------------------------------------- /spark-core/举例说明Spark-RDD的分区、依赖.md: -------------------------------------------------------------------------------- 1 | 例子如下: 2 | 3 | ``` 4 | scala> val textFileRDD = sc.textFile("/Users/zhuweibin/Downloads/hive_04053f79f32b414a9cf5ab0d4a3c9daf.txt") 5 | 15/08/03 07:00:08 INFO MemoryStore: ensureFreeSpace(57160) called with curMem=0, maxMem=278019440 6 | 15/08/03 07:00:08 INFO MemoryStore: Block broadcast_0 stored as values in memory (estimated size 55.8 KB, free 265.1 MB) 7 | 15/08/03 07:00:08 INFO MemoryStore: ensureFreeSpace(17237) called with curMem=57160, maxMem=278019440 8 | 15/08/03 07:00:08 INFO MemoryStore: Block broadcast_0_piece0 stored as bytes in memory (estimated size 16.8 KB, free 265.1 MB) 9 | 15/08/03 07:00:08 INFO BlockManagerInfo: Added broadcast_0_piece0 in memory on localhost:51675 (size: 16.8 KB, free: 265.1 MB) 10 | 15/08/03 07:00:08 INFO SparkContext: Created broadcast 0 from textFile at :21 11 | textFileRDD: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[1] at textFile at :21 12 | 13 | scala>     println( textFileRDD.partitions.size ) 14 | 15/08/03 07:00:09 INFO FileInputFormat: Total input paths to process : 1 15 | 2 16 | 17 | scala>     textFileRDD.partitions.foreach { partition => 18 |      |       println("index:" + partition.index + "  hasCode:" + partition.hashCode()) 19 |      |     } 20 | index:0  hasCode:1681 21 | index:1  hasCode:1682 22 | 23 | scala>     println("dependency size:" + textFileRDD.dependencies) 24 | dependency size:List(org.apache.spark.OneToOneDependency@543669de) 25 | 26 | scala>     println( textFileRDD ) 27 | MapPartitionsRDD[1] at textFile at :21 28 | 29 | scala>     textFileRDD.dependencies.foreach { dep => 30 |      |       println("dependency type:" + dep.getClass) 31 |      |       println("dependency RDD:" + dep.rdd) 32 |      |       println("dependency partitions:" + dep.rdd.partitions) 33 |      |       println("dependency partitions size:" + dep.rdd.partitions.length) 34 |      |     } 35 | dependency type:class org.apache.spark.OneToOneDependency 36 | dependency RDD:/Users/zhuweibin/Downloads/hive_04053f79f32b414a9cf5ab0d4a3c9daf.txt HadoopRDD[0] at textFile at :21 37 | dependency partitions:[Lorg.apache.spark.Partition;@c197f46 38 | dependency partitions size:2 39 | 40 | scala>  41 | 42 | scala>     val flatMapRDD = textFileRDD.flatMap(_.split(" ")) 43 | flatMapRDD: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[2] at flatMap at :23 44 | 45 | scala>     println( flatMapRDD ) 46 | MapPartitionsRDD[2] at flatMap at :23 47 | 48 | scala>     flatMapRDD.dependencies.foreach { dep => 49 |      |       println("dependency type:" + dep.getClass) 50 |      |       println("dependency RDD:" + dep.rdd) 51 |      |       println("dependency partitions:" + dep.rdd.partitions) 52 |      |       println("dependency partitions size:" + dep.rdd.partitions.length) 53 |      |     } 54 | dependency type:class org.apache.spark.OneToOneDependency 55 | dependency RDD:MapPartitionsRDD[1] at textFile at :21 56 | dependency partitions:[Lorg.apache.spark.Partition;@c197f46 57 | dependency partitions size:2 58 | 59 | scala>  60 | 61 | scala>     val mapRDD = flatMapRDD.map(word => (word, 1)) 62 | mapRDD: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[3] at map at :25 63 | 64 | scala>     println( mapRDD ) 65 | MapPartitionsRDD[3] at map at :25 66 | 67 | scala>     mapRDD.dependencies.foreach { dep => 68 |      |       println("dependency type:" + dep.getClass) 69 |      |       println("dependency RDD:" + dep.rdd) 70 |      |       println("dependency partitions:" + dep.rdd.partitions) 71 |      |       println("dependency partitions size:" + dep.rdd.partitions.length) 72 |      |     } 73 | dependency type:class org.apache.spark.OneToOneDependency 74 | dependency RDD:MapPartitionsRDD[2] at flatMap at :23 75 | dependency partitions:[Lorg.apache.spark.Partition;@c197f46 76 | dependency partitions size:2 77 | 78 | scala>  79 | 80 | scala>  81 | 82 | scala>     val counts = mapRDD.reduceByKey(_ + _) 83 | counts: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[4] at reduceByKey at :27 84 | 85 | scala>     println( counts ) 86 | ShuffledRDD[4] at reduceByKey at :27 87 | 88 | scala>     counts.dependencies.foreach { dep => 89 |      |       println("dependency type:" + dep.getClass) 90 |      |       println("dependency RDD:" + dep.rdd) 91 |      |       println("dependency partitions:" + dep.rdd.partitions) 92 |      |       println("dependency partitions size:" + dep.rdd.partitions.length) 93 |      |     } 94 | dependency type:class org.apache.spark.ShuffleDependency 95 | dependency RDD:MapPartitionsRDD[3] at map at :25 96 | dependency partitions:[Lorg.apache.spark.Partition;@c197f46 97 | dependency partitions size:2 98 | 99 | scala> 100 | ``` 101 | 102 | 从输出我们可以看出,对于任意一个RDD x来说,其dependencies代表了其直接依赖的RDDs(一个或多个)。那dependencies又是怎么能够表明RDD之间的依赖关系呢?假设dependency为dependencies成员 103 | 104 | * dependency的类型(NarrowDependency或ShuffleDependency)说明了该依赖是窄依赖还是宽依赖 105 | * 通过dependency的```def getParents(partitionId: Int): Seq[Int]```方法,可以得到子RDD的每个分区依赖父RDD的哪些分区 106 | * dependency包含RDD成员,即子RDD依赖的父RDD,该RDD的compute函数说明了对该父RDD的分区进行怎么样的计算能得到子RDD的分区 107 | * 该父RDD中同样包含dependency成员,该dependency同样包含上述特点,同样可以通过该父RDD的dependency成员来确定该父RDD依赖的爷爷RDD。同样可以通过```dependency.getParents```方法和爷爷RDD.compute来得出如何从父RDD回朔到爷爷RDD,依次类推,可以回朔到第一个RDD 108 | 109 | 那么,如果某个RDD的partition计算失败,要回朔到哪个RDD为止呢?上例中打印出的dependency.RDD如下: 110 | 111 | ``` 112 | MapPartitionsRDD[1] at textFile at :21 113 | MapPartitionsRDD[2] at flatMap at :23 114 | MapPartitionsRDD[3] at map at :25 115 | ShuffledRDD[4] at reduceByKey at :27 116 | ``` 117 | 118 | 可以看出每个RDD都有一个编号,在回朔的过程中,每向上回朔一次变回得到一个或多个相对父RDD,这时系统会判断该RDD是否存在(即被缓存),如果存在则停止回朔,如果不存在则一直向上回朔到某个RDD存在或到最初RDD的数据源为止。 119 | 120 | --- 121 | 122 | 欢迎关注我的微信公众号:FunnyBigData 123 | 124 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 125 | -------------------------------------------------------------------------------- /spark-core/如何保证一个Spark-Application只有一个SparkContext实例.md: -------------------------------------------------------------------------------- 1 | Spark有个关于是否允许一个application存在多个SparkContext实例的配置项, 如下: 2 | 3 | **spark.driver.allowMultipleContexts: ** If true, log warnings instead of throwing exceptions when multiple SparkContexts are active. 4 | 5 | 该值默认为false, 即不允许一个application同时存在一个以上的avtive SparkContext实例. 如何保证这一点呢? 6 | 7 | 在SparkContext构造函数最开始处获取是否允许存在多个SparkContext实例的标识allowMultipleContexts, 我们这里只讨论否的情况 ( 默认也是否, 即allowMultipleContexts为false ) 8 | 9 | ``` 10 | class SparkContext(config: SparkConf) extends Logging with ExecutorAllocationClient { 11 | 12 | //< 如果为true,有多个SparkContext处于active状态时记录warning日志而不是抛出异常. 13 | private val allowMultipleContexts: Boolean = 14 | config.getBoolean("spark.driver.allowMultipleContexts", false) 15 | 16 | //< 此处省略n行代码 17 | } 18 | ``` 19 | 20 | ``` 21 | 22 | //< 注意: 这必须放在SparkContext构造器的最开始 23 | SparkContext.markPartiallyConstructed(this, allowMultipleContexts) 24 | 25 | private[spark] def markPartiallyConstructed( 26 | sc: SparkContext, 27 | allowMultipleContexts: Boolean): Unit = { 28 | SPARK_CONTEXT_CONSTRUCTOR_LOCK.synchronized { 29 | assertNoOtherContextIsRunning(sc, allowMultipleContexts) 30 | contextBeingConstructed = Some(sc) 31 | } 32 | } 33 | ``` 34 | 35 | ``` 36 | //< 伴生对象SparkContext包含一组实用的转换和参数来和各种Spark特性一起使用 37 | object SparkContext extends Logging { 38 | private val SPARK_CONTEXT_CONSTRUCTOR_LOCK = new Object() 39 | 40 | //< 此处省略n行代码 41 | } 42 | ``` 43 | 44 | 结合以上三段代码, 可以看出保证一个Spark Application只有一个SparkContext实例的步骤如下: 45 | 46 | 1. 通过SparkContext伴生对象object SparkContext中维护了一个对象 ```SPARK_CONTEXT_CONSTRUCTOR_LOCK```, 单例SparkContext在一个进程中是唯一的, 所以```SPARK_CONTEXT_CONSTRUCTOR_LOCK```在一个进程中也是唯一的 47 | 2. 函数markPartiallyConstructed中通过synchronized方法保证同一时间只有一个线程能处理 48 | 49 | ``` 50 | assertNoOtherContextIsRunning(sc, allowMultipleContexts) 51 | contextBeingConstructed = Some(sc) 52 | ``` 53 | assertNoOtherContextIsRunning会检测是否有其他SparkContext对象正在被构造或已经构造完成, 若allowMultipleContexts为true且确有正在或者已经完成构造的SparkContext对象, 则抛出异常, 否则完成SparkContext对象构造 54 | 55 | 看到这里, 有人可能会有疑问, 这虽然能保证在一个进程内只有唯一的SparkContext对象, 但Spark是分布式的, 是不是无法保证在在其他节点的进程内会构造SparkContext对象. 其实并不存在这样的问题, 因为SparkContext只会在Driver中得main函数中声明并初始化, 也就是说只会在Driver所在节点的一个进程内构造. 56 | 57 | --- 58 | 59 | 欢迎关注我的微信公众号:FunnyBigData 60 | 61 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 62 | -------------------------------------------------------------------------------- /spark-sql/Spark-Sql-源码剖析(一):sql-执行的主要流程.md: -------------------------------------------------------------------------------- 1 | > 本文基于 Spark 2.1,其他版本实现可能会有所不同 2 | 3 | 之前写过不少 Spark Core、Spark Streaming 相关的文章,但使用更广泛的 Spark Sql 倒是极少,恰好最近工作中使用到了,便开始研读相关的源码以及写相应的文章,这篇便作为 Spark Sql 系列文章的第一篇。 4 | 5 | 既然是第一篇,那么就来说说在 Spark Sql 中一条 sql 语句的主要执行流程,来看看下面这个简单的例子: 6 | 7 | ``` 8 | val spark = SparkSession 9 | .builder() 10 | .appName("Spark SQL basic example") 11 | .config("spark.some.config.option", "some-value") 12 | .getOrCreate() 13 | 14 | // For implicit conversions like converting RDDs to DataFrames 15 | import spark.implicits._ 16 | 17 | val df = spark.read.json("examples/src/main/resources/people.json") 18 | 19 | // Register the DataFrame as a SQL temporary view 20 | df.createOrReplaceTempView("people") 21 | 22 | val sqlDF = spark.sql("SELECT * FROM people") 23 | sqlDF.show() 24 | // +----+-------+ 25 | // | age| name| 26 | // +----+-------+ 27 | // |null|Michael| 28 | // | 30| Andy| 29 | // | 19| Justin| 30 | // +----+-------+ 31 | ``` 32 | 33 | 上面这段代码主要做了这么几件事: 34 | 35 | 1. 读取 json 文件得到 df 36 | 2. 基于 df 创建临时视图 people 37 | 3. 执行 sql 查询 ```SELECT * FROM people```,得到 sqlDF 38 | 4. 打印出 sqlDF 的前 20 条记录 39 | 40 | 在这里,主要关注第 3、4 步。第3步是从 sql 语句转化为 DataFrame 的过程,该过程尚未执行 action 操作,并没有执行计算任务;第4步是一个 action 操作,会触发计算任务的调度、执行。下面,我们分别来看看这两大块 41 | 42 | ## sql 语句到 sqlDataFrame 43 | 这个过程的 uml 时序图如下: 44 | 45 | 46 | ![](http://upload-images.jianshu.io/upload_images/204749-7077714cd2be7f12.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 47 | 48 | 49 | 50 | 根据该时序图,我们对该过程进一步细分: 51 | 52 | * 第1~3步:将 sql 语句解析为 unresolved logical plan,可以大致认为是解析 sql 为抽象语法树 53 | * 第4~13步:使用之前得到的 unresolved logical plan 来构造 QueryExecution 对象 qe,qe 与 Row 编码器一起来构造 DataFrame(QueryExecution 是一个关键类,之后的 logical plan 的 analyzer、optimize 以及将 logical plan 转换为 physical plan 都要通过这个类的对象 qe 来调用) 54 | 55 | 需要注意的是,到这里为止,虽然 ```SparkSession#sql``` 已经返回,并生成了 sqlDataFrame,但由于该 sqlDataFrame 并没有执行任何 action 操作,所以到这里为止,除了在 driver 端执行了上述分析的操作外,其实并没有触发或执行其他的计算任务。 56 | 57 | 这个过程最重要的产物 unresolved logical plan 被存放在 ```sqlDataFrame.queryExecution``` 中,即 ```sqlDataFrame.queryExecution.logical``` 58 | 59 | ## sqlDataFrame 的 action 60 | 前面已经得到了 unresolved logical plan 以及 sqlDataFrame,这里便要执行 action 操作来触发并执行计算任务,大致流程如下: 61 | 62 | 63 | ![](http://upload-images.jianshu.io/upload_images/204749-ea822e91bcc4f173.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 64 | 65 | 66 | 67 | 同样可以将上面这个过程进行细分(忽略第1、2步): 68 | 69 | 1. 第3~5步:从更外层慢慢往更直接的执行层的一步步调用 70 | 2. 第6步:Analyzer 借助于数据元数据(Catalog)将 unresolved logical plan 转化为 resolved logical plan 71 | 3. 第7~8步:Optimizer 将包含的各种优化规则作用于 resolved plan 进行优化 72 | 4. 第9~10步:SparkPlanner 将 optimized logical plan 转换为 physical plan 73 | 5. 第11~12步:调用 ```QueryExecution#prepareForExecution``` 方法,将 physical plan 转化为 executable physical plan,主要是插入 shuffle 操作和 internal row 的格式转换 74 | 6. 第13~14步:将 executable physical plan 转化为 RDD,并调用 RDD collect 来触发计算 75 | 76 | ## 总结 77 | 78 | 如果将 sql 到 dataFrame 及 dataFrame action 串起来,简化上文的分析,最核心的流程应该如下图所示: 79 | 80 | ![](http://upload-images.jianshu.io/upload_images/204749-8a58b60c7bbcf443.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 81 | 82 | 83 | 这篇文章是一片相对宏观的整体流程的分析,目的有二: 84 | 85 | * 一是说清楚 Spark Sql 中一条 sql 语句的执行会经过哪几个核心的流程,各个核心流程大概做了什么 86 | * 二是这里指出的各个核心流程也是接下来进一步进行分析学习的方向 87 | 88 | 更多关于各个流程的进一步实现分析请见之后的文章 89 | 90 | --- 91 | 92 | 欢迎关注我的微信公众号:FunnyBigData 93 | 94 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 95 | -------------------------------------------------------------------------------- /spark-sql/如何让你的-Spark-SQL-查询加速数十倍?.md: -------------------------------------------------------------------------------- 1 | 先来回答标题所提的问题,这里的答案是列存储,下面对列存储及在列存储加速 Spark SQL 查询速度进行介绍 2 | 3 | ## 列存储 4 | ### 什么是列存储 5 | 传统的数据库通常以行单位做数据存储,而列式存储(后文均以列存储简称)以列为单位做数据存储,如下: 6 | 7 | 8 | ![](http://upload-images.jianshu.io/upload_images/204749-eeaefd09414fbe0e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 9 | 10 | 11 | ![](http://upload-images.jianshu.io/upload_images/204749-b552a887b57fef00.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 12 | 13 | ### 优势 14 | 列存储相比于行存储主要有以下几个优势: 15 | 16 | * 数据即索引,查询是可以跳过不符合条件的数据,只读取需要的数据,降低 IO 数据量(行存储没有索引查询时造成大量 IO,建立索引和物化视图代价较大) 17 | * 只读取需要的列,进一步降低 IO 数据量,加速扫描性能(行存储会扫描所有列) 18 | * 由于同一列的数据类型是一样的,可以使用高效的压缩编码来节约存储空间 19 | 20 | 当然列存储并不是在所有场景都强于行存储,当查询要读取多个列时,行存储一次就能读取多列,而列存储需要读取多次。Spark 原始支持 parquet 和 orc 两个列存储,下文的实践使用 parquet 21 | 22 | ## 使用 Parquet 加速 Spark SQL 查询 23 | 在我的实践中,使用的 Spark 版本是 2.0.0,测试数据集包含1.18亿条数据,44G,每条数据共有17个字段,假设字段名是 f1,f2...f17。 24 | 25 | 使用 Parquet 格式的列存储主要带来三个好处 26 | 27 | #### 大大节省存储空间 28 | 使用行存储占用 44G,将行存储转成 parquet 后仅占用 5.6G,节省了 87.2% 空间,使用 Spark 将数据转成列存储耗时4分钟左右(该值与使用资源相关) 29 | 30 | ### 只读取指定行 31 | Sql: ```select count(distinct f1) from tbInRow/tbInParquet``` 32 | 33 | 行存储耗时: 119.7s 34 | 列存储耗时: 3.4s 35 | 加速 35 倍 36 | 37 | ### 跳过不符合条件数据 38 | Sql: ```select count(f1) from tbInRow/tbInParquet where f1 > 10000``` 39 | 40 | 行存储耗时: 102.8s 41 | 列存储耗时: 1.3s 42 | 加速 78 倍 43 | 44 | **当然,上文也提到了,列存储在查询需要读取多列时并不占优势:** 45 | Sql: ```select f1, f2, f3...f17 from tbInRow/tbInParquet limit 1``` 46 | 47 | 行存储耗时: 1.7s 48 | 列存储耗时: 1.9s 49 | 50 | 列存储带来的加速会因为不同的数据,不同的查询,不同的资源情况而不同,也许在你的实践中加速效果可能不如或比我这里例子的更好,这需要我们根据列存储的特性来善用之 51 | 52 | ## 参考 53 | * http://www.infoq.com/cn/articles/in-depth-analysis-of-parquet-column-storage-format 54 | * http://chattool.sinaapp.com/?p=1234 55 | 56 | --- 57 | 58 | 欢迎关注我的微信公众号:FunnyBigData 59 | 60 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 61 | -------------------------------------------------------------------------------- /spark-streaming/Spark-Streaming-+-Kakfa-编程指北.md: -------------------------------------------------------------------------------- 1 | 本文简述如何结合 Spark Streaming 和 Kakfa 来做实时计算。截止目前(2016-03-27)有两种方式: 2 | 3 | 1. 使用 kafka high-level API 和 Receivers,不需要自己管理 offsets 4 | 2. 不使用 Receivers 而直接拉取 kafka 数据,需要自行管理 offsets 5 | 6 | 两种方式在编程模型、运行特性、语义保障方面均不相同,让我们进一步说明。 7 | 如果你对 Receivers 没有概念,请先移步:[揭开Spark Streaming神秘面纱② - ReceiverTracker 与数据导入](http://www.jianshu.com/p/3195fb3c4191) 8 | 9 | ##方式一:Receiver-based 10 | 这种方法使用一个 Receiver 来接收数据。在该 Receiver 的实现中使用了 Kafka high-level consumer API。Receiver 从 kafka 接收的数据将被存储到 Spark executor 中,随后启动的 job 将处理这些数据。 11 | 12 | 在默认配置下,该方法失败后会丢失数据(保存在 executor 内存里的数据在 application 失败后就没了),若要保证数据不丢失,需要启用 WAL(即预写日志至 HDFS、S3等),这样再失败后可以从日志文件中恢复数据。WAL 相关内容请参见:http://spark.apache.org/docs/latest/streaming-programming-guide.html#deploying-applications 13 | 14 | --- 15 | 16 | 接下来讨论如何在 streaming application 中应用这种方法。使用 ```KafkaUtils.createStream```,实例代码如下: 17 | 18 | ``` 19 | def main(args: Array[String]) { 20 | if (args.length < 4) { 21 | System.err.println("Usage: KafkaWordCount ") 22 | System.exit(1) 23 | } 24 | 25 | StreamingExamples.setStreamingLogLevels() 26 | 27 | val Array(zkQuorum, group, topics, numThreads) = args 28 | val sparkConf = new SparkConf().setAppName("KafkaWordCount") 29 | val ssc = new StreamingContext(sparkConf, Seconds(2)) 30 | ssc.checkpoint("checkpoint") 31 | 32 | val topicMap = topics.split(",").map((_, numThreads.toInt)).toMap 33 | val lines = KafkaUtils.createStream(ssc, zkQuorum, group, topicMap).map(_._2) 34 | val words = lines.flatMap(_.split(" ")) 35 | val wordCounts = words.map(x => (x, 1L)) 36 | .reduceByKeyAndWindow(_ + _, _ - _, Minutes(10), Seconds(2), 2) 37 | wordCounts.print() 38 | 39 | ssc.start() 40 | ssc.awaitTermination() 41 | } 42 | ``` 43 | 44 | 需要注意的点: 45 | 46 | * Kafka Topic 的 partitions 与RDD 的 partitions 没有直接关系,不能一一对应。如果增加 topic 的 partition 个数的话仅仅会增加单个 Receiver 接收数据的线程数。事实上,使用这种方法只会在一个 executor 上启用一个 Receiver,该 Receiver 包含一个线程池,线程池的线程个数与所有 topics 的 partitions 个数总和一致,每条线程接收一个 topic 的一个 partition 的数据。而并不会增加处理数据时的并行度。不过度展开了,有兴趣请移步:[揭开Spark Streaming神秘面纱② - ReceiverTracker 与数据导入](http://www.jianshu.com/p/3195fb3c4191) 47 | * 对于一个 topic,可以使用多个 groupid 相同的 input DStream 来使用多个 Receivers 来增加并行度,然后 union 他们;对于多个 topics,除了可以用上个办法增加并行度外,还可以对不同的 topic 使用不同的 input DStream 然后 union 他们来增加并行度 48 | * 如果你启用了 WAL,为能将接收到的数据将以 log 的方式在指定的存储系统备份一份,需要指定输入数据的存储等级为 ```StorageLevel.MEMORY_AND_DISK_SER``` 或 ```StorageLevel.MEMORY_AND_DISK_SER_2``` 49 | 50 | ##方式二:Without Receiver 51 | 自 Spark-1.3.0 起,提供了不需要 Receiver 的方法。替代了使用 receivers 来接收数据,该方法定期查询每个 topic+partition 的 lastest offset,并据此决定每个 batch 要接收的 offsets 范围。需要注意的是,该特性在 Spark-1.3(Scala API)是实验特性。 52 | 53 | 该方式相比使用 Receiver 的方式有以下好处: 54 | 55 | * 简化并行:不再需要创建多个 kafka input DStream 然后再 union 这些 input DStream。使用 directStream,Spark Streaming会创建与 Kafka partitions 相同数量的 paritions 的 RDD,RDD 的 partition与 Kafka 的 partition 一一对应,这样更易于理解及调优 56 | * 高效:在方式一中要保证数据零丢失需要启用 WAL(预写日志),这会占用更多空间。而在方式二中,可以直接从 Kafka 指定的 topic 的指定 offsets 处恢复数据,不需要使用 WAL 57 | * 恰好一次语义保证:第一种方式使用了 Kafka 的 high level API 来在 Zookeeper 中存储已消费的 offsets。这在某些情况下会导致一些数据被消费两次,比如 streaming app 在处理某个 batch 内已接受到的数据的过程中挂掉,但是数据已经处理了一部分,但这种情况下无法将已处理数据的 offsets 更新到 Zookeeper 中,下次重启时,这批数据将再次被消费且处理。方式二中,只要将 output 操作和保存 offsets 操作封装成一个原子操作就能避免失败后的重复消费和处理,从而达到恰好一次的语义(Exactly-once) 58 | 59 | 当然,方式二相比于方式一也有缺陷,即不会自动更新消费的 offsets 至 Zookeeper,从而一些监控工具就无法看到消费进度。方式二需要自行保存消费的 offsets,这在 topic 新增 partition 时会变得更加麻烦。 60 | 61 | 下面来说说怎么使用方式二,示例如下: 62 | 63 | ``` 64 | import org.apache.spark.streaming.kafka._ 65 | 66 | val directKafkaStream = KafkaUtils.createDirectStream[ 67 | [key class], [value class], [key decoder class], [value decoder class] ]( 68 | streamingContext, [map of Kafka parameters], [set of topics to consume]) 69 | ``` 70 | 71 | Kafka 参数中,需要指定 ```metadata.broker.list``` 或 ```bootstrap.servers```。默认会从每个 topic 的每个 partition 的 lastest offset 开始消费,也可以通过将 ```auto.offset.reset``` 设置为 ```smallest``` 来从每个 topic 的每个 partition 的 smallest offset 开始消费。 72 | 73 | 使用其他重载的 ```KafkaUtils.createDirectStream``` 函数也支持从任意 offset 消费数据。另外,如果你想在每个 bath 内获取消费的 offset,可以按下面的方法做: 74 | 75 | ``` 76 | // Hold a reference to the current offset ranges, so it can be used downstream 77 | var offsetRanges = Array[OffsetRange]() 78 | 79 | directKafkaStream.transform { rdd => 80 | offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges 81 | rdd 82 | }.map { 83 | ... 84 | }.foreachRDD { rdd => 85 | for (o <- offsetRanges) { 86 | println(s"${o.topic} ${o.partition} ${o.fromOffset} ${o.untilOffset}") 87 | } 88 | ... 89 | } 90 | ``` 91 | 92 | 你可以用上面的方法获取 offsets 并保存到Zookeeper或数据库中等。 93 | 94 | 需要注意的是,RDD partition 与 Kafka partition 的一一对应关系在shuffle或repartition之后将不复存在( 如reduceByKey() 或 window() ),所以要获取 offset 需要在此之前。 95 | 96 | 另一个需要注意的是,由于方式二不使用 Receiver,所以任何 Receiver 相关的配置,即```spark.streaming.receiver.*```均不生效,需要转而使用 ```spark.streaming.kafka.*```。一个重要的参数是 ```spark.streaming.kafka.maxRatePerPartition```,用来控制每个 partition 每秒能接受的数据条数的上限。 97 | 98 | ##参考 99 | 1. http://spark.apache.org/docs/latest/streaming-kafka-integration.html 100 | 101 | --- 102 | 103 | 欢迎关注我的微信公众号:FunnyBigData 104 | 105 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 106 | -------------------------------------------------------------------------------- /spark-streaming/【实战篇】如何优雅的停止你的-Spark-Streaming-Application.md: -------------------------------------------------------------------------------- 1 | ##Spark 1.3及其前的版本 2 | 你的一个 spark streaming application 已经好好运行了一段时间了,这个时候你因为某种原因要停止它。你应该怎么做?直接暴力 kill 该 application 吗?这可能会导致数据丢失,因为 receivers 可能已经接受到了数据,但该数据还未被处理,当你强行停止该 application,driver 就没办法处理这些本该处理的数据。 3 | 4 | 所以,我们应该使用一种避免数据丢失的方式,官方建议调用 ```StreamingContext#stop(stopSparkContext: Boolean, stopGracefully: Boolean)```,将 stopGracefully 设置为 true,这样可以保证在 driver 结束前处理完所有已经接受的数据。 5 | 6 | 一个 streaming application 往往是长时间运行的,所以存在两个问题: 7 | 8 | 1. 应该在什么时候去调用 ```StreamingContext#stop``` 9 | 2. 当 streaming application 已经在运行了该怎么去调用 ```StreamingContext#stop``` 10 | 11 | ##how 12 | 通过 ```Runtime.getRuntime().addShutdownHook``` 注册关闭钩子, JVM将在关闭之前执行关闭钩子中的 ```run``` 函数(不管是正常退出还是异常退出都会调用),所以我们可以在 driver 代码中加入以下代码: 13 | 14 | ``` 15 | Runtime.getRuntime().addShutdownHook(new Thread() { 16 | override def run() { 17 | log("Shutting down streaming app...") 18 | streamingContext.stop(true, true) 19 | log("Shutdown of streaming app complete.") 20 | } 21 | }) 22 | ``` 23 | 24 | 这样就能保证即使 application 被强行 kill 掉,在 driver 结束前,```streamingContext.stop(true, true)```也会被调用,从而保证已接收的数据都会被处理。 25 | 26 | ##Spark 1.4及其后的版本 27 | 上一小节介绍的方法仅适用于 1.3及以前的版本,在 1.4及其后的版本中不仅不能保证生效,甚至会引起死锁等线程问题。在 1.4及其后的版本中,我们只需设置 ```spark.streaming.stopGracefullyOnShutdown``` 为 ```true``` 即可达到上一小节相同的效果。 28 | 29 | 下面来分析为什么上一小节介绍的方法在 1.4其后的版本中不能用。首先,需要明确的是: 30 | 31 | 1. 当我们注册了多个关闭钩子时,JVM开始启用其关闭序列时,它会以某种未指定的顺序启动所有已注册的关闭钩子,并让它们同时运行 32 | 2. 万一不止一个关闭钩子,它们将并行地运行,并容易引发线程问题,例如死锁 33 | 34 | 综合以上两点,我们可以明确,如果除了我们注册的关闭钩子外,driver 还有注册了其他钩子,将会引发上述两个问题。 35 | 36 | 在 StreamingContext#start 中,会调用 37 | 38 | ``` 39 | ShutdownHookManager.addShutdownHook(StreamingContext.SHUTDOWN_HOOK_PRIORITY)(stopOnShutdown) 40 | ``` 41 | 42 | 该函数最终注册一个关闭钩子,并会在 ```run``` 方法中调用 ```stopOnShutdown```, 43 | 44 | ``` 45 | private def stopOnShutdown(): Unit = { 46 | val stopGracefully = conf.getBoolean("spark.streaming.stopGracefullyOnShutdown", false) 47 | logInfo(s"Invoking stop(stopGracefully=$stopGracefully) from shutdown hook") 48 | // Do not stop SparkContext, let its own shutdown hook stop it 49 | stop(stopSparkContext = false, stopGracefully = stopGracefully) 50 | } 51 | ``` 52 | 53 | 从 ```stopOnShutdown``` 中会根据 ```stopGracefully``` 的值来决定是否以优雅的方式结束 ```driver,而 stopGracefully``` 的值由 ```spark.streaming.stopGracefullyOnShutdown``` 决定。结合上文,也就能说明为什么 ```spark.streaming.stopGracefullyOnShutdown```能决定是否优雅的结束 application 和为什么上一小节的方法不适用与 1.4及其后版本。 54 | 55 | --- 56 | 57 | 欢迎关注我的微信公众号:FunnyBigData 58 | 59 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 60 | -------------------------------------------------------------------------------- /spark-streaming/【容错篇】Spark-Streaming的还原药水——Checkpoint.md: -------------------------------------------------------------------------------- 1 | 一个 Streaming Application 往往需要7*24不间断的跑,所以需要有抵御意外的能力(比如机器或者系统挂掉,JVM crash等)。为了让这成为可能,Spark Streaming需要 checkpoint 足够多信息至一个具有容错设计的存储系统才能让 Application 从失败中恢复。Spark Streaming 会 checkpoint 两种类型的数据。 2 | 3 | * Metadata(元数据) checkpointing - 保存定义了 Streaming 计算逻辑至类似 HDFS 的支持容错的存储系统。用来恢复 driver,元数据包括: 4 | * 配置 - 用于创建该 streaming application 的所有配置 5 | * DStream 操作 - DStream 一些列的操作 6 | * 未完成的 batches - 那些提交了 job 但尚未执行或未完成的 batches 7 | * Data checkpointing - 保存已生成的RDDs至可靠的存储。这在某些 stateful 转换中是需要的,在这种转换中,生成 RDD 需要依赖前面的 batches,会导致依赖链随着时间而变长。为了避免这种没有尽头的变长,要定期将中间生成的 RDDs 保存到可靠存储来切断依赖链 8 | 9 | 总之,metadata checkpointing 主要用来恢复 driver;而 RDD数据的 checkpointing 对于stateful 转换操作是必要的。 10 | 11 | ##什么时候需要启用 checkpoint? 12 | 什么时候该启用 checkpoint 呢?满足以下任一条件: 13 | 14 | * 使用了 stateful 转换 - 如果 application 中使用了```updateStateByKey```或```reduceByKeyAndWindow```等 stateful 操作,必须提供 checkpoint 目录来允许定时的 RDD checkpoint 15 | * 希望能从意外中恢复 driver 16 | 17 | 如果 streaming app 没有 stateful 操作,也允许 driver 挂掉后再次重启的进度丢失,就没有启用 checkpoint的必要了。 18 | 19 | ##如何使用 checkpoint? 20 | 启用 checkpoint,需要设置一个支持容错 的、可靠的文件系统(如 HDFS、s3 等)目录来保存 checkpoint 数据。通过调用 ```streamingContext.checkpoint(checkpointDirectory)``` 来完成。另外,如果你想让你的 application 能从 driver 失败中恢复,你的 application 要满足: 21 | 22 | * 若 application 为首次重启,将创建一个新的 StreamContext 实例 23 | * 如果 application 是从失败中重启,将会从 checkpoint 目录导入 checkpoint 数据来重新创建 StreamingContext 实例 24 | 25 | 通过 ```StreamingContext.getOrCreate``` 可以达到目的: 26 | 27 | ``` 28 | // Function to create and setup a new StreamingContext 29 | def functionToCreateContext(): StreamingContext = { 30 | val ssc = new StreamingContext(...) // new context 31 | val lines = ssc.socketTextStream(...) // create DStreams 32 | ... 33 | ssc.checkpoint(checkpointDirectory) // set checkpoint directory 34 | ssc 35 | } 36 | 37 | // Get StreamingContext from checkpoint data or create a new one 38 | val context = StreamingContext.getOrCreate(checkpointDirectory, functionToCreateContext _) 39 | 40 | // Do additional setup on context that needs to be done, 41 | // irrespective of whether it is being started or restarted 42 | context. ... 43 | 44 | // Start the context 45 | context.start() 46 | context.awaitTermination() 47 | ``` 48 | 49 | 如果 checkpointDirectory 存在,那么 context 将导入 checkpoint 数据。如果目录不存在,函数 functionToCreateContext 将被调用并创建新的 context 50 | 51 | 除调用 getOrCreate 外,还需要你的集群模式支持 driver 挂掉之后重启之。例如,在 yarn 模式下,driver 是运行在 ApplicationMaster 中,若 ApplicationMaster 挂掉,yarn 会自动在另一个节点上启动一个新的 ApplicationMaster。 52 | 53 | 需要注意的是,随着 streaming application 的持续运行,checkpoint 数据占用的存储空间会不断变大。因此,需要小心设置checkpoint 的时间间隔。设置得越小,checkpoint 次数会越多,占用空间会越大;如果设置越大,会导致恢复时丢失的数据和进度越多。一般推荐设置为 batch duration 的5~10倍。 54 | 55 | ##导出 checkpoint 数据 56 | 上文提到,checkpoint 数据会定时导出到可靠的存储系统,那么 57 | 58 | 1. 在什么时机进行 checkpoint 59 | 2. checkpoint 的形式是怎么样的 60 | 61 | ###checkpoint 的时机 62 | 在 Spark Streaming 中,JobGenerator 用于生成每个 batch 对应的 jobs,它有一个定时器,定时器的周期即初始化 StreamingContext 时设置的 batchDuration。这个周期一到,JobGenerator 将调用generateJobs方法来生成并提交 jobs,这之后调用 doCheckpoint 方法来进行 checkpoint。doCheckpoint 方法中,会判断当前时间与 streaming application start 的时间之差是否是 checkpoint duration 的倍数,只有在是的情况下才进行 checkpoint。 63 | 64 | ###checkpoint 的形式 65 | 最终 checkpoint 的形式是将类 Checkpoint的实例序列化后写入外部存储,值得一提的是,有专门的一条线程来做将序列化后的 checkpoint 写入外部存储。类 Checkpoint 包含以下数据 66 | 67 | 68 | ![](http://upload-images.jianshu.io/upload_images/204749-f40597d29d729280.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 69 | 70 | 71 | 除了 Checkpoint 类,还有 CheckpointWriter 类用来导出 checkpoint,CheckpointReader 用来导入 checkpoint 72 | 73 | ##Checkpoint 的局限 74 | Spark Streaming 的 checkpoint 机制看起来很美好,却有一个硬伤。上文提到最终刷到外部存储的是类 Checkpoint 对象序列化后的数据。那么在 Spark Streaming application 重新编译后,再去反序列化 checkpoint 数据就会失败。这个时候就必须新建 StreamingContext。 75 | 76 | 针对这种情况,在我们结合 Spark Streaming + kafka 的应用中,我们自行维护了消费的 offsets,这样一来及时重新编译 application,还是可以从需要的 offsets 来消费数据,这里只是举个例子,不详细展开了。 77 | 78 | --- 79 | 80 | 欢迎关注我的微信公众号:FunnyBigData 81 | 82 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 83 | -------------------------------------------------------------------------------- /spark-streaming/【容错篇】WAL在Spark-Streaming中的应用.md: -------------------------------------------------------------------------------- 1 | # 【容错篇】WAL在Spark Streaming中的应用 2 | WAL 即 write ahead log(预写日志),是在 1.2 版本中就添加的特性。作用就是,将数据通过日志的方式写到可靠的存储,比如 HDFS、s3,在 driver 或 worker failure 时可以从在可靠存储上的日志文件恢复数据。WAL 在 driver 端和 executor 端都有应用。我们分别来介绍。 3 | 4 | ##WAL在 driver 端的应用 5 | ###何时创建 6 | 用于写日志的对象 ```writeAheadLogOption: WriteAheadLog``` 7 | 在 StreamingContext 中的 JobScheduler 中的 ReceiverTracker 的 ReceivedBlockTracker 构造函数中被创建,ReceivedBlockTracker 用于管理已接收到的 blocks 信息。需要注意的是,这里只需要启用 checkpoint 就可以创建该 driver 端的 WAL 管理实例,而不需要将 ```spark.streaming.receiver.writeAheadLog.enable``` 设置为 ```true```。 8 | 9 | 参见:[揭开Spark Streaming神秘面纱② - ReceiverTracker 与数据导入 10 | ](http://www.jianshu.com/p/3195fb3c4191) 11 | ###写什么、何时写 12 | **写什么** 13 | 首选需要明确的是,ReceivedBlockTracker 通过 WAL 写入 log 文件的内容是3种事件(当然,会进行序列化): 14 | 15 | * case class BlockAdditionEvent(receivedBlockInfo: ReceivedBlockInfo);即新增了一个 block 及该 block 的具体信息,包括 streamId、blockId、数据条数等 16 | * case class BatchAllocationEvent(time: Time, allocatedBlocks: AllocatedBlocks);即为某个 batchTime 分配了哪些 blocks 作为该 batch RDD 的数据源 17 | * case class BatchCleanupEvent(times: Seq[Time]);即清理了哪些 batchTime 对应的 blocks 18 |  19 | 知道了写了什么内容,结合源码,也不难找出是什么时候写了这些内容。需要再次注意的是,写上面这三种事件,也不需要将 ```spark.streaming.receiver.writeAheadLog.enable``` 设置为 ```true```。 20 | 21 | **何时写BlockAdditionEvent** 22 | 在[揭开Spark Streaming神秘面纱② - ReceiverTracker 与数据导入 23 | ](http://www.jianshu.com/p/3195fb3c4191)一文中,已经介绍过当 Receiver 接收到数据后会调用 ```ReceiverSupervisor#pushAndReportBlock```方法,该方法将 block 数据存储并写一份到日志文件中(即 WAL),之后最终将 block 信息,即 ```receivedBlockInfo```(包括 streamId、batchId、数据条数)传递给 ```ReceivedBlockTracker```. 24 | 25 | 当 ```ReceivedBlockTracker``` 接收到 ```receivedBlockInfo``` 后,将之封装成 ```BlockAdditionEvent(receivedBlockInfo)``` 并写入日志(WAL)。 26 | 27 | 抛开代码调用逻辑不谈,一句话总结的话,就是当 Receiver 接收数据产生新的 block 时,最终会触发产生并写 ```BlockAdditionEvent``` 28 | 29 | **何时写BatchAllocationEvent** 30 | 在[揭开Spark Streaming神秘面纱③ - 动态生成 job](http://www.jianshu.com/p/ee845802921e)一文中介绍了 JobGenerator 每隔 batch duration 就会为这个 batch 生成对应的 jobs。在生成 jobs 的时候需要为 RDD 提供数据,这个时候就会触发执行 31 | 32 | ``` 33 | jobScheduler.receiverTracker.allocateBlocksToBatch(time) 34 | ``` 35 | 36 | 该操作将把所有该 streamId 对应的已接收存储但未分配的 blocks 都分配给该 batch,我们知道,ReceivedBlockTracker 保存着所有的 blocks 信息,所以为某个 batch 分配 blocks 这个分配请求最终会给到 ReceivedBlockTracker,ReceivedBlockTracker 在确认要分配哪些 blocks 之后,会将给某个 batchTime 分配了哪些 blocks 的对应关系封装成 ```BatchAllocationEvent(batchTime, allocatedBlocks)``` 并写入日志文件(WAL),这之后才进行真正的分配。 37 | 38 | **何时写BatchCleanupEvent** 39 | 40 | 从我以前写的一些文章中可以知道,一个 batch 对应的是一个 jobSet,因为在一个 batch 可能会有多个 DStream 执行了多次 output 操作,每个 output 操作都将生成一个 job,这些 job 将组成 jobSet。总共有两种时机会触发将 ```BatchCleanupEvent``` 事件写入日志(WAL),我们进行依次介绍 41 | 42 | 我们先来介绍第一种,废话不多说,直接看具体步骤: 43 | 44 | 1. 每当 jobSet 中某一个 job 完成的时候,job 调度器会去检查该 job 对应的 jobSet 中的所有 job 是否均已完成 45 | 2. 若是,会通过 jobGenerator.eventLoop 给自身发送 ```ClearMetadata``` 消息 46 | 3. jobGenerator 在接收到该消息后,调用自身clearMetadata方法,clearMetadata方法最终会调用到 ```ReceiverTracker#cleanupOldBlocksAndBatches```,具体cleanupOldBlocksAndBatches方法干了什么稍后分析 47 | 48 | 另一种时机如下: 49 | 50 | 1. JobGenerator在完成 checkpoint 时,会给自身发送一个 ```ClearCheckpointData``` 消息 51 | 2. JobGenerator在收到 ```ClearCheckpointData``` 消息后,调用 ```clearCheckpointData``` 方法 52 | 3. 在 ```JobGenerator#ClearCheckpointData``` 方法中,会调用到 ```ReceiverTracker#drcleanupOldBlocksAndBatches``` 53 | 54 | 从上面的两小段分析我们可以知道,当一个 batch 的 jobSet 中的 jobs 都完成的时候和每次 checkpoint操作完成的时候会触发执行 ```ReceiverTracker#cleanupOldBlocksAndBatches``` 方法,该方法里做了什么呢?见下图: 55 | ![](http://upload-images.jianshu.io/upload_images/204749-71627efceab321ae.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 56 | 57 | 58 | 上图描述了以上两个时机下,是如何: 59 | 60 | 1. 将 batch cleanup 事件写入 WAL 中 61 | 2. 清理过期的 blocks 及 batches 的元数据 62 | 3. 清理过期的 blocks 数据(只有当将 ```spark.streaming.receiver.writeAheadLog.enable``` 设置为 ```true```才会执行这一步) 63 | 64 | ##WAL 在 executor 端的应用 65 | Receiver 接收到的数据会源源不断的传递给 ReceiverSupervisor,是否启用 WAL 机制(即是否将 ```spark.streaming.receiver.writeAheadLog.enable``` 设置为 ```true```)会影响 ReceiverSupervisor 在存储 block 时的行为: 66 | 67 | * 不启用 WAL:你设置的StorageLevel是什么,就怎么存储。比如MEMORY_ONLY只会在内存中存一份,MEMORY_AND_DISK会在内存和磁盘上各存一份等 68 | * 启用 WAL:在StorageLevel指定的存储的基础上,写一份到 WAL 中。存储一份在 WAL 上,更不容易丢数据但性能损失也比较大 69 | 70 | 关于什么时候以及如何清理存储在 WAL 中的过期的数据已在上图中说明 71 | 72 | ##WAL 使用建议 73 | 关于是否要启用 WAL,要视具体的业务而定: 74 | 75 | * 若可以接受一定的数据丢失,则不需要启用 WAL,因为对性能影响较大 76 | * 若完全不能接受数据丢失,那就需要同时启用 checkpoint 和 WAL,checkpoint 保存着执行进度(比如已生成但未完成的 jobs),WAL 中保存着 blocks 及 blocks 元数据(比如保存着未完成的 jobs 对应的 blocks 信息及 block 文件)。同时,这种情况可能要在数据源和 Streaming Application 中联合来保证 exactly once 语义 77 | 78 | --- 79 | 80 | 欢迎关注我的微信公众号:FunnyBigData 81 | 82 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 83 | -------------------------------------------------------------------------------- /spark-streaming/为什么-Spark-Streaming-+-Kafka-无法保证-exactly-once?.md: -------------------------------------------------------------------------------- 1 | ##Streaming job 的调度与执行 2 | 结合文章 [揭开Spark Streaming神秘面纱④ - job 的提交与执行](http://www.jianshu.com/p/d6168b4bea88)我们画出了如下 job 调度执行流程图: 3 | 4 | 5 | ![](http://upload-images.jianshu.io/upload_images/204749-894a30f8b2455893.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 6 | 7 | 8 | ##为什么很难保证 exactly once 9 | 上面这张流程图最主要想说明的就是,**job 的提交执行是异步的,与 checkpoint 操作并不是原子操作**。这样的机制会引起数据重复消费问题: 10 | 11 | 为了简化问题容易理解,我们假设一个 batch 只生成一个 job,并且 ```spark.streaming.concurrentJobs``` 值为1,该值代表 jobExecutor 线程池中线程的个数,也即可以同时执行的 job 的个数。 12 | 13 | 假设,batch duration 为2s,一个 batch 的总共处理时间为1s,此时,一个 batch 开始了,第一步生成了一个 job,假设花了0.1s,然后把该 job 丢到了 jobExecutor 线程池中等待调度执行,由于 checkpoint 操作和 job 在线程池中执行是异步的,在0.2s 的时候,checkpoint 操作完成并且此时开始了 job 的执行。 14 | 15 | 注意,这个时候 checkpoint 完成了并且该 job 在 checkpoint 中的状态是未完成的,随后在第1s 的时候 job 完成了,那么在这个 batch 结束的时候 job 已经完成了但该 job 在 checkpoint 中的状态是未完成的(要了解 checkpoint 都保存了哪些数据请移步[Spark Streaming的还原药水——Checkpoint](http://www.jianshu.com/p/00b591c5f623))。 16 | 17 | 在下一个 batch 运行到 checkpoint 之前就挂了(比如在拉取数据的时候挂了、OOM 挂了等等异常情况),driver 随后从 checkpoint 中恢复,那么上述的 job 依然是未执行的,根据使用的 api 不同,对于这个 job 会再次拉取数据或从 wal 中恢复数据重新执行该 job,那么这种情况下该 job 的数据就就会被重复处理。比如这时记次的操作,那么次数就会比真实的多。 18 | 19 | 如果一个 batch 有多个 job 并且```spark.streaming.concurrentJobs```大于1,那么这种情况就会更加严重,因为这种情况下就会有多个 job 已经完成但在 checkpoint 中还是未完成状态,在 driver 重启后这些 job 对应的数据会被重复消费处理。 20 | 21 | --- 22 | 23 | 另一种会导致数据重复消费的情况主要是由于 Spark 处理的数据单位是 partition 引起的。比如在处理某 partition 的数据到一半的时候,由于数据内容或格式会引起抛异常,此时 task 失败,Spark 会调度另一个同样的 task 执行,那么此时引起 task 失败的那条数据之前的该 partition 数据就会被重复处理,虽然这个 task 被再次调度依然会失败。若是失败还好,如果某些特殊的情况,新的 task 执行成功了,那么我们就很难发现数据被重复消费处理了。 24 | 25 | ##如何保证 exactly once 26 | 至于如何才能保证 exactly once,其实要根据具体情况而定(废话)。总体来说,可以考虑以下几点: 27 | 28 | 1. 业务是否不能容忍即使是极少量的数据差错,如果是那么考虑 exactly once。如果可以容忍,那就没必要非实现 exactly once 不可 29 | 2. 即使重复处理极小部分数据会不会对最终结果产生影响。若不会,那重复处理就重复吧,比如排重统计 30 | 3. 若一定要保证 exactly once,应该考虑将对 partition 处理和 checkpoint或自己实现类似 checkpoint 功能的操作做成原子的操作;并且对 partition 整批数据进行类似事物的处理 31 | 32 | --- 33 | 34 | 欢迎关注我的微信公众号:FunnyBigData 35 | 36 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 37 | -------------------------------------------------------------------------------- /spark-streaming/揭开Spark-Streaming神秘面纱①---DStreamGraph-与-DStream-DAG.md: -------------------------------------------------------------------------------- 1 | 在 Spark Streaming 中,DStreamGraph 是一个非常重要的组件,主要用来: 2 | 3 | 1. 通过成员 inputStreams 持有 Spark Streaming 输入源及接收数据的方式 4 | 2. 通过成员 outputStreams 持有 Streaming app 的 output 操作,并记录 DStream 依赖关系 5 | 3. 生成每个 batch 对应的 jobs 6 | 7 | 下面,我将通过分析一个简单的例子,结合源码分析来说明 DStreamGraph 是如何发挥作用的。例子如下: 8 | 9 | ``` 10 | val sparkConf = new SparkConf().setAppName("HdfsWordCount") 11 | val ssc = new StreamingContext(sparkConf, Seconds(2)) 12 | 13 | val lines = ssc.textFileStream(args(0)) 14 | val words = lines.flatMap(_.split(" ")) 15 | val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _) 16 | wordCounts.print() 17 | ssc.start() 18 | ssc.awaitTermination() 19 | ``` 20 | 21 | ##创建 DStreamGraph 实例 22 | 代码```val ssc = new StreamingContext(sparkConf, Seconds(2))```创建了 StreamingContext 实例,StreamingContext 包含了 DStreamGraph 类型的成员graph,graph 在 StreamingContext主构造函数中被创建,如下 23 | 24 | ``` 25 | private[streaming] val graph: DStreamGraph = { 26 | if (isCheckpointPresent) { 27 | cp_.graph.setContext(this) 28 | cp_.graph.restoreCheckpointData() 29 | cp_.graph 30 | } else { 31 | require(batchDur_ != null, "Batch duration for StreamingContext cannot be null") 32 | val newGraph = new DStreamGraph() 33 | newGraph.setBatchDuration(batchDur_) 34 | newGraph 35 | } 36 | } 37 | ``` 38 | 39 | 可以看到,若当前 checkpoint 可用,会优先从 checkpoint 恢复 graph,否则新建一个。还可以从这里知道的一点是:graph 是运行在 driver 上的 40 | 41 | ##DStreamGraph记录输入源及如何接收数据 42 | DStreamGraph有和application 输入数据相关的成员和方法,如下: 43 | 44 | ``` 45 | private val inputStreams = new ArrayBuffer[InputDStream[_]]() 46 | 47 | def addInputStream(inputStream: InputDStream[_]) { 48 | this.synchronized { 49 | inputStream.setGraph(this) 50 | inputStreams += inputStream 51 | } 52 | } 53 | ``` 54 | 55 | 成员inputStreams为 InputDStream 类型的数组,InputDStream是所有 input streams(数据输入流) 的虚基类。该类提供了 start() 和 stop()方法供 streaming 系统来开始和停止接收数据。那些只需要在 driver 端接收数据并转成 RDD 的 input streams 可以直接继承 InputDStream,例如 FileInputDStream是 InputDStream 的子类,它监控一个 HDFS 目录并将新文件转成RDDs。而那些需要在 workers 上运行receiver 来接收数据的 Input DStream,需要继承 ReceiverInputDStream,比如 KafkaReceiver。 56 | 57 | 我们来看看```val lines = ssc.textFileStream(args(0))```调用。 58 | 为了更容易理解,我画出了```val lines = ssc.textFileStream(args(0))```的调用流程 59 | 60 | 61 | 62 | ![](http://upload-images.jianshu.io/upload_images/204749-dbc4b52e7c2e9cc7.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 63 | 64 | 从上面的调用流程图我们可以知道: 65 | 66 | 1. ssc.textFileStream会触发新建一个FileInputDStream。FileInputDStream继承于InputDStream,其start()方法定义了数据源及如何接收数据 67 | 2. 在FileInputDStream构造函数中,会调用```ssc.graph.addInputStream(this)```,将自身添加到 DStreamGraph 的 ```inputStreams: ArrayBuffer[InputDStream[_]]``` 中,这样 DStreamGraph 就知道了这个 Streaming App 的输入源及如何接收数据。可能你会奇怪为什么inputStreams 是数组类型,举个例子,这里再来一个 ```val lines1 = ssc.textFileStream(args(0))```,那么又将生成一个 FileInputStream 实例添加到inputStreams,所以这里需要集合类型 68 | 3. 生成FileInputDStream调用其 map 方法,将以 FileInputDStream 本身作为 partent 来构造新的 MappedDStream。对于 DStream 的 transform 操作,都将生成一个新的 DStream,和 RDD transform 生成新的 RDD 类似 69 | 70 | 与MappedDStream 不同,所有继承了 InputDStream 的定义了输入源及接收数据方式的 sreams 都没有 parent,因为它们就是最初的 streams。 71 | 72 | ##DStream 的依赖链 73 | 每个 DStream 的子类都会继承 ```def dependencies: List[DStream[_]] = List()```方法,该方法用来返回自己的依赖的父 DStream 列表。比如,没有父DStream 的 InputDStream 的 dependencies方法返回List()。 74 | 75 | MappedDStream 的实现如下: 76 | 77 | ``` 78 | class MappedDStream[T: ClassTag, U: ClassTag] ( 79 | parent: DStream[T], 80 | mapFunc: T => U 81 | ) extends DStream[U](parent.ssc) { 82 | 83 | override def dependencies: List[DStream[_]] = List(parent) 84 | 85 | ... 86 | } 87 | ``` 88 | 89 | 在上例中,构造函数参数列表中的 parent 即在 ssc.textFileStream 中new 的定义了输入源及数据接收方式的最初的 FileInputDStream实例,这里的 dependencies方法将返回该FileInputDStream实例,这就构成了第一条依赖。可用如下图表示,这里特地将 input streams 用蓝色表示,以强调其与普通由 transform 产生的 DStream 的不同: 90 | 91 | 92 | ![](http://upload-images.jianshu.io/upload_images/204749-30e8a5026ae33154.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 93 | 94 | 95 | 继续来看```val words = lines.flatMap(_.split(" "))```,flatMap如下: 96 | 97 | ``` 98 | def flatMap[U: ClassTag](flatMapFunc: T => Traversable[U]): DStream[U] = ssc.withScope { 99 | new FlatMappedDStream(this, context.sparkContext.clean(flatMapFunc)) 100 | } 101 | ``` 102 | 103 | 每一个 transform 操作都将创建一个新的 DStream,flatMap 操作也不例外,它会创建一个FlatMappedDStream,FlatMappedDStream的实现如下: 104 | 105 | ``` 106 | class FlatMappedDStream[T: ClassTag, U: ClassTag]( 107 | parent: DStream[T], 108 | flatMapFunc: T => Traversable[U] 109 | ) extends DStream[U](parent.ssc) { 110 | 111 | override def dependencies: List[DStream[_]] = List(parent) 112 | 113 | ... 114 | } 115 | ``` 116 | 117 | 与 MappedDStream 相同,FlatMappedDStream#dependencies也返回其依赖的父 DStream,及 lines,到这里,依赖链就变成了下图: 118 | 119 | 120 | ![](http://upload-images.jianshu.io/upload_images/204749-062ac48682d6cfde.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 121 | 122 | 123 | 之后的几步操作不再这样具体分析,到生成wordCounts时,依赖图将变成下面这样: 124 | 125 | 126 | 127 | ![](http://upload-images.jianshu.io/upload_images/204749-cdc22eefe55d261d.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 128 | 129 | 在 DStream 中,与 transofrm 相对应的是 output 操作,包括 ```print```, ```saveAsTextFiles```, ```saveAsObjectFiles```, ```saveAsHadoopFiles```, ```foreachRDD```。output 操作中,会创建ForEachDStream实例并调用register方法将自身添加到DStreamGraph.outputStreams成员中,该ForEachDStream实例也会持有是调用的哪个 output 操作。本例的代码调用如下,只需看箭头所指几行代码 130 | 131 | 132 | ![](http://upload-images.jianshu.io/upload_images/204749-17591982bd064c2f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 133 | 134 | 与 DStream transform 操作返回一个新的 DStream 不同,output 操作不会返回任何东西,只会创建一个ForEachDStream作为依赖链的终结。 135 | 136 | 至此, 生成了完成的依赖链,也就是 DAG,如下图(这里将 ForEachDStream 标为黄色以显示其与众不同): 137 | 138 | 139 | ![](http://upload-images.jianshu.io/upload_images/204749-7b638055eaf21878.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 140 | 141 | 这里的依赖链又叫 DAG。本文以一个简单的例子说明 DStream DAG 的生成过程,之后将再写两篇文章说明如何根据这个 DStream DAG 得到 RDD DAG 及如何定时生成 job。 142 | 143 | --- 144 | 145 | 欢迎关注我的微信公众号:FunnyBigData 146 | 147 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 148 | -------------------------------------------------------------------------------- /spark-streaming/揭开Spark-Streaming神秘面纱②---ReceiverTracker-与数据导入.md: -------------------------------------------------------------------------------- 1 | Spark Streaming 在数据接收与导入方面需要满足有以下三个特点: 2 | 3 | 1. 兼容众多输入源,包括HDFS, Flume, Kafka, Twitter and ZeroMQ。还可以自定义数据源 4 | 2. 要能为每个 batch 的 RDD 提供相应的输入数据 5 | 3. 为适应 7*24h 不间断运行,要有接收数据挂掉的容错机制 6 | 7 | ##有容乃大,兼容众多数据源 8 | 在文章[DStreamGraph 与 DStream DAG](http://www.jianshu.com/p/ffecb3386a33)中,我们提到 9 | 10 | > InputDStream是所有 input streams(数据输入流) 的虚基类。该类提供了 start() 和 stop()方法供 streaming 系统来开始和停止接收数据。那些只需要在 driver 端接收数据并转成 RDD 的 input streams 可以直接继承 InputDStream,例如 FileInputDStream是 InputDStream 的子类,它监控一个 HDFS 目录并将新文件转成RDDs。而那些需要在 workers 上运行receiver 来接收数据的 Input DStream,需要继承 ReceiverInputDStream,比如 KafkaReceiver 11 | 12 | 只需在 driver 端接收数据的 input stream 一般比较简单且在生产环境中使用的比较少,本文不作分析,只分析继承了 ReceiverInputDStream 的 input stream 是如何导入数据的。 13 | 14 | ReceiverInputDStream有一个```def getReceiver(): Receiver[T]```方法,每个继承了ReceiverInputDStream的 input stream 都必须实现这个方法。该方法用来获取将要分发到各个 worker 节点上用来接收数据的 receiver(接收器)。不同的 ReceiverInputDStream 子类都有它们对应的不同的 receiver,如KafkaInputDStream对应KafkaReceiver,FlumeInputDStream对应FlumeReceiver,TwitterInputDStream对应TwitterReceiver,如果你要实现自己的数据源,也需要定义相应的 receiver。 15 | 16 | 继承 ReceiverInputDStream 并定义相应的 receiver,就是 Spark Streaming 能兼容众多数据源的原因。 17 | 18 | ##为每个 batch 的 RDD 提供输入数据 19 | 在 StreamingContext 中,有一个重要的组件叫做 ReceiverTracker,它是 Spark Streaming 作业调度器 JobScheduler 的成员,负责启动、管理各个 receiver 及管理各个 receiver 接收到的数据。 20 | 21 | ###确定 receiver 要分发到哪些 executors 上执行 22 | 23 | ####创建 ReceiverTracker 实例 24 | 我们来看 ```StreamingContext#start()``` 方法部分调用实现,如下: 25 | 26 | ![](http://upload-images.jianshu.io/upload_images/204749-bf9b38d23925a091.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 27 | 28 | 29 | 可以看到,```StreamingContext#start()``` 会调用 ```JobScheduler#start()``` 方法,在 ```JobScheduler#start()``` 中,会创建一个新的 ReceiverTracker 实例 receiverTracker,并调用其 start() 方法。 30 | 31 | ####ReceiverTracker#start() 32 | 继续跟进 ```ReceiverTracker#start()```,如下图,它主要做了两件事: 33 | 34 | 1. 初始化一个 endpoint: ReceiverTrackerEndpoint,用来接收和处理来自 ReceiverTracker 和 receivers 发送的消息 35 | 2. 调用 launchReceivers 来自将各个 receivers 分发到 executors 上 36 | 37 | 38 | ![](http://upload-images.jianshu.io/upload_images/204749-355a9beff1de7903.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 39 | 40 | 41 | ####ReceiverTracker#launchReceivers() 42 | 继续跟进 launchReceivers,它也主要干了两件事: 43 | 44 | 1. 获取 DStreamGraph.inputStreams 中继承了 ReceiverInputDStream 的 input streams 的 receivers。也就是数据接收器 45 | 2. 给消息接收处理器 endpoint 发送 StartAllReceivers(receivers)消息。直接返回,不等待消息被处理 46 | 47 | 48 | ![](http://upload-images.jianshu.io/upload_images/204749-ced9c860d8d7c02a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 49 | 50 | ####处理StartAllReceivers消息 51 | endpoint 在接收到消息后,会先判断消息类型,对不同的消息做不同处理。对于StartAllReceivers消息,处理流程如下: 52 | 53 | 1. 计算每个 receiver 要分发的目的 executors。遵循两条原则: 54 | * 将 receiver 分布的尽量均匀 55 | * 如果 receiver 的preferredLocation本身不均匀,以preferredLocation为准 56 | 2. 遍历每个 receiver,根据第1步中得到的目的 executors 调用 startReceiver 方法 57 | 58 | 59 | ![](http://upload-images.jianshu.io/upload_images/204749-933b30645f821f62.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 60 | 61 | 到这里,已经确定了每个 receiver 要分发到哪些 executors 上 62 | 63 | ###启动 receivers 64 | 接上,通过 ```ReceiverTracker#startReceiver(receiver: Receiver[_], scheduledExecutors: Seq[String])``` 来启动 receivers,我们来看具体流程: 65 | 66 | ![](http://upload-images.jianshu.io/upload_images/204749-cd51158e2d2877d1.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 67 | 68 | 如上流程图所述,分发和启动 receiver 的方式不可谓不精彩。其中,startReceiverFunc 函数主要实现如下: 69 | 70 | ``` 71 | val supervisor = new ReceiverSupervisorImpl( 72 | receiver, SparkEnv.get, serializableHadoopConf.value, checkpointDirOption) 73 | supervisor.start() 74 | supervisor.awaitTermination() 75 | ``` 76 | 77 | supervisor.start() 中会调用 receiver#onStart 后立即返回。receiver#onStart 一般自行新建线程或线程池来接收数据,比如在 KafkaReceiver 中,就新建了线程池,在线程池中接收 topics 的数据。 78 | supervisor.start() 返回后,由 supervisor.awaitTermination() 阻塞住线程,以让这个 task 一直不退出,从而可以源源不断接收数据。 79 | 80 | ###数据流转 81 | 82 | 83 | ![](http://upload-images.jianshu.io/upload_images/204749-372c9a75a4b76f9b.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 84 | 85 | 86 | 上图为 receiver 接收到的数据的流转过程,让我们来逐一分析 87 | ####Step1: Receiver -> ReceiverSupervisor 88 | 这一步中,Receiver 将接收到的数据源源不断地传给 ReceiverSupervisor。Receiver 调用其 store(...) 方法,store 方法中继续调用 supervisor.pushSingle 或 supervisor.pushArrayBuffer 等方法来传递数据。Receiver#store 有多重形式, ReceiverSupervisor 也有 pushSingle、pushArrayBuffer、pushIterator、pushBytes 方法与不同的 store 对应。 89 | 90 | * pushSingle: 对应单条小数据 91 | * pushArrayBuffer: 对应数组形式的数据 92 | * pushIterator: 对应 iterator 形式数据 93 | * pushBytes: 对应 ByteBuffer 形式的块数据 94 | 95 | 对于细小的数据,存储时需要 BlockGenerator 聚集多条数据成一块,然后再成块存储;反之就不用聚集,直接成块存储。当然,存储操作并不在 Step1 中执行,只为说明之后不同的操作逻辑。 96 | 97 | ####Step2.1: ReceiverSupervisor -> BlockManager -> disk/memory 98 | 在这一步中,主要将从 receiver 收到的数据以 block(数据块)的形式存储 99 | 100 | 存储 block 的是```receivedBlockHandler: ReceivedBlockHandler```,根据参数```spark.streaming.receiver.writeAheadLog.enable```配置的不同,默认为 false,receivedBlockHandler对象对应的类也不同,如下: 101 | 102 | ``` 103 | private val receivedBlockHandler: ReceivedBlockHandler = { 104 | if (WriteAheadLogUtils.enableReceiverLog(env.conf)) { 105 | //< 先写 WAL,再存储到 executor 的内存或硬盘 106 | new WriteAheadLogBasedBlockHandler(env.blockManager, receiver.streamId, 107 | receiver.storageLevel, env.conf, hadoopConf, checkpointDirOption.get) 108 | } else { 109 | //< 直接存到 executor 的内存或硬盘 110 | new BlockManagerBasedBlockHandler(env.blockManager, receiver.storageLevel) 111 | } 112 | } 113 | ``` 114 | 115 | 启动 WAL 的好处就是在application 挂掉之后,可以恢复数据。 116 | 117 | ``` 118 | //< 调用 receivedBlockHandler.storeBlock 方法存储 block,并得到一个 blockStoreResult 119 | val blockStoreResult = receivedBlockHandler.storeBlock(blockId, receivedBlock) 120 | //< 使用blockStoreResult初始化一个ReceivedBlockInfo实例 121 | val blockInfo = ReceivedBlockInfo(streamId, numRecords, metadataOption, blockStoreResult) 122 | //< 发送消息通知 ReceiverTracker 新增并存储了 block 123 | trackerEndpoint.askWithRetry[Boolean](AddBlock(blockInfo)) 124 | ``` 125 | 126 | 不管是 WriteAheadLogBasedBlockHandler 还是 BlockManagerBasedBlockHandler 最终都是通过 BlockManager 将 block 数据存储 execuor 内存或磁盘或还有 WAL 方式存入。 127 | 128 | 这里需要说明的是 streamId,每个 InputDStream 都有它自己唯一的 id,即 streamId,blockInfo包含 streamId 是为了区分block 是哪个 InputDStream 的数据。之后为 batch 分配 blocks 时,需要知道每个 InputDStream 都有哪些未分配的 blocks。 129 | 130 | ####Step2.2: ReceiverSupervisor -> ReceiverTracker 131 | 将 block 存储之后,获得 block 描述信息 ```blockInfo: ReceivedBlockInfo```,这里面包含:streamId、数据位置、数据条数、数据 size 等信息。 132 | 133 | 之后,封装以 block 作为参数的 ```AddBlock(blockInfo)``` 消息并发送给 ReceiverTracker 以通知其有新增 block 数据块。 134 | 135 | ####Step3: ReceiverTracker -> ReceivedBlockTracker 136 | ReceiverTracker 收到 ReceiverSupervisor 发来的 ```AddBlock(blockInfo)``` 消息后,直接调用以下代码将 block 信息传给 ReceivedBlockTracker: 137 | 138 | ``` 139 | private def addBlock(receivedBlockInfo: ReceivedBlockInfo): Boolean = { 140 | receivedBlockTracker.addBlock(receivedBlockInfo) 141 | } 142 | ``` 143 | 144 | ```receivedBlockTracker.addBlock```中,如果启用了 WAL,会将新增的 block 信息以 WAL 方式保存。 145 | 无论 WAL 是否启用,都会将新增的 block 信息保存到 ```streamIdToUnallocatedBlockQueues: mutable.HashMap[Int, ReceivedBlockQueue]```中,该变量 key 为 InputDStream 的唯一 id,value 为已存储未分配的 block 信息。之后为 batch 分配blocks,会访问该结构来获取每个 InputDStream 对应的未消费的 blocks。 146 | 147 | ##总结 148 | 至此,本文描述了: 149 | 150 | * streaming application 如何兼容众多数据源 151 | * receivers 是如何分发并启动的 152 | * receiver 接收到的数据是如何流转的 153 | 154 | --- 155 | 156 | 欢迎关注我的微信公众号:FunnyBigData 157 | 158 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 159 | -------------------------------------------------------------------------------- /spark-streaming/揭开Spark-Streaming神秘面纱③---动态生成-job.md: -------------------------------------------------------------------------------- 1 | JobScheduler有两个重要成员,一是上文介绍的 ReceiverTracker,负责分发 receivers 及源源不断地接收数据;二是本文将要介绍的 JobGenerator,负责定时的生成 jobs 并 checkpoint。 2 | 3 | ##定时逻辑 4 | 在 JobScheduler 的主构造函数中,会创建 JobGenerator 对象。在 JobGenerator 的主构造函数中,会创建一个定时器: 5 | 6 | ``` 7 | private val timer = new RecurringTimer(clock, ssc.graph.batchDuration.milliseconds, 8 | longTime => eventLoop.post(GenerateJobs(new Time(longTime))), "JobGenerator") 9 | ``` 10 | 11 | 该定时器每隔 ```ssc.graph.batchDuration.milliseconds``` 会执行一次 ```eventLoop.post(GenerateJobs(new Time(longTime)))``` 向 eventLoop 发送 ```GenerateJobs(new Time(longTime))```消息,**eventLoop收到消息后会进行这个 batch 对应的 jobs 的生成及提交执行**,eventLoop 是一个消息接收处理器。 12 | 需要注意的是,timer 在创建之后并不会马上启动,将在 ```StreamingContext#start()``` 启动 Streaming Application 时间接调用到 ```timer.start(restartTime.milliseconds)```才启动。 13 | 14 | ##为 batch 生成 jobs 15 | 16 | 17 | ![](http://upload-images.jianshu.io/upload_images/204749-e6cd05d35d7031b9.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 18 | 19 | eventLoop 在接收到 ```GenerateJobs(new Time(longTime))```消息后的主要处理流程有以上图中三步: 20 | 21 | 1. 将已接收到的 blocks 分配给 batch 22 | 2. 生成该 batch 对应的 jobs 23 | 3. 将 jobs 封装成 JobSet 并提交执行 24 | 25 | 接下来我们就将逐一展开这三步进行分析 26 | 27 | ###将已接受到的 blocks 分配给 batch 28 | 29 | ![](http://upload-images.jianshu.io/upload_images/204749-c85680875a7557c2.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 30 | 31 | 上图是根据源码画出的为 batch 分配 blocks 的流程图,这里对 『获得 batchTime 各个 InputDStream 未分配的 blocks』作进一步说明: 32 | 在文章 『文章链接』 中我们知道了各个 ReceiverInputDStream 对应的 receivers 接收并保存的 blocks 信息会保存在 ```ReceivedBlockTracker#streamIdToUnallocatedBlockQueues```,该成员 key 为 streamId,value 为该 streamId 对应的 InputDStream 已接收保存但尚未分配的 blocks 信息。 33 | 所以获取某 InputDStream 未分配的 blocks 只要以该 InputDStream 的 streamId 来从 streamIdToUnallocatedBlockQueues 来 get 就好。获取之后,会清楚该 streamId 对应的value,以保证 block 不会被重复分配。 34 | 35 | 在实际调用中,为 batchTime 分配 blocks 时,会从streamIdToUnallocatedBlockQueues取出未分配的 blocks 塞进 ```timeToAllocatedBlocks: mutable.HashMap[Time, AllocatedBlocks]``` 中,以在之后作为该 batchTime 对应的 RDD 的输入数据。 36 | 37 | 通过以上步骤,就可以为 batch 的所有 InputDStream 分配 blocks。也就是为 batch 分配了 blocks。 38 | 39 | ###生成该 batch 对应的 jobs 40 | 41 | 42 | ![](http://upload-images.jianshu.io/upload_images/204749-1a1227d30560e8eb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 43 | 44 | 为指定 batchTime 生成 jobs 的逻辑如上图所示。你可能会疑惑,为什么 ```DStreamGraph#generateJobs(time: Time)```为什么返回 ```Seq[Job]```,而不是单个 job。这是因为,在一个 batch 内,可能会有多个 OutputStream 执行了多次 output 操作,每次 output 操作都将产生一个 Job,最终就会产生多个 Jobs。 45 | 46 | 我们结合上图对执行流程进一步分析。 47 | 在```DStreamGraph#generateJobs(time: Time)```中,对于DStreamGraph成员ArrayBuffer[DStream[_]]的每一项,调用```DStream#generateJob(time: Time)```来生成这个 outputStream 在该 batchTime 的 job。该生成过程主要有三步: 48 | 49 | ####Step1: 获取该 outputStream 在该 batchTime 对应的 RDD 50 | 每个 DStream 实例都有一个 ```generatedRDDs: HashMap[Time, RDD[T]]``` 成员,用来保存该 DStream 在每个 batchTime 生成的 RDD,当 ```DStream#getOrCompute(time: Time)```调用时 51 | 52 | * 首先会查看generatedRDDs中是否已经有该 time 对应的 RDD,若有则直接返回 53 | * 若无,则调用```compute(validTime: Time)```来生成 RDD,这一步根据每个 InputDStream继承 compute 的实现不同而不同。例如,对于 FileInputDStream,其 compute 实现逻辑如下: 54 | 55 | 1. 先通过一个 findNewFiles() 方法,找到多个新 file 56 | 2. 对每个新 file,都将其作为参数调用 sc.newAPIHadoopFile(file),生成一个 RDD 实例 57 | 3. 将 2 中的多个新 file 对应的多个 RDD 实例进行 union,返回一个 union 后的 UnionRDD 58 | 59 | ####Step2: 根据 Step1中得到的 RDD 生成最终 job 要执行的函数 jobFunc 60 | jobFunc定义如下: 61 | 62 | ``` 63 | val jobFunc = () => { 64 | val emptyFunc = { (iterator: Iterator[T]) => {} } 65 | context.sparkContext.runJob(rdd, emptyFunc) 66 | } 67 | ``` 68 | 69 | 可以看到,每个 outputStream 的 output 操作生成的 Job 其实与 RDD action 一样,最终调用 SparkContext#runJob 来提交 RDD DAG 定义的任务 70 | 71 | ####Step3: 根据 Step2中得到的 jobFunc 生成最终要执行的 Job 并返回 72 | Step2中得到了定义 Job 要干嘛的函数-jobFunc,这里便以 jobFunc及 batchTime 生成 Job 实例: 73 | 74 | ``` 75 | Some(new Job(time, jobFunc)) 76 | ``` 77 | 78 | 该Job实例将最终封装在 JobHandler 中被执行 79 | 80 | 至此,我们搞明白了 JobScheduler 是如何通过一步步调用来动态生成每个 batchTime 的 jobs。下文我们将分析这些动态生成的 jobs 如何被分发及如何执行。 81 | 82 | --- 83 | 84 | 欢迎关注我的微信公众号:FunnyBigData 85 | 86 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 87 | -------------------------------------------------------------------------------- /spark-streaming/揭开Spark-Streaming神秘面纱④---job-的提交与执行.md: -------------------------------------------------------------------------------- 1 | 前文[揭开Spark Streaming神秘面纱③ - 动态生成 job 2 | ](http://www.jianshu.com/p/ee845802921e)我们分析了 JobScheduler 是如何动态为每个 batch生成 jobs,本文将说明这些生成的 jobs 是如何被提交的。 3 | 4 | 在 JobScheduler 生成某个 batch 对应的 Seq[Job] 之后,会将 batch 及 Seq[Job] 封装成一个 JobSet 对象,JobSet 持有某个 batch 内所有的 jobs,并记录各个 job 的运行状态。 5 | 6 | 之后,调用```JobScheduler#submitJobSet(jobSet: JobSet)```来提交 jobs,在该函数中,除了一些状态更新,主要任务就是执行 7 | 8 | ``` 9 | jobSet.jobs.foreach(job => jobExecutor.execute(new JobHandler(job))) 10 | ``` 11 | 12 | 即,对于 jobSet 中的每一个 job,执行```jobExecutor.execute(new JobHandler(job))```,要搞懂这行代码干了什么,就必须了解 JobHandler 及 jobExecutor。 13 | 14 | ##JobHandler 15 | JobHandler 继承了 Runnable,为了说明与 job 的关系,其精简后的实现如下: 16 | 17 | ``` 18 | private class JobHandler(job: Job) extends Runnable with Logging { 19 | import JobScheduler._ 20 | 21 | def run() { 22 | _eventLoop.post(JobStarted(job)) 23 | PairRDDFunctions.disableOutputSpecValidation.withValue(true) { 24 | job.run() 25 | } 26 | _eventLoop = eventLoop 27 | if (_eventLoop != null) { 28 | _eventLoop.post(JobCompleted(job)) 29 | } 30 | } 31 | 32 | } 33 | ``` 34 | 35 | ```JobHandler#run``` 方法主要执行了 ```job.run()```,该方法最终将调用到 36 | [揭开Spark Streaming神秘面纱③ - 动态生成 job 37 | ](http://www.jianshu.com/p/ee845802921e) 38 | 中的『生成该 batch 对应的 jobs的Step2 定义的 jobFunc』,jonFunc 将提交对应 RDD DAG 定义的 job。 39 | 40 | ##JobExecutor 41 | 知道了 JobHandler 是用来执行 job 的,那么 JobHandler 将在哪里执行 job 呢?答案是 42 | jobExecutor,jobExecutor为 JobScheduler 成员,是一个线程池,在JobScheduler 主构造函数中创建,如下: 43 | 44 | ``` 45 | private val numConcurrentJobs = ssc.conf.getInt("spark.streaming.concurrentJobs", 1) 46 | private val jobExecutor = ThreadUtils.newDaemonFixedThreadPool(numConcurrentJobs, "streaming-job-executor") 47 | ``` 48 | 49 | JobHandler 将最终在 线程池jobExecutor 的线程中被调用,jobExecutor的线程数可通过```spark.streaming.concurrentJobs```配置,默认为1。若配置多个线程,就能让多个 job 同时运行,若只有一个线程,那么同一时刻只能有一个 job 运行。 50 | 51 | 以上,即 jobs 被执行的逻辑。 52 | 53 | --- 54 | 55 | 欢迎关注我的微信公众号:FunnyBigData 56 | 57 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 58 | -------------------------------------------------------------------------------- /spark-streaming/揭开Spark-Streaming神秘面纱⑤---Block-的生成与存储.md: -------------------------------------------------------------------------------- 1 | ReceiverSupervisorImpl共提供了4个将从 receiver 传递过来的数据转换成 block 并存储的方法,分别是: 2 | 3 | * pushSingle: 处理单条数据 4 | * pushArrayBuffer: 处理数组形式数据 5 | * pushIterator: 处理 iterator 形式处理 6 | * pushBytes: 处理 ByteBuffer 形式数据 7 | 8 | 其中,pushArrayBuffer、pushIterator、pushBytes最终调用pushAndReportBlock;而pushSingle将调用defaultBlockGenerator.addData(data),我们分别就这两种形式做说明 9 | 10 | ##pushAndReportBlock 11 | 我们针对存储 block 简化 pushAndReportBlock 后的代码如下: 12 | 13 | ``` 14 | def pushAndReportBlock( 15 | receivedBlock: ReceivedBlock, 16 | metadataOption: Option[Any], 17 | blockIdOption: Option[StreamBlockId] 18 | ) { 19 | ... 20 | val blockId = blockIdOption.getOrElse(nextBlockId) 21 | receivedBlockHandler.storeBlock(blockId, receivedBlock) 22 | ... 23 | } 24 | ``` 25 | 26 | 首先获取一个新的 blockId,之后调用 ```receivedBlockHandler.storeBlock```, ```receivedBlockHandler``` 在 ```ReceiverSupervisorImpl``` 构造函数中初始化。当启用了 checkpoint 且 ```spark.streaming.receiver.writeAheadLog.enable``` 为 ```true``` 时,```receivedBlockHandler``` 被初始化为 ```WriteAheadLogBasedBlockHandler``` 类型;否则将初始化为 ```BlockManagerBasedBlockHandler```类型。 27 | 28 | ```WriteAheadLogBasedBlockHandler#storeBlock``` 将 ArrayBuffer, iterator, bytes 类型的数据序列化后得到的 serializedBlock 29 | 30 | 1. 交由 BlockManager 根据设置的 StorageLevel 存入 executor 的内存或磁盘中 31 | 2. 通过 WAL 再存储一份 32 | 33 | 而```BlockManagerBasedBlockHandler#storeBlock```将 ArrayBuffer, iterator, bytes 类型的数据交由 BlockManager 根据设置的 StorageLevel 存入 executor 的内存或磁盘中,并不再通过 WAL 存储一份 34 | 35 | ##pushSingle 36 | pushSingle将调用 BlockGenerator#addData(data: Any) 通过积攒的方式来存储数据。接下来对 BlockGenerator 是如何积攒一条一条数据最后写入 block 的逻辑。 37 | 38 | 39 | ![](http://upload-images.jianshu.io/upload_images/204749-7db72fcc95767ec3.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 40 | 41 | 上图为 BlockGenerator 的各个成员,首选对各个成员做介绍: 42 | 43 | ###currentBuffer 44 | 变长数组,当 receiver 接收的一条一条的数据将会添加到该变长数组的尾部 45 | 46 | * 可能会有一个 receiver 的多个线程同时进行添加数据,这里是同步操作 47 | * 添加前,会由 rateLimiter 检查一下速率,是否加入的速度过快。如果过快的话就需要 block 住,等到下一秒再开始添加。最高频率由 ```spark.streaming.receiver.maxRate``` 控制,默认值为 ```Long.MaxValue```,具体含义是单个 Receiver 每秒钟允许添加的条数。 48 | 49 | ###blockIntervalTimer & blockIntervalMs 50 | 分别是定时器和时间间隔。blockIntervalTimer中有一个线程,每隔blockIntervalMs会执行以下操作: 51 | 52 | 1. 将 currentBuffer 赋值给 newBlockBuffer 53 | 2. 将 currentBuffer 指向新的空的 ArrayBuffer 对象 54 | 3. 将 newBlockBuffer 封装成 newBlock 55 | 4. 将 newBlock 添加到 blocksForPushing 队列中 56 | 57 | blockIntervalMs 由 ```spark.streaming.blockInterval``` 控制,默认是 200ms。 58 | 59 | ###blockPushingThread & blocksForPushing & blockQueueSize 60 | blocksForPushing 是一个定长数组,长度由 blockQueueSize 决定,默认为10,可通过 ```spark.streaming.blockQueueSize``` 改变。上面分析到,blockIntervalTimer中的线程会定时将 block 塞入该队列。 61 | 62 | 还有另一条线程不断送该队列中取出 block,然后调用 ```ReceiverSupervisorImpl.pushArrayBuffer(...)``` 来将 block 存储,这条线程就是blockPushingThread。 63 | 64 | PS: blocksForPushing为ArrayBlockingQueue类型。ArrayBlockingQueue是一个阻塞队列,能够自定义队列大小,当插入时,如果队列已经没有空闲位置,那么新的插入线程将阻塞到该队列,一旦该队列有空闲位置,那么阻塞的线程将执行插入 65 | 66 | 67 | 以上,通过分析各个成员,也说明了 BlockGenerator 是如何存储单条数据的。 68 | 69 | --- 70 | 71 | 欢迎关注我的微信公众号:FunnyBigData 72 | 73 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 74 | -------------------------------------------------------------------------------- /spark-streaming/揭开Spark-Streaming神秘面纱⑥---Spark-Streaming结合-Kafka-两种不同的数据接收方式比较.md: -------------------------------------------------------------------------------- 1 | DirectKafkaInputDStream 只在 driver 端接收数据,所以继承了 InputDStream,是没有 receivers 的 2 | 3 | --- 4 | 5 | 在结合 Spark Streaming 及 Kafka 的实时应用中,我们通常使用以下两个 API 来获取最初的 DStream(这里不关心这两个 API 的重载): 6 | 7 | ``` 8 | KafkaUtils#createDirectStream 9 | ``` 10 | 11 | 及 12 | 13 | ``` 14 | KafkaUtils#createStream 15 | ``` 16 | 17 | 这两个 API 除了要传入的参数不同外,接收 kafka 数据的节点、拉取数据的时机也完全不同。本文将分别就两者进行详细分析。 18 | 19 | ##KafkaUtils#createStream 20 | 先来分析 ```createStream```,在该函数中,会新建一个 ```KafkaInputDStream```对象,```KafkaInputDStream```继承于 ```ReceiverInputDStream```。我们在文章[揭开Spark Streaming神秘面纱② - ReceiverTracker 与数据导入](http://www.jianshu.com/p/3195fb3c4191)分析过 21 | 22 | 1. 继承ReceiverInputDStream的类需要重载 getReceiver 函数以提供用于接收数据的 receiver 23 | 2. recever 会调度到某个 executor 上并启动,不间断的接收数据并将收到的数据交由 ReceiverSupervisor 存成 block 作为 RDD 输入数据 24 | 25 | KafkaInputDStream当然也实现了getReceiver方法,如下: 26 | 27 | ``` 28 | def getReceiver(): Receiver[(K, V)] = { 29 | if (!useReliableReceiver) { 30 | //< 不启用 WAL 31 | new KafkaReceiver[K, V, U, T](kafkaParams, topics, storageLevel) 32 | } else { 33 | //< 启用 WAL 34 | new ReliableKafkaReceiver[K, V, U, T](kafkaParams, topics, storageLevel) 35 | } 36 | } 37 | ``` 38 | 39 | 根据是否启用 WAL,receiver 分为 KafkaReceiver 和 ReliableKafkaReceiver。[揭开Spark Streaming神秘面纱②-ReceiverTracker 与数据导入](http://www.jianshu.com/p/3195fb3c4191)一文中详细地介绍了 40 | 1. receiver 是如何被分发启动的 41 | 2. receiver 接受数据后数据的流转过程 42 | 并在 [揭开Spark Streaming神秘面纱③ - 动态生成 job](http://www.jianshu.com/p/ee845802921e) 一文中详细介绍了 43 | 1. receiver 接受的数据存储为 block 后,如何将 blocks 作为 RDD 的输入数据 44 | 2. 动态生成 job 45 | 46 | 以上两篇文章并没有具体介绍 receiver 是如何接收数据的,当然每个重载了 ReceiverInputDStream 的类的 receiver 接收数据方式都不相同。下图描述了 KafkaReceiver 接收数据的具体流程: 47 | 48 | 49 | ![](http://upload-images.jianshu.io/upload_images/204749-360390c136ebe260.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 50 | 51 | ##KafkaUtils#createDirectStream 52 | 在[揭开Spark Streaming神秘面纱③ - 动态生成 job](http://www.jianshu.com/p/ee845802921e)中,介绍了在生成每个 batch 的过程中,会去取这个 batch 对应的 RDD,若未生成该 RDD,则会取该 RDD 对应的 blocks 数据来生成 RDD,最终会调用到```DStream#compute(validTime: Time)```函数,在```KafkaUtils#createDirectStream```调用中,会新建```DirectKafkaInputDStream```,```DirectKafkaInputDStream#compute(validTime: Time)```会从 kafka 拉取数据并生成 RDD,流程如下: 53 | 54 | 55 | ![](http://upload-images.jianshu.io/upload_images/204749-9d7be6b6c04c700b.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 56 | 57 | 如上图所示,该函数主要做了以下三个事情: 58 | 59 | 1. 确定要接收的 partitions 的 offsetRange,以作为第2步创建的 RDD 的数据来源 60 | 2. 创建 RDD 并执行 count 操作,使 RDD 真实具有数据 61 | 3. 以 streamId、数据条数,offsetRanges 信息初始化 inputInfo 并添加到 JobScheduler 中 62 | 63 | 进一步看 KafkaRDD 的 getPartitions 实现: 64 | 65 | ``` 66 | override def getPartitions: Array[Partition] = { 67 | offsetRanges.zipWithIndex.map { case (o, i) => 68 | val (host, port) = leaders(TopicAndPartition(o.topic, o.partition)) 69 | new KafkaRDDPartition(i, o.topic, o.partition, o.fromOffset, o.untilOffset, host, port) 70 | }.toArray 71 | } 72 | ``` 73 | 74 | 从上面的代码可以很明显看到,KafkaRDD 的 partition 数据与 Kafka topic 的某个 partition 的 o.fromOffset 至 o.untilOffset 数据是相对应的,也就是说 KafkaRDD 的 partition 与 Kafka partition 是一一对应的 75 | 76 | --- 77 | 78 | 通过以上分析,我们可以对这两种方式的区别做一个总结: 79 | 80 | 1. createStream会使用 Receiver;而createDirectStream不会 81 | 2. createStream使用的 Receiver 会分发到某个 executor 上去启动并接受数据;而createDirectStream直接在 driver 上接收数据 82 | 3. createStream使用 Receiver 源源不断的接收数据并把数据交给 ReceiverSupervisor 处理最终存储为 blocks 作为 RDD 的输入,从 kafka 拉取数据与计算消费数据相互独立;而createDirectStream会在每个 batch 拉取数据并就地消费,到下个 batch 再次拉取消费,周而复始,从 kafka 拉取数据与计算消费数据是连续的,没有独立开 83 | 4. createStream中创建的KafkaInputDStream 每个 batch 所对应的 RDD 的 partition 不与 Kafka partition 一一对应;而createDirectStream中创建的 DirectKafkaInputDStream 每个 batch 所对应的 RDD 的 partition 与 Kafka partition 一一对应 84 | 85 | --- 86 | 87 | 欢迎关注我的微信公众号:FunnyBigData 88 | 89 | ![FunnyBigData](http://upload-images.jianshu.io/upload_images/204749-2f217e5d38fc1bcb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 90 | --------------------------------------------------------------------------------