private[spark] class ExternalSorter[K, V, C]( context: TaskContext, aggregator: Option[Aggregator[K, V, C]] = None, partitioner: Option[Partitioner] = None, ordering: Option[Ordering[K]] = None, serializer: Serializer = SparkEnv.get.serializer)extends Spillable[WritablePartitionedPairCollection[K, C]](context.taskMemoryManager())
参数介绍:-
aggregator用来完成聚合操作。
-
partitioner就是shuffle的算子的分区器。也是一个maptask,写数据输出给哪个reducer,由该分区器决定。
-
ordering排序器,可选,对key进行排序。
-
serializer用来在写入数据到磁盘的时候对数据进行序列化,读数据的时候要用他进行反序列化。
-
实例化一个ExternalSorter。
-
调用insertAll(),并传入records数据集。
-
触发排序及合并。可以使用iterator()去对元素进行迭代排序或聚合。也可以调用writePartitionedFile()函数,创建已经排序或者聚合的文件,该文件适用于spark sort shuffle。
val size = 400
val sparkConf = new SparkConf()sparkConf.setMaster("local")sparkConf.setAppName(this.getClass.getCanonicalName)
sparkConf.set("spark.shuffle.manager", "sort")sparkConf.set("spark.shuffle.spill.numElementsForceSpillThreshold", (size / 40).toString)
val sc = new SparkContext(sparkConf)
val context = SparkUtils.fakeTaskContext(sc)
val agg = new Aggregator[Int, Int, Int](i => i, (i, j) => i + j, (i, j) => i + j)val ord = implicitly[Ordering[Int]]
// // Both aggregator and orderingval sorter = new ExternalSorter[Int, Int, Int]( context, Some(agg), Some(new HashPartitioner(4)),Some(ord))
上面agg和ord是聚合器和排序器,两者均可以自定义,也可以设置为None,浪尖这里给了最简单的案例:key,value及聚合后的结果都是Int类型。3.2 插入数据浪尖这里是400条数据,key的范围是0-39,value范围是0-399.sorter.partitionedIterator.map(p => (p._1, p._2)).filter(p=> p._1 == 0).flatMap(p=>p._2).foreach(println)浪尖这里是获取了partition ID的为0的数据,并输出,结果如下:
sparkConf.set("spark.shuffle.spill.numElementsForceSpillThreshold", (size / 40).toString)浪尖这里测试方便,达到10条就会触发刷磁盘,临时文件会在调用sorter.stop()之后删除。要想看是否有中间文件,操作方法也很简单,spark的blockmanager提供了接口:
val beforeCleanUp = SparkUtils.getBlockManager(sc).diskBlockManager.getAllFiles().sizeprintln(beforeCleanUp)sorter.stop()val afterCleanUp = SparkUtils.getBlockManager(sc).diskBlockManager.getAllFiles().sizeprintln(afterCleanUp)结果如下:
sorter.partitionedIterator.map(p => (p._1, p._2)).filter(p=> p._1 == 0).flatMap(p=>p._2).foreach(println)
val outputFile = File.createTempFile("test-unsafe-row-serializer-spill", "",new File("data/"))
// outputFile.deleteOnExit()sorter.writePartitionedFile(ShuffleBlockId(0,0,0),outputFile).foreach(println)
执行之后会在工程的data目录下生成文件,文件是unsaferow及序列化的,不可以直接查看。3.5 读取溢写文件sorter的writePartitionedFile方法,返回值是一个数组,数组的下标是 partition ID,元素是该分区数据的大小。读数据的时候由于sorter会将所有的分区数据写入同一个数据文件,其实spark shuffle里还有一个索引文件,浪尖这里是测试用的所有没有索引文件。val serializer = SparkEnv.get.serializer.newInstance()val input = new FileInputStream(new File("data/test-unsafe-row-serializer-spill7127803973846287207"))input.skip(271+271+271)val deserializer = serializer.deserializeStream(input)try { val rows = deserializer.asKeyValueIterator while (rows.hasNext) { val (key,value)=rows.next(); println(key+":"+value) }} catch { case ex: Exception => { ex.printStackTrace() // 打印到标准err System.err.println("exception===>: ...") // 打印到标准err }}
结果,第一个元素是key,第二个是聚合后的value,可以看到分区特点也很均匀key差值是4,由于排序的原因,所以key也是递增的,这是由于浪尖这个给的hashpartitioner分区数为4,且给了排序器的原因。3:18307:187011:191015:195019:199023:203027:207031:211035:215039:2190浪尖这里读取分区文件的时候由于分区segment之间有分隔符,所以会抛异常,而中止,这正好是给我们结束契机。4. 代码补充自己的类要包路径是org.apache.spark.文章提到的工具类是:
package org.apache.spark5.总结这个思路主要来源于知识星球之前有人问过浪尖,数据集比较大,写分布式spark程序集成到自己的任务里有比较麻烦,所以想问问浪尖有没有好思路。浪尖想自己实现基于磁盘的排序算法,实际上重复造轮子太复杂了,而且性能不知如何,所以想到利用spark shuffle的基于磁盘的排序操作,把它拿出来,然后使用起来。其实想给大家的提醒是:学一个框架源码,不要只停留在阅读理解,要分析总结化为己用,这样理解才会比较深入,成长才会比较大。
import java.util.Properties
import org.apache.spark.memory.TaskMemoryManagerimport org.apache.spark.storage.BlockManager
object SparkUtils { def fakeTaskContext(sc: SparkContext): TaskContext = { val env = sc.env val taskMemoryManager = new TaskMemoryManager(env.memoryManager, 0) new TaskContextImpl( stageId = 0, stageAttemptNumber = 0, partitionId = 0, taskAttemptId = 0, attemptNumber = 0, taskMemoryManager = taskMemoryManager, localProperties = new Properties, metricsSystem = env.metricsSystem) }
def getBlockManager(sc:SparkContext):BlockManager = { sc.env.blockManager }}