spark sortShuffleWriter源码学习
查看的源码为spark2.3

调用ShuffleMapTask的runTask方法
org.apache.spark.scheduler.ShuffleMapTask#runTask
ShuffleMapTask继承了org.apache.spark.scheduler.Task,重写了Task的runTask方法,在该方法中关于shuffle部分主要是获取shuffleManager,然后得到sortShuffleManager,然后再通过manager获取writer,得到sortShuffleWriter,然后调用writer方法
override def runTask(context: TaskContext): MapStatus = {
// Deserialize the RDD using the broadcast variable.
val threadMXBean = ManagementFactory.getThreadMXBean
val deserializeStartTime = System.currentTimeMillis()
val deserializeStartCpuTime = if (threadMXBean.isCurrentThreadCpuTimeSupported) {
threadMXBean.getCurrentThreadCpuTime
} else 0L
val ser = SparkEnv.get.closureSerializer.newInstance()
val (rdd, dep) = ser.deserialize[(RDD[_], ShuffleDependency[_, _, _])](
ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)
_executorDeserializeTime = System.currentTimeMillis() - deserializeStartTime
_executorDeserializeCpuTime = if (threadMXBean.isCurrentThreadCpuTimeSupported) {
threadMXBean.getCurrentThreadCpuTime - deserializeStartCpuTime
} else 0L
//定义writer对象
var writer: ShuffleWriter[Any, Any] = null
try {
//获取shuffleManager
val manager = SparkEnv.get.shuffleManager
//通过shuffleManager获取Writer对象,这里的partitionId传入的其实是mapId,每个map有个mapId
writer = manager.getWriter[Any, Any](dep.shuffleHandle, partitionId, context)
//调用write方法。write方法如下
writer.write(rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]])
writer.stop(success = true).get
} catch {
case e: Exception =>
try {
if (writer != null) {
writer.stop(success = false)
}
} catch {
case e: Exception =>
log.debug("Could not stop writer", e)
}
throw e
}
}
调用SortShuffleWriter的write方法
org.apache.spark.shuffle.sort.SortShuffleWriter#write
SortShuffleWriter继承了org.apache.spark.shuffle.ShuffleWriter并重写了其write方法
/** Write a bunch of records to this task's output */
override def write(records: Iterator[Product2[K, V]]): Unit = {
//根据是否存在map端聚合获取ExternalSorter对象(sorter)
sorter = if (dep.mapSideCombine) {
require(dep.aggregator.isDefined, "Map-side combine without Aggregator specified!")
new ExternalSorter[K, V, C](
context, dep.aggregator, Some(dep.partitioner), dep.keyOrdering, dep.serializer)
} else {
// In this case we pass neither an aggregator nor an ordering to the sorter, because we don't
// care whether the keys get sorted in each partition; that will be done on the reduce side
// if the operation being run is sortByKey.如果没有map-side聚合,那么创建sorter对象时候,aggregator和ordering将不传入对应的值
new ExternalSorter[K, V, V](
context, aggregator = None, Some(dep.partitioner), ordering = None, dep.serializer)
}
//通过insertAll方法先写数据到buffer
sorter.insertAll(records) // Don't bother including the time to open the merged output file in the shuffle write time,
// because it just opens a single file, so is typically too fast to measure accurately
// (see SPARK-3570). //通过blockManager获取对应mapId.shuffleId的文件输出路径
val output = shuffleBlockResolver.getDataFile(dep.shuffleId, mapId)
//返回与“path”位于同一目录中的临时文件的路径。
val tmp = Utils.tempFileWith(output)
try {
val blockId = ShuffleBlockId(dep.shuffleId, mapId, IndexShuffleBlockResolver.NOOP_REDUCE_ID)
//将所有的数据合并到一个文件中
val partitionLengths = sorter.writePartitionedFile(blockId, tmp)
//生成index文件,也就是每个reduce通过该index文件得知它哪些是属于它的数据
shuffleBlockResolver.writeIndexFileAndCommit(dep.shuffleId, mapId, partitionLengths, tmp)
mapStatus = MapStatus(blockManager.shuffleServerId, partitionLengths)
} finally {
if (tmp.exists() && !tmp.delete()) {
logError(s"Error while deleting temp file ${tmp.getAbsolutePath}")
}
}
}
ExternalSorter类
创建ExternalSorter对象时,各参数对应的意思。
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) aggregator:在RDD shuffle时,map/reduce-side使用的aggregator
partitioner:对shuffle的输出,使用哪种partitioner对数据做分区,比如hashPartitioner或者rangePartitioner
ordering:根据哪个key做排序
serializer:使用哪种序列化,如果没有显示指定,默认使用spark.serializer参数值
从一个high level的角度看ExternalSorter到底做了什么?
第一:反复的将数据填充到内存buffer中(如果需要通过key做map-side聚合,则使用PartitionedAppendOnlyMap;如果不需要,则使用PartitionedPairBuffer),如下
// Data structures to store in-memory objects before we spill. Depending on whether we have an
// Aggregator set, we either put objects into an AppendOnlyMap where we combine them, or we
// store them in an array buffer.
@volatile private var map = new PartitionedAppendOnlyMap[K, C]
@volatile private var buffer = new PartitionedPairBuffer[K, C]
第二:在buffer中,通过key计算partition ID,通过partition ID对数据进行排序(partition ID可以理解为reduce ID,意思就是数据被分给了哪个reduce),为了避免对key调用多次partitioner,spark会将partition ID跟每一条数据一起存储。
第三:当buffer达到内存限制时(buffer默认大小32k,由spark.shuffle.file.buffer参数决定),会将buffer中的数据spill到文件中(每次spill都会生成一个文件),如果我们需要做map-side聚合,该文件生成时会通过partition ID先做排序,然后通过key或者key的hashcode值做二次排序。
第四:将spill形成的多个文件合并包括还在内存中的数据,文件合并时候将会排序,排序方式跟上面一样,生成数据文件dataFile以及索引文件indexFile
第五:最后调用stop方法,删除所有中间文件
结合下图更好理解

