这是我参与「第四届青训营 」笔记创作活动的第6天
1. MapReduce的三个阶段
- Map阶段,是在单机上进行的针对一小块数据的计算过程
- Shuffle阶段,在Map阶段的基础上,不同分区(不同分区可能在同一节点上,也可能不在同一节点上)之间进行数据移动,为后续的Reduce阶段做准备
- Reduce阶段,对移动后的数据进行处理,依然是在单机上处理一小份数据
2. 为什么Shuffle对性能非常重要
- M * R 次网络连接
- 大量的数据移动
- 数据丢失风险
- 可能存在大量的排序操作(Map阶段)
- 大量的数据序列化、反序列化操作
- 将Java对象或者内存中直观的数据转化成二进制流放入文件中,在从文件中读到内存里面来,重新变成一个对象
- 数据压缩
3. Spark中会产生Shuffle的算子
| repartition | ByKey | join | distict |
|---|---|---|---|
| coalesce | groupByKey | cogroup | distinct |
| repartition | reduceByKey | join | |
| aggregateByKey | leftOuterJoin | ||
| combineByKey | intersection | ||
| sortByKey | substract | ||
| sortBy | substractByKey |
4. Spark中Shuffle的抽象 - 宽依赖、窄依赖
窄依赖:父RDD的每一个分片至多被子RDD中的一个分片所依赖
宽依赖:父RDD中的分片可能被子RDD中的多个分片所依赖
5. Hash Shuffle - 写数据
- 每个partition会映射到一个独立的文件,每个Map Task都会给每个partition创建一个Buffer,写满了之后就flush到磁盘里,最终会生成 M * R 个文件
-
出现的问题:
- 生成的文件太多,对文件系统造成的压力太大
- 而且在运行时,同时打开的文件也多,如果一个Map Task打开的文件太多,这个Map Task就有可能内存溢出
-
写数据优化
-
每个partition映射到一个文件片段
-
同一核上运行的多个 Map Task 输出合并到同一个文件
- 资源是有限的,比如一个stage有 10000 Map Task,因为资源有限,所以没有办法同时跑,只申请到了100个CPU来处理这些数据,可能只有100个Map Task同时跑,打开了100个文件,每个Map Task会往这个文件里写一个文件片段
- 在执行下一批的Map Task时,会复用已经创建好的磁盘文件,即数据会继续写入到已有的磁盘文件
- 该机制允许不同Map Task复用同一个磁盘文件,对于多个Map Task进行了一定程度的合并,减少了文件的数量,一定程度上提升了性能
-
这样,最终会得到 C * R 个文件(C是申请到的CPU核数),但从M改成C,问题并没有根本性解决
-
6. Sort Shuffle - 写数据
- 不再给每个partition一个Buffer了,给每个Task一个Buffer,这个Task中所有partition的数据都写到一个Buffer里面,当Buffer中的数据达到阈值的时候,通过排序的方式,把相同的partition数据放在一起,排序之后将数据分批溢写到磁盘(分批能够减少IO次数),所以叫Sort Shuffle。
- 相比于Hash Shuffle,Sort Shuffle在排序上会消耗更多的CPU,好处是每个Task只会创建一个完整的文件(实际上是两个文件,还创建了一个indexfile去记录每一个FileSegment的偏移量),最终每个Task生成了一个包含所有partition的数据文件
7. Shuffle - 读数据
每个reduce task分别获取所有map task生成的属于自己的片段