前言
呵呵 想以 Spark 的 WordCount 来梳理一下 流程已经很久了
从最开始的 在学校的源码调试, 记录了一系列的 文档
到后来 再次重新 调试了一下这一系列的东西, 呵呵 还是需要整理下来, 呵呵 中间应该梳理了几次吧, 只是 现在懒得去找了
呵呵 本文将以 WordCount, 来调试, 了解 着整个流程
我本地没有 集群, 因此是直接使用的 local 模式来进行调试
以下调试基于 jdk1.8.0_211 + scala2.11.12 + spark-2.4.5
测试代码
package com.hx.test
import org.apache.spark.{SparkConf, SparkContext}
/**
* Test01WordCount
*
* @author Jerry.X.He <970655147@qq.com>
* @version 1.0
* @date 2020-04-12 11:00
*/
object Test01WordCount {
// Test01WordCount
def main(args : Array[String]): Unit = {
val logFile = "resources/Test01WordCount.txt"
val conf = new SparkConf().setAppName("Test01WordCount").setMaster("local[1]")
val sc = new SparkContext(conf)
val result = sc
.textFile(logFile,2)
.flatMap(line => line.split(" "))
.map(word => (word, 1))
.reduceByKey((left, right) => left + right)
result
.collect()
.foreach(entry => println(entry._1, entry._2))
sc.stop()
}
}
Test01WordCount.txt 里面的内容如下
234 345 123 346 234
123 124
执行结果如下
然后我们再来看下各个 函数执行之后的结果, 为了体现结果, 使用了 collect 来触发 action, 提交 job
sc.textFile(logFile,2)
sc.textFile(logFile,2).flatMap(line => line.split(" "))
sc.textFile(logFile, 2).flatMap(line => line.split(" ")).map(word => (word, 1))
sc.textFile(logFile, 2).flatMap(line => line.split(" ")).map(word => (word, 1)).reduceByKey((left, right) => left + right)
然后我们查看一下 spark UI, 查看任务的情况
可以看到, 提交了一个 job, 里面包含了两个 Stage, rdd 的分区数是 2, 因此创建了 四个 task
我们再看一下这个 job 的情况
这个 job 根据 ShuffleDependency 划分了两个 Stage, rdd 的分区数是 2, 因此创建了 四个 task
stage0 包含了textFile(logFile, 2).flatMap(line => line.split(" ")).map(word => (word, 1))
stage1 包含了 reduceByKey((left, right) => left + right)
我们看一下 stage0 的情况
两个 partition 对应了两个 ShuffleMapTask
task0 输入为 27 byte, 输出为 112 byte, 输入记录数为 1, 输出记录数为 4
task1 输入为 14 byte, 输出为 98 byte, 输入记录数为 1, 输出记录数为 2
其他的大多数是一些 时间统计, 汇总统计, 百分比统计
我们再来看一下 stage1 的情况
两个 partition 对应了两个 ResultTask
task0 输入为 105 byte, 输入记录数为 3, 输出为 result.collect 对应的结果(我们上面看到的 Array[Tuple2])的一部分
task1 输入为 105 byte, 输入记录数为 3, 输出为 result.collect 对应的结果(我们上面看到的 Array[Tuple2])的一部分
其他的大多数是一些 时间统计, 汇总统计, 百分比统计
本文的目的就是 期望梳理出 WordCount 的执行流程, 以及上面的这些数据统计对应于这个场景的意义
当然 本文设定 您对 spark 有一定的了解, 因此 一些 基础的东西 不会过于详细的表述
Driver 提交 Job
runjob 的参数1
从 result.collect 开始
这里提交 job 传入了两个东西, 一个是需要计算的 rdd
另外的一个是一个函数, 表示的是 当前 job 对应的 ResultTask 主要干什么事情, 比如这里的 两个 partition, 最后 parition0 计算的结果是 [(345, 1), (123,2)], parition1的计算结果为 [(234,2), (346,1), (124,1)]
这里传入的 func 是吧 partition 中的内容转换为数组, 也就是上面看到的 results[0], results[1](Tuple2的数组)
在用外面的 Array.concat 处理一层, 其意义就是 获取 rdd 中的每个分区的数据元素(func), 然后 在汇总一下(Array.concat), 得到一个 rdd 中的所有的元素的一个 Array
又比如我们看 rdd.count 来提交 job, func 传入的是 Utils.getIteratorSize _(获取迭代器中元素的数量)
所以 我们这里得到的 result[0], result[1] 分别为 2, 3 代表的是 paritiion0, partition1 中的元素的数量, 合计起来就是 rdd 中的元素的数量
runjob 的参数2
这里传入了一个 “(index, res) => results(index) = res”, 指代的是 ResultTask 计算了结果之后, 对于结果 怎么处理
比如这里 runJob 里面的就是, 把结果放入 results 中
parittion0 计算的结果为 [(345, 1), (123,2)], 把他放入 result[0] 中
parittion1 计算的结果为 [(234,2), (346,1), (124,1)] 把他放入 result[1] 中
这的 partitions 是需要计算的 paritition 的列表, 默认为当前 rdd 的所有的 paritition
submitJob 之后
然后 进行 submitJob(创建 Job, 划分Stage, 提交Job, 创建Task, TaskScheduler进行调度, SchedulerBackend进行调度, 发送任务到Executor执行), 之后等待任务 在"集群"中执行完成
执行完成之后 进行回调的处理, 还有这里的 打印日志信息 等等
划分 Stage - finalStage
可以看出 这个 finalStage 需要计算的 rdd 为一个 ShuffleRDD, 并且依赖于一个 ShuffleMapStage
我们再来看一下这个 ShuffleMapStage 的情况
需要计算的 rdd 为 一个 MapPartitionsRDD(map), 这个 rdd 的计算依赖于一个 MapPartitionsRDD(flatMap), 这个 rdd 的计算又依赖于一个 MapPartitionsRDD(map), 这个 rdd 的计算依赖于一个 HadoopRDD(textFile)
划分 Stage 的细节, 我们这里暂时不详细介绍, 只需要了解, 根据 ShuffleDependency 来划分 Stage 就行
发现这些 RDD 的关系
val result = sc
.textFile(logFile,2)
.flatMap(line => line.split(" "))
.map(word => (word, 1))
.reduceByKey((left, right) => left + right)
sc.textFile
rdd.map/flatMap
rdd.reduceByKey
看了这么一圈, 上面的 stage 中的各个 rdd 的关系应该就比较清楚了
提交 Stage
如果还有没有执行的 依赖的 Stage 先提交依赖的 Stage, 如果依赖的 Stage 均已经完成, 提交当前 Stage
在这里, 因为 ResultStage1 依赖于 ShuffleMapStage0, 并且 ShuffleMapStage0 还未执行, 因此 先执行 ShuffleMapStage0, ShuffleMapStage0 完成之后 在执行 ResultStage1
ShuffleMapStage0 执行完成之后, 通知 ResultStage1 的执行
提交 Task
根据 Stage 创建 Task, 多少个需要计算的 partition 就会创建多少个 Task
ShuffleMapStage 创建的是 ShuffleMapTask
然后 将创建的 task 列表封装成 TaskSet 提交给 TaskScheduler
ResultStage 创建的是 ResultTask
TaskScheduler 处理任务
封装 TaskSetManager, 注册信息, 然后通知 backend 活儿来了
我们这里 暂时只关注 LocalSchedulerBackend
获取可用的 Executor(offer) 的信息, 然后经过 taskScheduler 调度获取任务, 然后 将任务发送到 Executor 执行
"集群"执行任务
Executor 执行任务
将 Task 反序列化, 然后 执行, Task 是 ShuffleMapTask 或者是 ResultTask
ShuffleMapTask 的执行
给定的 rdd 的迭代器的获取, 这个 MapPartitionsRDD(map), 这个 rdd 的计算依赖于一个 MapPartitionsRDD(flatMap), 这个 rdd 的计算又依赖于一个 MapPartitionsRDD(map), 这个 rdd 的计算依赖于一个 HadoopRDD(textFile)
给定的 rdd 的数据 迭代
如下图的数据为 paritition0, 最底层的 HadoopRDD 拿到的是 第一行的数据 "234 345 123 346 234"
下面的几行函数 需要注意一下
分别对应的是 SparkContext.textFile 中的 map(pair => pair._2.toString)
Test01WordCount 里面的 .flatMap(line => line.split(" "))
Test01WordCount 里面的 .map(word => (word, 1))
hasNext:409, Iterator$$anon$11 (scala.collection)
hasNext:440, Iterator$$anon$12 (scala.collection)
hasNext:409, Iterator$$anon$11 (scala.collection)
然后再经历一次 mapSideCombine, 所以 第一个 ShuffleMapTask 最后的结果如下
同理 第二个 ShuffleMapTask 最后的结果如下
ShuffleMapTask 结果的输出
向 shuffleBlockResolver 解析当前 paritition 对应的 ShuffleMapTask 应该写到什么文件, 向该文件写出了 当前 Task 的数据结果信息
呵呵 因为之前整理好了一份, 我这里直接贴当时的整理结果了
ShuffleMapTask 执行过程, 里面 ExternalSorter.writePartitionedFile
blockManager 里面根据 shuffleId, mapId, reduceId(NONE_REDUCE_ID) 来计算应该存放的临时文件, 这个计算出来的结果是固定(hash%localDir.length, (hash/localDir.length)%localSubDir.length)
参见 LZ4BlockOutputStream
magic bytes[8] + token[1] + compressed length[4] + decompressed length[4] + checksum[4] + body
4C5A3442 6C6F636B 150E0000 000E0000 006B3E4D 03030131 32B30202 03013334 B502024C 5A34426C 6F636B15 00000000 00000000 00000000
4C5A3442 6C6F636B : magic bytes : LZ4Block
15 : token(COMPRESSION_METHOD_RAW | compressLevel) : 21
0E0000 00 : compressed length : 14
0E0000 00 : decompressed length : 14
6B3E4D 03 : checksum
03 : idx of java.lang.String
01 : represent not null
31 32B3 : "123", 最后一个字节 b3, 原来的数据为 33(|0x80 标记结束)
02 : idx of int
02 : 1, 记录的数据未 (value << 1) ^ (value >> 31)
03 : idx of java.lang.String
01 : represent not null
3334 B5 : "345"
02 : idx of int
02 : 1
4C 5A34426C 6F636B : magic bytes : LZ4Block
15 : token : 21
00000000 : compressed lenth
00000000 : decompressed lenth
00000000 : checksum
4C5A3442 6C6F636B 150E0000 000E0000 00055AAA 0E030132 33B40204 03013334 B602024C 5A34426C 6F636B15 00000000 00000000 00000000
4C5A3442 6C6F636B : magic bytes : LZ4Block
15 : token(COMPRESSION_METHOD_RAW | compressLevel) : 21
0E0000 00 : compressed length : 14
0E0000 00 : decompressed length : 14
6B3E4D 03 : checksum
03 : idx of java.lang.String
01 : represent not null
32 33B4 : "234"
02 : idx of int
04 : 2
03 : idx of java.lang.String
01 : represent not null
3334 B6 : "346"
02 : idx of int
02 : 1
4C 5A34426C 6F636B : magic bytes : LZ4Block
15 : token : 21
00000000 : compressed lenth
00000000 : decompressed lenth
00000000 : checksum
ShuffleMapTask 输出了两个文件, 每个里面对应两个 partition
task0 : partition0[(123, 1), (345, 1)], partition1[(234, 2), (346, 1)]
task1 : parittion0[(123, 1)], partition1[(124, 1)]
index file 里面存放了 00000000 00000000 00000000 00000038 00000000 00000070
00000000 00000000 : start
00000000 00000038 : offset0
00000000 00000070 : offset1
文件系统中的数据结果
Executor 反馈 ShuffleMapTask 结果给 Driver
Executor 封装计算结果信息
如果序列化之后的结果 超过了 maxResultSize(默认1g), 丢弃执行结果
如果是 超过了可以直接进行网络交互的大小, 先暂存在 blockManager, 返回 InDirectTaskResult 包含元数据信息
如果可以直接传输, 封装 DirectTaskResult 返回
Driver 收到 ShuffleMapTask 任务状态更新
SchedulerBackend 收到任务状态更新
通知 taskScheduler, 之后维护资源信息, 主动尝试获取可执行的任务
TaskScheduler 获取结果信息(如果是 InDirectTaskResult 从 blockManager 获取结果), 并处理
TaskSchedulerManager 更新 task, stage 的相关信息, 并通知 DagScheduler 任务完成, 所有任务完成通知 TaskScheduler
DagScheduler 处理 ShuffleMapTask 任务完成, 向 mapOutputTracker 注册 ShuffleMapTask 的输出结果信息
ResultStage 的执行
ShuffleMapStage0 两个 ShuffleMapTask 执行完成之后, 会通知到 ResultStage1 的执行
ShuffleRDD 读取 ShuffleMapTask 的输出结果
创建 ShuffleBlockFetcherIterator 的时候, 传入了 需要获取 block 列表信息, 这两个都属于 localBlocks
partition0 的 ResultTask 从本地获取这两个 blocks 的数据信息, partition0 的 ShuffleMapTask 的输出文件获取 0 - 55 字节, partition1 的 ShuffleMapTask 的输出文件获取 0 - 48 字节 ([(123, 1), (345, 1)] + [(123, 1)])
对应的 partition1 的 ResultTask 从本地获取这两个 blocks 的数据信息, partition0 的 ShuffleMapTask 的输出文件获取 56 - 112 字节, partition1 的 ShuffleMapTask 的输出文件获取 49 - 98 字节 ([(234, 2), (346, 1)] + [(124, 1)])
获取数据的 偏移, 数据量, 实际获取数据是 参照 index file 中的存储的数据, 获取当前 reduceId 需要截取的部分
ShuffleRDD 的迭代计算
这里的 iter 是在 ShuffleBlockFetcherIterator 获取的数据的 迭代器的基础上面增加了 flatter 多个 blockId 上面的结果, 增加统计信息的计算, 可中断特性
这里的 mergeCombiner 来自于 Test01WordCount 的 reduceByKey((left, right) => left + right)
ResultTask 的 Executor 封装计算结果
partition0 对应的 ResultTask 返回的数据为 [(345,1), (123,2)](一个 Tuple2 数组, 包含两个元素, 是通过 collect 方法中传入的 func "(iter: Iterator[T]) => iter.toArray" 处理之后的结果)
partition0 对应的 ResultTask 需要写出的数据拆解如下
resultSerialzie
TC_ARRAY TC_CLASSDESC classDesc[2-27] TC_BLOCKEND superClassDesc lengthOf(tuple) TC_OBJECT classDesc
valueBuffer : -84 -19 0 5 117 114 0 15 91 76 115 99 97 108 97 46 84 117 112 108 101 50 59 46 -52 0 -33 -47 79 -41 -64 2 0 0 120 112 0 0 0 2
115 114 0 12 115 99 97 108 97 46 84 117 112 108 101 50 46 -108 102 125 91 -110 -7 -11 2 0 2 76 0 2 95 49 116 0 18 76 106 97 118 97 47 108 97 110 103 47 79 98 106 101 99 116 59 76 0 2 95 50 113 0 126 0 3 120 112
116 0 3 51 52 53 115 114 0 17 106 97 118 97 46 108 97 110 103 46 73 110 116 101 103 101 114 18 -30 -96 -92 -9 -127 -121 56 2 0 1 73 0 5 118 97 108 117 101
120 114 0 16 106 97 118 97 46 108 97 110 103 46 78 117 109 98 101 114 -122 -84 -107 29 11 -108 -32 -117 2 0 0 120 112
0 0 0 1 115 113 0 126 0 2 116 0 3 49 50 51 115 113 0 126 0 6 0 0 0 2
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
需要写出的数据为 : value = {Tuple2[2]@6640}
0 = {Tuple2@6657} "(345,1)"
1 = {Tuple2@6658} "(123,2)"
# 拆解一下整个字节数组
$streamMagic : -84 -19
$streamVersion : 0 5
TC_ARRAY : 117
$TC_CLASSDESC : 114
$nameLen : 0 15
$className : 91 76 115 99 97 108 97 46 84 117 112 108 101 50 59 : [Lscala.Tuple2;
$serialVersionUid : 46 -52 0 -33 -47 79 -41 -64 : 3372071182283036608
$flags : 2 : $SC_SERIALIZABLE
$fieldsLen : 0 0
TC_ENDBLOCKDATA : 120
$superClassDesc : 112 : null
$arrayLength : 0 0 0 2
TC_OBJECT : 115
$TC_CLASSDESC : 114
$nameLen : 0 12
$className : 115 99 97 108 97 46 84 117 112 108 101 50 : scala.Tuple2
$serialVersionUid : 46 -108 102 125 91 -110 -7 -11 : 3356420310891166197
$flags : 2 : $SC_SERIALIZABLE
$fieldsLen : 0 2
$typeOf 'Ljava/lang/String;' : 76
$fieldLen : 0 2
$fieldName : 95 49 : _1
$TC_STRING, $len, Ljava/lang/String; : 116 0 18 76 106 97 118 97 47 108 97 110 103 47 79 98 106 101 99 116 59
$typeOf 'Ljava/lang/String;' : 76
$fieldLen : 0 2
$fieldName : 95 50 : _2
$TC_REFERENCE $handleOf'Ljava/lang/String;' : 113 0 126 0 3
$TC_ENDBLOCKDATA : 120
$superDesc[recursely] : 112
_1, $TC_STRING, $len, $345 : 116 03 51 52 53 : 345
TC_OBJECT : 115
$TC_CLASSDESC : 114
$nameLen : 0 17
$className : 106 97 118 97 46 108 97 110 103 46 73 110 116 101 103 101 114 : java.lang.Integer
$serialVersionUid : 18 -30 -96 -92 -9 -127 -121 56
$flags : 2 : $SC_SERIALIZABLE
$fieldsLen : 0 1
$typeOf 'Ljava/lang/Integer;' : 73
$fieldLen : 0 5
$fieldName : 118 97 108 117 101 : value
$TC_ENDBLOCKDATA : 120
$TC_CLASSDESC : 114
$nameLen : 0 16
$className : 106 97 118 97 46 108 97 110 103 46 78 117 109 98 101 114 : java.lang.Number
$serialVersionUid : -122 -84 -107 29 11 -108 -32 -117
$flags : 2 : $SC_SERIALIZABLE
$fieldsLen : 0 0
$TC_ENDBLOCKDATA : 120
$superClassDesc : 112 : null
_2 : 0 0 0 1
TC_OBJECT : 115
$TC_REFERENCE $handleOf'scala.Tuple2' : 113 0 126 0 2
_1, $TC_STRING, $len, $123 : 116 0 3 49 50 51 : 123
TC_OBJECT : 115
$TC_REFERENCE $handleOf'java.lang.Integer' : 113 0 126 0 6
_2 : 0 0 0 2
Driver 对应 ResultTask 任务完成的处理
DagScheduler 的处理
更新job的相关信息, 通知到 jobListener(就是最开始 submitJob 创建的 JobWaiter)
JobWaiter 的处理
JobWaiter 这边会使用 resultHandler 来处理结果, 如果所有的任务都完成了, 通知到 jobPromise
然后回到 DagScheduler 之后, 继续走后面的流程
Spark UI 上面的统计信息
回顾一下 文章最开始的几张图, 我们来看一下 其中的一部分数据
job 的概览信息如下
stage0 的概览信息如下
stage1 的概览信息如下
为了便于理解, 我们先从 stage 的详细信息开始
stage0 的概览信息
先看一下左上角, 描述了 当前这个 stage 的那些 transformation
sc.textFile(func).flatMap(func).map(func)
textFile 中包含了 一个 hadoopFile 的操作 和一个 map 的操作, 在 Test01WordCount 的 21 行
然后 之后有一个 flatMap 的操作, 在 Test01WordCount 的 22 行
然后 之后有一个 map 的操作, 在 Test01WordCount 的 23 行
然后我们再来看一下 Task 的信息
index 为 0 的 task, 读取了 27 字节, 读取了 1 条记录, shuffle 的时候 写出了 112 字节, 写出了 4 条记录
index 为 1 的 task, 读取了 14 字节, 读取了 1 条记录, shuffle 的时候 写出了 98 字节, 写出了 2 条记录
我们剖析一下这个数据
index 为 0 的 task 对应的是 partition0 的 ShuffleMapTask, 操作的数据是 第一行的数据 "234 345 123 346 234", 总共是 19 个字符
根据 splitLength 来往 buffer 里面填充数据, 第一个批次 13 个字节, 里面没得 CR/LF, 继续第二个批次(读取了14字节), 读到第 6 个字节之后遇到了 CR/LF, 返回 text, 这里读取是 以字节的形式读取的, 然后 text 解码为字符串(编码为 utf8)
Text 的编码, 解码方式
那么这里的 index 为 0 的 task, 读取了 27 字节, 是指的这个 13 + 14 么 ?
我们来看一下 metrics 中更新 读取字节数 的地方
获取读取的字节数的方式, 从 FileSystem 的统计信息中获取的
我们再来看一下 FileSystem 更新统计信息的地方
原来是有一个 BufferedInputStream 里面的 buffer 的长度是 65536, 因此会填充 min(available, 65536) 个字节, 对于我们这里 有 27 个字节, 因此 index 为 0 的 task, 读取了 27 字节
再看一下 index 为 0 的 task 记录的输出, 可以参见 上面 "ShuffleMapTask 结果的输出"
可以 清晰的看到, 这 112 个字节是什么, 有什么逻辑意义, 然后 输出的 4 条记录 是什么
同样的道理, 可以剖析出
index 为 1 的 task, 读取了 14 字节, 读取了 1 条记录, shuffle 的时候 写出了 98 字节, 写出了 2 条记录
stage1 的概览信息
这里的业务操作 是一个 reduceByKey(func)
然后我们再来看一下 Task 的信息
index 为 0 的 task, 读取了 105 字节, 读取了 3 条记录,
index 为 1 的 task, 读取了 105 字节, 读取了 3 条记录
partition0 的 ResultTask 从本地获取这两个 blocks 的数据信息, partition0 的 ShuffleMapTask 的输出文件获取 0 - 55 字节, partition1 的 ShuffleMapTask 的输出文件获取 0 - 48 字节 ([(123, 1), (345, 1)] + [(123, 1)])
对应的 partition1 的 ResultTask 从本地获取这两个 blocks 的数据信息, partition0 的 ShuffleMapTask 的输出文件获取 56 - 112 字节, partition1 的 ShuffleMapTask 的输出文件获取 49 - 98 字节 ([(234, 2), (346, 1)] + [(124, 1)])
因此 上面的信息 大致就是这样, 56 + 49 = 105 字节, 三条记录
job 的概览信息
当前 job 有两个 Stage, 根据 ShuffleDependecy 划分的 Stage
一个 ShuffleMapStage 和 一个 ResultStage
ShuffleMapStage 中包含 transformation : sc.textFile(func).flatMap(func).map(func)
ResultStage : redunceByKey(func)
下面是这两个 Stage 的详情
ShuffleMapStage 对应两个 Task, 读取的是 27 + 14 字节, shuffle 输出 112 + 98 字节
ResultStage 对应两个 Task, 读取 210 字节
呵呵 写了一天多, 真够多的, 愿你去调试, 愿你有所收获, 记录于 2020.09.20
完