mapTask通过externalSorter生成多个文件,也就是fileSegment,最后每个map任务的所有filesegment将会合并成一个file

上图数据插入的是appendOnlyMap,也就是使用了map-side聚合,所以有merger value,appendOnlyMap在满了以后(默认32k)将spill成文件,多次spill生成多个文件,最后merge所有文件包括还在内存buffer中的数据。
调用ExternalSorter的insertAll方法
这一步主要是往buffer写数据,对数据分partition ID,buffer满了spill数据到磁盘且对数据排序
def insertAll(records: Iterator[Product2[K, V]]): Unit = {
// TODO: stop combining if we find that the reduction factor isn't high如果合并比例不高的话,就不会继续合并了
// 通过创建ExternalSorter对象时传入的aggregator获取是否存在合并
val shouldCombine = aggregator.isDefined
if (shouldCombine) {
// Combine values in-memory first using our AppendOnlyMap
val mergeValue = aggregator.get.mergeValue
val createCombiner = aggregator.get.createCombiner
var kv: Product2[K, V] = null
val update = (hadValue: Boolean, oldValue: C) => {
//合并值方式
if (hadValue) mergeValue(oldValue, kv._2) else createCombiner(kv._2)
}
while (records.hasNext) {
addElementsRead()
kv = records.next()
//这个map就是该类中定义的PartitionedAppendOnlyMap,getPartition方法通过key获取所属Partition ID(hashPartitioner)
map.changeValue((getPartition(kv._1), kv._1), update)
// buffer满的话将内存中的数据spill成文件
maybeSpillCollection(usingMap = true)
}
} else {
// Stick values into our buffer
while (records.hasNext) {
addElementsRead()
val kv = records.next()
//这个buffer就是该类中定义的PartitionedPairBuffer
buffer.insert(getPartition(kv._1), kv._1, kv._2.asInstanceOf[C])
maybeSpillCollection(usingMap = false)
}
}
}
insertAll方法中调用maybeSpillCollection方法
/**
* Spill the current in-memory collection to disk if needed.
*
* @param usingMap whether we're using a map or buffer as our current in-memory collection
* 不同的数据结构(也就是buffer)调用不同的方法
*/
private def maybeSpillCollection(usingMap: Boolean): Unit = {
var estimatedSize = 0L
if (usingMap) {
estimatedSize = map.estimateSize()
//maybeSpill方法会尝试申请buffer内存,如果申请到内存,则spill且返回false。否则true
if (maybeSpill(map, estimatedSize)) {
//appendOnlyMap的数据spill以后,创建一个新的appendOnlyMap
map = new PartitionedAppendOnlyMap[K, C]
}
} else {
estimatedSize = buffer.estimateSize()
if (maybeSpill(buffer, estimatedSize)) {
buffer = new PartitionedPairBuffer[K, C]
}
} if (estimatedSize > _peakMemoryUsedBytes) {
_peakMemoryUsedBytes = estimatedSize
}
}
maybeSpillCollection方法中调用maybeSpill方法,判断是否应该执行spill
/**
* Spills the current in-memory collection to disk if needed. Attempts to acquire more
* memory before spilling.
* 在spill之前会尝试申请内存,最后才判断是否真正执行spill
* @param collection collection to spill to disk
* @param currentMemory estimated size of the collection in bytes
* @return true if `collection` was spilled to disk; false otherwise
*/
protected def maybeSpill(collection: C, currentMemory: Long): Boolean = {
var shouldSpill = false
if (elementsRead % 32 == 0 && currentMemory >= myMemoryThreshold) {
// Claim up to double our current memory from the shuffle memory pool;从上次spill以后,每读取32个元素判断一次,声明申请额外内存
val amountToRequest = 2 * currentMemory - myMemoryThreshold
val granted = acquireMemory(amountToRequest)
myMemoryThreshold += granted
// If we were granted too little memory to grow further (either tryToAcquire returned 0,
// or we already had more memory than myMemoryThreshold), spill the current collection
shouldSpill = currentMemory >= myMemoryThreshold
}
shouldSpill = shouldSpill || _elementsRead > numElementsForceSpillThreshold
// Actually spill
if (shouldSpill) {
_spillCount += 1
logSpillage(currentMemory)
spill(collection)
_elementsRead = 0
_memoryBytesSpilled += currentMemory
releaseMemory()
}
shouldSpill
}

