【背景】
最近看了很多spark shffle相关的东西,脑子嗡嗡地,似懂非懂,所以决定用一个常见例子来把知识点串起来。
假设现在有一个表a是用户表(其中a_non_cn是非cn数据,a_cn是cn数据),b是全地区地址表
准备的sql如下:
select a.name,a.region,b.address from
(select * from a_non_cn
where age>10
union all
select * from a_cn
where age>10
)a
left join b
on a.id=b.id and a.region=b.region
limit 20
这里面包含了where/limit/union/join等,应该算比较普遍了
假设a_non_cn表有90个数据分区,a_cn表有10个数据分区,b表有200个数据分区,表都很大无法做broadcast join,做的是 sort merge join
运行环境:executor是用动态分配,0~100个,每个executor内存是4G、core是1,默认情况下一个task对应cpu的一个核(spark.task.cpus为1),因此同一时刻最大可以并行执行的 Task 数目=100*1=100
【具体过程】
读取a表
一、首先stage0会对a_non_cn和a_cn进行scan、filter、project等常规操作后进行union
一般union后的分区数是两个表之和(不涉及shuffle),即90+10=100
union后,由于这是左边而且最终只取limit 20,所以会增加一个limit算子
大致如下图(stage0)::
注:
- map阶段的task的数量一般是分区的数量
- 同一个stage的task是可以并行的,无依赖关系的不同stage的task可以并行。有依赖关系的不同stage的task不可以并行,必须等上一个依赖的stage完成,下一个stage才可以执行
- 钨丝计划tungsten中WholeStageCodegen的优化会体现在这里,例如ColumnarToRow、Filter、Project、LocalLimit这几个算子会被重写成同一个函数,以减少函数调用
读取b表
二、表b也是同样的操作,但是注意由于表b是left join的右表,所以不会有limit算子
大致如下图(stage1):
sort merge join
三、开始进入a表和b表的sort merge join,可以分成3个过程;
1.shuffle write
a/b表根据会Join keys(id,region)进行Shuffle重分区。
partition算法是可以自定义的,默认的算法是根据keys哈希到不同的bucket中去
显然,我们可以对参与 Join 的表按照 Keys 进行 Bucket 来避免 Shuffle Sort Merge Join 的 Shuffle 操作,因为 Bucket 的表事先已经按照 Keys 进行分区排序,所以做 Shuffle Sort Merge Join 的时候就无需再进行分区和排序了。
a表有100个分区,b表有200个分区,他们是不同的stage而且没有依赖关系,所以可以并行执行。
上面说到我们最大并发task是100,假设a表得到了50个task,b表得到了50个task
采用的是 Sorted-Based Shuffle,因此对于a表会有50个shufflemaptask(executor中的线程)依次执行a表100个分区数据的 计算,得到50*2个文件(数据文件和索引文件),数据文件中先按partition排序,再按本身数据的keys排序(在这里就是按id、region字段) 。
同理,50个shufflemaptask依次执行b表200个分片数据的 计算,得到50*2个文件(数据文件和索引文件),数据文件中先按partition排序,再按本身数据的keys排序(在这里就是按id、region字段) 。
我们在web ui中往往看到task数目比分区数大很多,原因是这些task不一定是同时执行的,而且有些task可能是推测执行,会有重复
这些文件的产生,是通过executor本地的BlockManager,同时会上报消息到driver上的BlockManagerMaster,因此在所有的map task执行完毕后,Driver中就掌握了所有的磁盘小文件的地址。
在BlockManager中,根据blockId来进行区分。BlockId不仅是文件名,更是后续BlockManager定位文件的索引,以当前task的shuffle id,分区id(partition id)以及reduceid构造了BlockId。
具体定义可以org.apache.spark.network.shuffle.ExternalShuffleBlockResolver#getBlockData().
注意点:shuffle结果是写到本地存储而不是内存,避免reduce时拉取数据出现异常
2.shuffle read
一定是先完成 Mapper 端所有的 Tasks,才会进行 Reducer 端的 Shuffle 过程。
在reduce task执行之前,会通过本Executor中MapOutPutTrackerWorker向Driver端的MapOutputTrackerMaster获取磁盘小文件的地址(请求参数:会将当前rdd的shuffle id和分区号,组装成blockId,从对应的BlockManager进行拉取)。
获取到磁盘小文件的地址后,如果是非本地executor数据,则会通过BlockManager中的ConnectionManager连接数据所在executor节点上的ConnectionManager,然后通过BlockTransferService去数据所在executor上拉取数据,根据申请的blockid寻到对应的本地文件以流的形式返回。
如果是本地executor的数据,那么本地的BlockManager通过fetchLocalBlocks()方法根据BlockId尝试拉取数据。因为是本地,直接可以通过上述的getFiles()方法获取文件,不需要BlockManager之间的网络通信即可获取对应的数据。
Reduce task过来的数据首先会放在 Reducer 端的内存缓存区+ 磁盘 (数据结构类使用了 ExternalAppendOnlyMap)
这一步后,a表和b表按照keys(id,region)相同的数据被拉到同一个节点上
注意:在 Reduce 端是没有进行排序(而MapReduce的map和reduce都会排序,排序是MapReduce的灵魂),所以结果默认不是有序的。
3.merge
对来自不同表的排序好的分区数据进行JOIN
即分别遍历两个有序序列,碰到相同join key就merge输出,否则取更小一边,网上找了个示意图:
整个流程可以总结如下图: