Spark 调优:数据倾斜解决方案、Shuffle、Stage

    数据倾斜导致的后果,一般是 OOM 或者 速度异常慢

一、数据倾斜基本形态特征、表现

  • 数据分布不均匀,个别 Task 处理大量数据,造成热点数据问题
    • 大部分 Task 都执行特别快,剩下几个 Task 执行特别慢,超过平均时间好几倍,很明显就是数据倾斜
  • Spark 任务好好的,突然有一天发生了 OOM
    • 其他 Task 都执行完了,没什么特别的问题,但是有的 Task 会突然报一个 OOM、Task failed、Task lost、Resubmitting Task,反复执行几次都跑不通,最后挂掉
    • 或者某个 Task 直接 OOM

二、数据倾斜的定位

  • Web UI( job/stage/task ),可以清晰看见哪个 Task 运行的数据量大小
  • Log,可以清晰告诉你哪一行出现 OOM,同时看到具体哪个 Stage 出现了数据倾斜(一般是在 Shuffle 过程中产生)
    • 也可能发现绝大多数 Task 非常快,但是个别 Task 非常慢
  • 代码走读,重点看 distinct、join、groupByKey、reduceByKey、aggregateByKey、repartition 等关键代码
  • 对数据特征分布进行分析

三、数据倾斜原理

  • 在 Shuffle 的时候,必须将各个节点相同的 Key 拉到某一个节点进行 Task 的处理
    • 比如 join、group by,如果某个 Key 对应的数据量非常大,那么必然这个 key 对应的数据进行处理的时候就会产生数据倾斜
  • 归根到底就是 Key 分布不均匀问题

四、Stage 划分

  • 划分依赖
    • Stage 划分的依据是宽依赖,reduceByKey、groupByKey 等算子会导致宽依赖的产生
  • 核心算法
    • Spark 会从触发 Action 操作的那个 RDD 开始往回推,如果发现某个 RDD 是宽依赖,就为这个 RDD 创建新的 Stage,这个 RDD 就是新 Stage 的最后一个 RDD
  • Task 任务集
    • 有一组关联的、相互之间没有 Shuffle 依赖关系的任务所组成的任务集
    • 一个 Stage 创建一个 TaskSet,为 Stage 的每个 RDD 分区创建一个 Task,多个 Task 封装成 TaskSet
      • Task 任务被送到某个 Executor 上的工作任务,单分区数据集上的最小处理流程单元

五、Spark Shuffle

5.1 什么是 Shuffle

  • Shuffle:洗牌,连接 Map 和 Reduce 的桥梁
  • Shuffle write:在 DAG 阶段以 Shuffle 为界划分 Stage,上游 Stage 叫做 Map Task,每个 Map Task 将计算结果划分成多份,每一份对应下游 Stage 的每个 Partition,并临时写到磁盘
    • 涉及到序列化、磁盘 IO 等操作
  • Shuffle read:下游 Stage 作为 Reduce Task,通过网络拉取上游 Stage 中所有 Map Task 的指定分区结果数据,最后完成 Reduce 的任务逻辑
    • 涉及到反序列化、网络 IO 等操作
  • 案例:有 100个 Map Task 和 1000个 Reduce Task,那么 100个 Map Task 就会有 1000份数据,每个 Reduce Task 都会从 100个 Map Task 拉取对应的那份数据

5.2 宽窄依赖

  • 窄依赖:一个父 RDD 的 partition 至多被子 RDD 的某个 partition 使用一次
  • 宽依赖:一个父 RDD 的 partition 会被子 RDD 的 partition 使用多次,有 Shuffle
    • 遇到 Shuffle 原来的 stage 会被拆分

5.3 Shffle 类型

  • 1.1 版本以前只有 Hash Shuffle,1.1 引入 Sort Shuffle,1.5 引入钨丝引入 UnSafe Shuffle 优化内存和 CPU 的使用,1.6 钨丝并入 Sort Shuffle,2.0 删除 Hash Shuffle,现在只有 Sort Shuffle

5.4 Hash Shuffle

  • V1
    • Shuffle write 阶段会为下游 Stage 的每个 Partition 写一个临时文件,有多少个 Reduce Task 就有多少个文件
    • 一般来说,一个 executor 会运行多个 Map Task,产生非常多临时文件,一旦 Partition 数量变多,就会带来非常大的内存消耗
    • Shuffle read 阶段,executor 会打开临时文件准备网络传输,又会涉及到大量的文件
    • 如果 reduce 阶段有 combiner 操作,会把网络中拉取到的数据保存到一个 HashMap 中进行合并操作,数据量大的话很容易引发 OOM
    • 案例:一个 executor 运行 N 个 Map Task,下游有 M 个 Partition,就会产生 N*M 个文件
  • V2
    • 为了减少单个 executor 文件数,一个 executor 上所有的 Map Task 生成的分区文件只有一份,所有的 Map Task 相同分区文件合并,一个 executor 只产生 M 个文件
    • 表面上减少了文件数,但是下游 Stage 的分区很大,还是会在每个 executor 上生成非常多的文件
    • Shuffle read 阶段跟 V1 一样,没改变

