当spark程序中,存在过多的小任务的时候,可以通过 RDD.coalesce方法,收缩合并分区,减少分区的个数,减小任务调度成本,避免Shuffle导致,比RDD.repartition效率提高不少。
rdd.coalesce方法的作用是创建CoalescedRDD,源码如下:
def coalesce(numPartitions: Int, shuffle: Boolean = false)(implicit ord: Ordering[T] = null)
: RDD[T] = withScope {
if (shuffle) {
/** Distributes elements evenly across output partitions, starting from a random partition. */
val distributePartition = (index: Int, items: Iterator[T]) => {
var position = (new Random(index)).nextInt(numPartitions)
items.map { t =>
// Note that the hash code of the key will just be the key itself. The HashPartitioner
// will mod it with the number of total partitions.
position = position + 1
(position, t)
}
} : Iterator[(Int, T)]
// include a shuffle step so that our upstream tasks are still distributed
new CoalescedRDD(
new ShuffledRDD[Int, T, T](mapPartitionsWithIndex(distributePartition),
new HashPartitioner(numPartitions)),
numPartitions).values
} else {
new CoalescedRDD(this, numPartitions)
}
}
本文只讲默认情况下,不发生Shuffle的时候rdd.coalesce的原理
DAGScheduler进行任务分配的时候,需要调用CoalescedRDD.getPartitions方法,获取CoalescedRDD的分区信息。这个方法的代码如下:
override def getPartitions: Array[Partition] = {
val pc = new PartitionCoalescer(maxPartitions, prev, balanceSlack)
pc.run().zipWithIndex.map {
case (pg, i) =>
val ids = pg.arr.map(_.index).toArray//ids表示CoalescedRDD的某个分区,对应它的parent RDD的所有分区id
new CoalescedRDDPartition(i, prev, ids, pg.prefLoc)
}
}
在上面的方法中,CoalescedRDD一个分区,对应于它parent RDD的那些分区是由PartitionCoalescer数据结构确定的。在这里不详述PartitionCoalescer类的具体实现,只说这个类起到的作用:
1.保证CoalescedRDD的每个分区基本上对应于它Parent RDD分区的个数相同
2.CoalescedRDD的每个分区,尽量跟它的Parent RDD的本地性形同。比如说CoalescedRDD的分区1对应于它的Parent RDD的1到10这10个分区,但是1到7这7个分区在节点1.1.1.1上,那么 CoalescedRDD的分区1所要执行的节点就是1.1.1.1。这么做的目的是为了减少节点间的数据通信,提升处理能力。
3.CoalescedRDD的分区尽量分配到不同的节点执行
4.Be efficient, i.e. O(n) algorithm for n parent partitions (problem is likely NP-hard)(不知道该怎么翻译,只能粘原文了)
CoalescedRDD分区它数据结构表示,它是一个容器,包含了一个的Parent RDD的所有分区。在上面的代码中,创建CoalescedRDDPartition对象的时候,ids参数是一个数组,表示这个CoalescedRDDPartition对应的parent RDD的所有分区id。
CoalescedRDD.compute方法用于生成CoalescedRDD一个分区的数据,源码如下:
override def compute(partition: Partition, context: TaskContext): Iterator[T] = {
partition.asInstanceOf[CoalescedRDDPartition].parents.iterator.flatMap { parentPartition =>
firstParent[T].iterator(parentPartition, context)
}
}
如果CoalescedRDD的一个分区跟它的Parent RDD的分区没有在一个Executor,则需要通过Netty通信的方式,拿到它的Parent RDD的数据,然后再拼接。
采用rdd.coalesce方法修改了分区的个数,虽然由可能需要采用Netty通信的方式获取它的Parent RDD的数据,但是没有落地到磁盘的操作,避免了磁盘I/O,所以比Shuffle还是要快不少。