前言
本文隶属于专栏《1000个问题搞定大数据技术体系》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!
本专栏目录结构和参考文献请见1000个问题搞定大数据技术体系
正文
如何合理划分 Stage ,并确定 Task 的类型和个数?
一个直观的想法是将前后关联的 RDDs 组成一个 Stage ,每个 Stage 生成一个 Task 。
这样虽然可以解决问题,但效率不高。
除了效率问题,这个想法还有一个更严重的问题:大量中间数据需要存储。
对于 task 来说,其执行结果要么存到磁盘,要么存到内存,或者两者皆有。
如果每个箭头都是 Task ,每个 RDD 里面的数据都需要存起来,占用空间可想而知。
在每个 RDD 中,每个 Partition 是独立的。
也就是说,在 RDD 内部,每个 Partition 的数据依赖各自不会相互干扰。
因此,一个大胆的想法是将整个流程图看成一个 Stage ,为最后一个 FINAL RDD 中的每个 Partition 分配一个 Task。
最大化 Pipeline
Spark 算法构造和物理执行时最基本的核心:最大化 Pipeline 。
基于 Pipeline 的思想,数据被使用的时候才开始计算,从数据流动的视角来说,是数据流动到计算的位置。
实质上,从逻辑的角度看,是算子在数据上流动。
从算法构建的角度而言,肯定是算子作用于数据,所以是算子在数据上流动,从而方便算法的构建。
从物理执行的角度而言,是数据流动到计算的位置,方便系统最为高效地运行。
可以结合我的这篇博客来理解——为什么说数据不动代码动?移动计算比移动数据更划算?
DAGScheduler 划分 Stage 的原理
对于 Pipeline 而言,数据计算的位置就是每个 Stage 中最后的 RDD ,每个 Stage 中除了最后一个 RDD 算子是真实的外,前面的算子都是假的。
计算的 Lazy 特性导致计算从后往前回溯,形成 Computing Chain ,导致的结果是需要首先计算出具体一个 Stage 内部左侧的 RDD 中本次计算依赖的 Partition。
对应的源码即:先调用 getDependencies 再 调用 getPartitions
整个 Computing Chain 根据数据依赖关系自后向前建立,遇到 Shuffle dependency 后形成 Stage 。
在每个 Stage 中,每个 RDD 中的 compute 调用 parentRDD.iterator 将 parentRDDs 中的 records 一个个 fetch 过来。
总结
Spark 在分布式环境下将数据分区,然后将作业转化为 DAG ,并分阶段进行 DAG 的调度和任务的分布式并行处理。
DAG 将调度提交给 DAGScheduler , DAGScheduler 调度时会根据是否需要经过 Shuffle 过程将 Job 划分为多个 Stage 。