5.5 Sort Shuffle

  • V1
    • Shuffle write 阶段,按照 Partition ID 以及 Key 对数据进行排序,所有 Partition 数据写在同一个文件中,按照 Partition ID 一个一个分区顺序排序,每个 Partition 内部按照 Key 排序存放
    • Map Task 运行期间会顺序写每个 Partition 的数据,并通过一个索引文件记录每个 Partition 大小和偏移量,一个写数据,一个写索引,两个文件,减轻了 Hash Shuffle 大量文件的问题
    • Shuffle read 阶段拉取数据不再采用 HashMap,采用 Spark 重写的一个 ExternalAppendOnlyMap,在 combine 时,内存不足会刷写磁盘,很大程度保证了鲁棒性,避免大数据情况下的 OOM
  • V2
    • 把 UnSafa Shuffle(钨丝) 合并过来,如果满足条件就用钨丝,不满足就用 Sort Shuffle

5.6 钨丝

  • 将数据记录用二进制的方式存储,直接在序列化的二进制数据上 Sort,而不是在 Java 对象上,减少内存的使用和 GC 的开销,也避免频繁的序列化和反序列化
  • 限制:Shuffle 阶段不能有 aggregate 操作,分区数不能超过一定大小

六、处理方式/解决方案

  • 总结参考:https://betheme.net/news/txtlist_i96751v.html

6.1 聚合源数据

  • 在数据来源上把大量的 Key 先聚合,相当于做一个预处理,把 Key 对应的所有 value 进行一个拼接处理或者先一步聚合
  • 如果不能先一步预处理聚合,可以考虑放粗粒度,范围缩小一点

6.2 对源数据进行预处理

  • 文件有些大有些小,不均匀,Spark 读取大文件的时候会切分大文件,可以先对文件数据进行清洗、去重、重新分区等操作将原本不均匀的数据重新均匀分布

6.3 过滤少数导致倾斜的 Key

  • 如果倾斜的 Key 没有业务意义,比如是 Null 值,直接过滤调
  • 如果倾斜的 Key 有业务意义,单独拎出来进行单独处理
  • 做取舍,业务和需求能理解和接受的情况下,可以添加 where 条件

6.4 提高并行度

  • 触发 Shuffle 的算子,在调用的时候传入一个值,代表 reduce 端的并行度,创建对应数量的 Reduce Task
  • 缺陷
    • 治标不治本,没有从根本上改变数据倾斜的问题,只能尽可能缓解和减轻 Shuffle Reduce Task 的压力
  • 生产经验
    • 理想情况下,减轻了数据倾斜的问题,甚至让数据倾斜的现象忽略不计,就不需要做其他方案
    • 不理想情况下,只是稍微快那么一点,只能说不 OOM 了,但还是很慢

6.5 局部聚合 + 全局聚合

  • 把相同 Key 随机添加前缀,变成多个不同的 Key,原本要被一个 Task 处理的数据分散到多个 Task 上做局部聚合,解决掉单个 Task 处理数据量过多的问题
  • 接着去除前缀,再进行一次全局聚合,得到最终结果
  • 对 groupByKey、reduceByKey 有比较好的效果

6.6 将 reduce join 转为 map join

  • 不走 Shuffle,牺牲一点内存资源,直接走 map join
  • 使用场景:两个 RDD 要 join,其中一个 RDD 是百万级数据,一个 RDD 是万级数据,其中一个 RDD 必须是比较小的,相差好上百倍
  • 方式
    • 将小的 RDD 进行 broadcast 广播,在每个 executor 的 block manager 中会保存一份,需要确保内存足够放小的 RDD 数据
    • 这种情况下一般不会触发 Shuffle,也不会有数据倾斜,从根本上杜绝了 join 操作可能导致的数据倾斜问题
  • 不适合的情况:两个 RDD 都比较大,内存不够放

6.7 sample 采样倾斜 key 单独进行 join

  • 思路:把发生倾斜的 key 单独拉出来,放到一个 RDD 中去,用原本会倾斜的 Key RDD 跟其他 RDD 单独去 join 一下
  • 场景:相同 Key 过大占用内存,无法转 map join,适合倾斜 Key 的种类不是很多的场景
  • 方式
    • 数据量过大的 Key 的 RDD 通过 sample 算子采集出一份样本来,然后统计一下哪个 Key 数据量最大
    • 将这几个 Key 对应的数据从原来的 RDD 中拆分出来,形成一个单独的 RDD,并给每个 Key 打上 N 以内的随机数前缀,这样就可以形成不同的 Key
    • 需要 join 操作的另一个 RDD,过滤出倾斜 Key 对应的数据形成单独的 RDD,然后每条数据都膨胀 N 条,对应上前缀
    • 然后这两个 RDD 进行 join,原先相同的 Key 打散成来 N 份,分散到多个 Task 去 join
    • 其他的 RDD 就照常 join 即可,最后 union 算子合并起来即可
  • 不适合的场景:导致数据倾斜的 Key 特别多

6.8 使用随机前缀和扩容 RDD 进行 join

  • 场景:join 操作中,RDD 有大量 Key 导致数据倾斜
  • 原理:将 RDD 分成含倾斜 Key 的 RDD,不含倾斜 Key 的 RDD,直接多原本的 RDD 加随机数,另一边进行扩容,对内存资源要求很高
  • 方式
    • 使用 flatMap 对 RDD 进行扩容,将每条数据映射为多条数据,每条映射出来的数据,都带一个 n 以内的随机数
    • 将另一个 RDD 做 map 操作,都打上 n 以内的随机数
    • 两个处理后的 RDD 进行 join 操作
  • 弊端:没办法将某一个 RDD 扩得特别大,只能缓和和减轻数据倾斜问题,不能彻底解决
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值