考察点
这道题出自字节跳动数据仓库工程师一面。字节内部数据量大,倾斜是日常高频问题,面试官想确认你是不是真的在生产上排查过倾斜,还是只背过"加盐打散"四个字。考察重点:能准确说出倾斜发生的环节,针对每个环节给出对应解法,并且能讲清楚为什么这样解有效。追问一般会往"你怎么定位倾斜的 key"、"加盐之后聚合结果怎么还原"、"Skew Join 参数怎么配"这几个方向走。
参考答案
倾斜到底发生在哪
先说结论:倾斜的本质是某些 key 的数据量远超其他 key,导致个别 task/reducer 处理的数据量是其他 task 的几百倍。它可能发生在三个阶段。
Map 阶段倾斜:源头文件本身不均。比如一个 gzip 文件 5G,gzip 不可切分,整个文件只能由一个 map task 处理,其他 map 早跑完了它在慢慢读。解法是换可切分的压缩格式(LZO 建索引、bzip2)或者用 SequenceFile/ORC 这类列存格式。
Shuffle/聚合阶段倾斜:group by 的 key 分布不均。典型场景是日志里大量空值、默认值,或者某个爆量用户、某个省份占了绝大部分流量。
Join 阶段倾斜:大表 join 大表时 join key 倾斜,集中在少数几个 reducer 上,表现是 99% 的 task 几分钟跑完,剩一两个 task 跑几个小时。
聚合场景的解法
group by 倾斜,Hive 里最省事的是 set hive.groupby.skewindata=true。它的原理是生成两个 MR job:第一个 job 把数据随机分发到 reducer 做局部聚合(类似预聚合),第二个 job 再按原始 key 分发做全局聚合。因为第一个 job 已经把同一个热点 key 的数据压缩掉了大部分,第二个 job 的压力就小很多。
Spark 里手写两阶段聚合也是同一个思路:第一阶段给 key 加上随机前缀,比如 key + "_" + (int)(Math.random()*10),先局部聚合;第二阶段去掉前缀再全局聚合。代价是多一次 shuffle,但换来的是热点 key 被拆成 N 份。
还有一个常被忽略的手段:能先在 map 端 combine 的就 combine。Hive 的 hive.map.aggr=true 默认开着,Spark 里用 reduceByKey 而不是 groupByKey,就是为了让 map 端先收敛一轮,减少 shuffle 数据量。
Join 场景的解法
分情况处理。如果一表大一表小(小表能放进内存,一般经验是几百 M 以内),直接 map join(broadcast join),把小表广播到每个 executor,大表 map 端本地关联,根本没有 shuffle,倾斜无从谈起。Hive 里 hive.auto.convert.join=true 加 hive.mapjoin.smalltable.filesize 控制,Spark 里 spark.sql.autoBroadcastJoinThreshold 默认 10M,生产上常调大。
大表 join 大表、只有少量 key 倾斜时,把热点 key 单独拎出来处理:先从大表过滤出热点 key 的行,热点 key 本身数据量小,对这部分做 broadcast join 或单独加盐处理;非热点部分正常 shuffle join,最后 union。这是字节面经里被反复验证过的"分而治之"答法。
如果热点 key 太多没法单独拎,就用加盐扩容:大表侧给 join key 加 0N 的随机后缀,小表侧(或另一侧大表)把每行复制 N 份、分别带上 0N 后缀,相当于把热点 key 的笛卡尔积摊到 N 个 task 上。N 取多大看倾斜倍数,一般先按热点 key 数据量/平均 key 数据量估算。
另外新一点的思路是直接用引擎自带的倾斜处理:Spark 3 的 AQE 有 spark.sql.adaptive.skewJoin.enabled,能自动把过大的 partition 切开;Hive on Tez 也有 skew join 优化。能答出这个说明跟进过新版本。
定位倾斜 key
解法之外,定位能力同样被考察。Hive 看日志里 reducer 的处理行数分布,Spark 看 UI 的 Stage 页面 task 耗时和 input size 的长尾。找到慢 task 后,对 join key 做一次 group by key count(*) 排序,top 几个 key 一目了然。如果是空值或 -1 这种脏 key,过滤或随机打散即可,不用走复杂的加盐流程。
可能的追问
- 加盐之后怎么保证聚合结果正确?局部聚合完成后去掉随机前缀,再做一次全局聚合,两层聚合保证语义等价,代价是多一轮 shuffle。
- map join 时小表多大算"小"?看 executor 内存,广播变量要能放进内存还留有余量,一般几百 M 是上限;调阈值前先看 OOM 风险和 GC 情况。
- 如果倾斜 key 是空字符串占了 40% 怎么办?空 key 不需要 join 结果的话直接过滤;需要保留就单独给空 key 随机打散成 N 份再处理,别把正常 key 也拖下水。