1、问题背景
总所周知,spark的rdd编程中有两个算子repartition和coalesce。公开的资料上定义为,两者都是对spark分区数进行调整的算子。
repartition会经过shuffle,其实际上就是调用的coalesce(shuffle=true)。
coalesce,默认shuffle=false,不会经过shuffle。
当前仅针对coalesce算子考虑,我们看一下官方的定义:
大概意思为:如果你想要从1000个分区到100个分区,并且不经过shuffle,近乎平均分配10个父分区到1个子分区。
首先我说下我个人简单理解:不经过shuffle,就意味着coalesce算子前后都是在一个stage中的。从该stage开始到coalesce算子之前的任务的迭代执行的并行度都是1000,从coalesce算子开始到该stage结束的任务的迭代执行的并行度都是100。
2、问题及复现
但是在实际使用过程中发现,对于在一个stage中的多个算子,如果先进行map、之后再coalesce(1),该stage的输出的并行度确实为1,但是coalesce之前的map算子的并行度也被缩减为1。这样看的话,似乎和官方的定义有些冲突。
因此简单对此问题进行了复现:
按照我的理解,应该有两个stage,并行读的变化流程应该为
textFlie(N) → repartition(4) → map(4) → coalesce(2) → count(2)
但是实际为,确实有两个stage,但是第二个stage的task数量始终为2,并没有中间4这个过程。
此外对于spark的并行度,具体的实现即是TaskRunner线程。通过VisualVM监控,第二个stage也始终只有2个TaskRunner线程。
3、问题分析
了解过spark的调度过程的都应该清楚。
首先application根据行动算子生成job,
job根据宽窄依赖切分stage,
stage根据最后一个rdd的分区数决定stage的并行度,即此stage中task的数量。
这么看的话,当前stage的最后一个rdd的并行度应该是coalesce之后的并行度,进而整个stage的task数量也就是缩减之后的了。
这么分析的话和实际执行的效果是一致的,但是coalesce官方定义的效果并不一致,coalesce并没有缩减分区,而是强制将整个stage的并行度都降低了。
4、coalesce源码分析
从coalesce源码看,shuffle=false的情况下,只是对现有rdd重新装饰为了一个新的CoalescedRDD。但是该Rdd的Partition有所不同,其实现为CoalescedRDDPartition类,内部维护了一个多对一的映射关系(看似是存在分区合并)。
5、调度源码分析
所谓spark的并行度就是TaskRunner线程的数量,根据源码分析,task线程的数量取决于触发宽依赖或者触发行动算子的rdd的并行度,对于coalesce问题来讲,就是coalesce算子之后的并行度,这与我的猜想也相符。
源码追踪暂时略过,后续在加。
6、总结
首先可以确定的是,executor实际启动的TaskRunner线程数肯定是coalesce算子缩减之后的并行度。
但是为什么CoalescedRDD还有所有不同之处呢?以我上面测试的例子为例。我猜想CoalescedRDD所在的Stage的上一个Stage再Shuffle Write的时候是按照coalesce算子之前的并行度4输出的。而Shuffle Read读取时虽然还是只有2个线程,但是仍然读取4个分区(4个分区两两顺序排列,而不是完全并行的),然后到coalesce算子时再合并为2个分区的。见下图
所谓spark的分布式计算,其原理就是再多机、多进程、多线程上采用分治的思想并行的进行计算。而上例中虽然时4个分区,但是是两两顺序排列的,因此并不是完全意义的并行性4。由此可简见和源码的解释稍有歧义。实际计算性能也会有很大差异。