Spark的flatMap算子引发的数据倾斜问题
问题背景
Spark中有时候会用到flatMap算子来处理数据,flatMap把序列打平,即将每一条记录变成多条记录。这个算子在数据量大的时候经常会发生数据倾斜问题,你会发现一旦原始数据记录到达亿级、十亿级甚至百亿级时,这个算子会非常令人头疼,任务一直卡在最后一个或者几个task上面,毫无进展,GC日志会显示“not enough memory”,最后任务因为内存出现“Executor heartbeat timed out after XXXX ms”的提示,然后任务就这样挂了。
数据示例
这里列出一个示例,我们有一个大数据表,假设在HBASE里面每条记录是KV形式存储,key是物品ID,ID唯一;value是物品的属性。假设有28个属性,用feature1、feature2…feature30来表示,这28个属性由逗号分隔拼接成一个字符串,如果该物品在某个属性上面没有值,用空 ( null ) 表示;如果某个属性可以有多个值,多个值之间用分号 ( ; ) 表示。以下给出示例
key | value |
123456 | 13,2,1,19,199,119199,440300,782,5,null,null,null,null,null,null,1,121;117;107;103;106;122;111;102;114;105,005,50908,1.0,null,null,13,null,null,19;22;28,1901;2802;2204,null |
123457 | 11,2,1,22,-22,122000,500000,345,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null |
123458 | 11,2,1,12,-12,112000,310000,233,null,null,null,null,null,null,null,null,104;103;112;106;122;111;102;115;101,null,null,null,50102,null,12,40404,null,null,null,34 |
123459 | 12,2,1,19,199,119199,440300,255,null,55,63,15,141,342,1,null,null,null,null,null,null,null,null,null,null,null,null,33 |
计算目标
这里需要统计每个特征的覆盖情况和每个特征值的占比情况。例如,feature( i ) 不是多值特征,有3个取值 ‘A’、‘B’、‘C’,即feature( i ) ∈ { ‘A’、‘B’、‘C’},假设物品ID一共10亿,在 feature( i )上面有值的ID一共有8亿,则覆盖率为80%;假设 feature( i ) 取值为 ‘A’ 的有4亿,取值为 ‘B’ 的有3亿,取值为 ‘C’ 有1亿,则占比分别为50%、37.5%、12.5%。
问题原因
我们读取全量数据之后,对每个value进行拆分(split)操作,拆分后对每个特征判断是否为空,不为空的话和ID一起形成一个pair对 ( ID , feature( i )_value ),后面的feature(i)表示特征名,value表示特征值,使用下划线连接起来;如果是多值特征,这个特征会变成多条记录出现 ( ID , feature( i )_value1 )、( ID , feature( i )_value2 ) . . .
这样一条记录经过 flatMap 操作之后可能会变成了28条甚至更多条记录,后面 reduceByKey 进行累计统计。
假设 flatMap 变成了28条记录,那么记录量从10亿瞬间变成了280亿,由于特征分布的不平衡性,有些分区的数据量会远远大于其他分区,造成实际的数据倾斜。
解决方式
- 增加分区的数量,相当于增加并行度,这是一种粗暴的方式,可以缓解下,但是对于大数据量的情况用处基本不大
- 调整参数的一些值,使用重试机制,一旦某些任务超时,及时启动重试机制, 一面某个任务卡死
- 分阶段进行 reduceByKey,可能某个pair对总记录非常多,那么在 reduceByKey 的时候就会把所有相同的key拉取到一个partition上面,这个partition内存就会爆掉。有一个办法可以大幅缓解,在 reduceByKey 之前将pair对加上一个随机数,例如将ID前面加入一个随机数,然后使用 “_” 分隔,这样就打散了原有的 key,如果某些 key 对应的记录特别多,可以再在前面加一个随机数和一个分隔符,这样后面两次 reduceBykey 来处理,但是如果数据量超大的话,因为传输的数据变大了,需要的内存也相应的变多,不建议添加超过2个随机数
- 如果以上方法都失效了,说明数据量是在是太大了,那么我们换种方式解决问题。使用for循环,每个特征 reduceBeKey 一次,串行的方式进行,这样虽然时间长些,但是任务可以正常跑完
- 如果连for循环单个特征都无法跑过,说明内存设置的确需要增大了,调大executor的数量和内存放大 1.2 - 1.5 倍基本可以解决