一、序引
当以分布式方式处理数据时,常常需要执行map与reduce转换。由于巨量数据必须从一个节点传输到另外的节点,给集群中的cpu、磁盘、内存造成沉重的负载压力,同时也会给网络带宽带来压力。所以,reduce阶段进行的shuffle过程,往往是性能的瓶颈所在。
shuffle过程涉及数据排序、重分区、网络传输时的序列化与反序列化,为了减少I/O带宽及磁盘I/O操作,还要对数据进行压缩。故而,shuffle的性能将直接spark的整体运算性能。
因为shuffle文件数(M(Map数) * R(Reduce数))将会非常大。故而,这是性能损失的关键所在。折衷方案:压缩输出文件。spark默认的是Snappy,但也支持选择lz4、lzf。
reduce阶段,内存问题异常突出。对于一个reducer而言,被shuffle的所有数据都必须放到内存中。若内存不够,则会因内存溢处而导致job失败。同时,这也是为何reducer数量如此重要的原因。reducer增加时,理应及时减少每个reducer对应的数据量。
Hadoop与Spark的对比:hadoop中,map与reduce阶段存在交叠,mapper把输出数据推送到reducer;spark中,只有map阶段结束,reduce阶段才启动工作,reducer将拉取shuffle后的数据。
在Spark中,引入了shuffle file consolidation机制,尽量输出少一点但大一些的文件。map阶段为每个分区输出一个shuffle文件。shuffle文件数是每个核的reducer数,而不是每个mapper的reducer数。原有运行在相同CPU核上的map任务都将输出相同的shuffle文件,每个reducer都有一个文件。要启用shuffle文件合并,必须把spark.shuffle.consolidateFile=true。
二、Shuffle与数据分区
join、reduceByKey、groupByKey、cogroup等转换(transformation)操作需要在集群中shuffle数据。这些操作可能需要对整个数据集进行shuffle、排序及重新分区,十分消耗性能。“预分区”(pre-partition)的方案,可以有效提高性能。
如果RDD已分区,就能避免数据shuffle。
对于涉及两个或更多RDD的转换操作,数据分区更为重要。需要join操作的RDD越多,要处理的数据就越多,这些数据是shuffle的重要内容。
为进一步改善join转换的性能,你可以用相同的分区器对两个RDD进行分区。如此,不需要通过网络进行shuffle。
总之,shuffle操作异常的消耗性能,当编写Spark应用时,应当减少Shuffle的次数及在集群中传输数据。由于避免了跨节点传输数据引起的CPU、磁盘与网络I/O压力,分区得当的RDD能有效提升应用的性能。
三、算子与shuffle
正确的时机选择正确的算子(operator),可有效避免让大量数据分布到集群各个worker节点。
初学者倾向于用转换来完成job,而不会思考它们背后触发的操作。这是一个极为重要的认识误区。
1、groupByKey vs reduceByKey
groupByKey,特定键(Key)的所有值必须在一个任务中进行处理。为达到这个目的,整个数据集被shuffle,特定键的所有单词对被发送到一个节点。因此,会消耗大量的时间。与此同时,可能还有“一个大数据集,一个键有很多值,一个任务可能会用完所有内存”的爆炸性问题。
reduceByKey算子的函数会被应用到单台机器上一个键的全部值,处理后得到的中间结果随后会在集群中发送。
groupByKey: {
{(A,1),(A,1),(A,1),(B,1)},
{(A,1),(A,1),(B,1),(B,1)},
{(A,1),(B,1),(B,1)}
} →→→ {
{{(A,1),(A,1),(A,1),(A,1),(A,1),(A,1)}→(A,6)}
{{(B,1),(B,1),(B,1),(B,1),(B,1)}→(B,5)}
}
reduceByKey: {
{{(A,1),(A,1),(A,1),(B,1)}→{(A,3),(B,1)}}
{{(A,1),(A,1),(B,1),(B,1)}→{(A,2),(B,2)}},
{{(A,1),(B,1),(B,1)}→{(A,1),(B,2)}}
} →→→ {
{{(A,1),(A,1),(A,1)}→(A,6)}
{{(B,1),(B,1),(B,1)}→(B,5)}
}
从流程图中不难看出,reduceByKey代替groupByKey来解决聚合问题,显著减少了需要压缩及shuffle的数据,性能有了巨大的提升空间。
2、repartition vs coalesce
两种方法,均可以实现“需要改变RDD分区数来改变并行度”的情形。
重分区算子会随机rshuffle数据,并将其分发到许多分区中。可能比RDD的原始分区数多,亦可能少。
coalesce算子会得到同样的结果,但是减少RDD的分区数能避免shuffle。而coalesce并不是总能避免shuffle。如果大幅减少分区数,将它设置成比节点数还少,那么剩余节点上的数据将被送到包含这些分区的其它节点上。在减少分区数时,由于仅对一部分数据而不是对整个数据集进行shuffle,因而coalesce比repartition执行的效果更好。
3、reduceByKey vs aggregateByKey
为了避免reduceByKey方案中的内存分配问题,可用aggregateByKey代替。
必须为aggregateByKey提供三个参数:1)、零值(zero),即聚合的初始值。2)、函数f:(U,V),把值V合并到数据结构U,该函数在分区内合并值时被调用。3)、函数g:(U,U),合并两个数据结构U。在分区间合并值时被调用。
四、Shuffle并不总是坏事
总的来说,避免shuffle是正确选择,但shuffle并不总是坏事。
第一,shuffle是spark在集群里重新组织数据的一种方式,因而是必不可少。
第二,有时,shuffle能节省应用执行的时间。