1
spark sortShuffleWriter源码学习的更多相关文章
- Spark源码学习1.1——DAGScheduler.scala
本文以Spark1.1.0版本为基础. 经过前一段时间的学习,基本上能够对Spark的工作流程有一个了解,但是具体的细节还是需要阅读源码,而且后续的科研过程中也肯定要修改源码的,所以最近开始Spark ...
- 【Spark2.0源码学习】-1.概述
Spark作为当前主流的分布式计算框架,其高效性.通用性.易用性使其得到广泛的关注,本系列博客不会介绍其原理.安装与使用相关知识,将会从源码角度进行深度分析,理解其背后的设计精髓,以便后续 ...
- 使用 IntelliJ IDEA 导入 Spark 最新源码及编译 Spark 源代码(博主强烈推荐)
前言 其实啊,无论你是初学者还是具备了有一定spark编程经验,都需要对spark源码足够重视起来. 本人,肺腑之己见,想要成为大数据的大牛和顶尖专家,多结合源码和操练编程. 准备工作 1.sca ...
- 【Spark SQL 源码分析系列文章】
从决定写Spark SQL源码分析的文章,到现在一个月的时间里,陆陆续续差不多快完成了,这里也做一个整合和索引,方便大家阅读,这里给出阅读顺序 :) 第一篇 Spark SQL源码分析之核心流程 第二 ...
- Spark SQL源码解析(二)Antlr4解析Sql并生成树
Spark SQL原理解析前言: Spark SQL源码剖析(一)SQL解析框架Catalyst流程概述 这一次要开始真正介绍Spark解析SQL的流程,首先是从Sql Parse阶段开始,简单点说, ...
- Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结
2017年的秋招彻底结束了,感觉Java上面的最常见的集合相关的问题就是hash--系列和一些常用并发集合和队列,堆等结合算法一起考察,不完全统计,本人经历:先后百度.唯品会.58同城.新浪微博.趣分 ...
- jQuery源码学习感想
还记得去年(2015)九月份的时候,作为一个大四的学生去参加美团霸面,结果被美团技术总监教育了一番,那次问了我很多jQuery源码的知识点,以前虽然喜欢研究框架,但水平还不足够来研究jQuery源码, ...
- MVC系列——MVC源码学习:打造自己的MVC框架(四:了解神奇的视图引擎)
前言:通过之前的三篇介绍,我们基本上完成了从请求发出到路由匹配.再到控制器的激活,再到Action的执行这些个过程.今天还是趁热打铁,将我们的View也来完善下,也让整个系列相对完整,博主不希望烂尾. ...
- MVC系列——MVC源码学习:打造自己的MVC框架(三:自定义路由规则)
前言:上篇介绍了下自己的MVC框架前两个版本,经过两天的整理,版本三基本已经完成,今天还是发出来供大家参考和学习.虽然微软的Routing功能已经非常强大,完全没有必要再“重复造轮子”了,但博主还是觉 ...
随机推荐
- java垃圾回收学习
经过一个晚上的努力终于完成了一个文件替换指定字符串的程序,但是由于我要替换的全站程序html文件太多, 所以eclipse下边老是在一个目录结束后报出java.lang.OutOfMemoryErro ...
- 从系统引导菜单禁用Hyper-V
1, 从当前引导运行的系统复制一个新引导菜单项: bcdedit /copy {current} /d "Win10 NO_HV" 这时候cmd会输出新引导项的 guid, 复制出 ...
- Windows xp Diskpart合并分区的方法
非常不错的合并分区的方法,经测试,好用,就是对于稳定性就不知道了,理论下应该没什么问题,对于个人电脑合并分区和服务器分区合并来说,无疑是一个非常好的办法 分区增容就是当一个分区的空间不能满足使 ...
- 思科 ASA 系列防火墙 官方文档下载指南
思科 ASA 系列命令参考 思科 ASA 系列命令参考,A 至 H 命令 思科 ASA 系列命令参考, I 至 R 命令 思科 ASA 系列命令参考,S 命令 思科 ASA 系列命令参考, ASASM ...
- python导入第三方库
2.Python的库一般包含两个方面:第三方库和标准库 3.Python的time标准库主要包含三个方面的内容:(1)时间处理函数(2)时间格式化(3)程序计时 4.turtle画笔运动函数的功能是进 ...
- redis之Set(无序)类型常用方法总结
redis之Set(无序)类型常用方法总结 存--sadd key member [member ...] 取--SMEMBERS key sadd key member [member ...] 向 ...
- 三 传递包装pojo&ResultMap
传递包装pojo: 将实体类对象封装在QueryVo类中进行操作 mapper接口: mapper映射文件: 测试: ResultMap的使用: type:映射成的pojo类型 id:resultMa ...
- python join 和setDaemon 简介
Python多线程编程时,经常会用到join()和setDaemon()方法 1.join ()方法:主线程A中,创建了子线程B,并且在主线程A中调用了B.join(),那么,主线程A会在调用的地方等 ...
- Python学习第九课——匿名函数
匿名函数 # 匿名函数 func = lambda x: x + 1 # x表示参数 x+1表示处理逻辑 print(func(10)) # 输出结果为11 # 例:如何将name="han ...
- java课堂第一次随机测试和课件课后动手动脑问题解决(2019-9-16 )
一.课堂测试 1.课堂测试:花二十分钟写一个能自动生成30道小学四则运算题目的 “软件” 要求 (1)减法结果不能为负数 (2)乘法结果不得超过一百,除法结果必须为整数 (3)题目避免重复: (4)可 ...