设想一下双十一深夜的快递分拣中心:几十辆卡车从全国各地收货网点涌来,司机们把包裹卸在中央大厅,分拣员们盯着面单上的城市名,飞快地把包裹丢进不同颜色的传送带,末端再由配送车拉走。你一眼就能看出问题——上海方向的传送带前堆成了小山,分拣员手忙脚乱;而小县城的传送带空空如也,偶尔才落下一个包裹。整个中心的总吞吐量,被这条“爆仓”的传送带死死卡住。 这就是分布式计算里每天都在上演的场景。数据就是包裹,计算节点就是分拣员,把中间结果按 key 重新分配的过程叫 Shuffle(数据搬移),而某个 key 的数据量远大于其他 key 时,就发生了数据倾斜。两者叠加,轻则作业多跑几个小时,重则整个集群被一个“吃货”任务拖垮。 这篇文章,我想带你以分拣中心管理员的视角,看清 Shuffle 的搬运路线、倾斜的爆仓成因,以及加盐、广播、AQE 这些“拆传送带”的治理手段。你会看懂 Spark 与 Flink 的调优参数背后到底在调什么,也会拿到一套真实生产问题的排查方法论。

岗位关联

对 AI 从业者而言,shuffle 与数据倾斜是分布式训练与推理链路中最常见的性能杀手。掌握这套治理思路,意味着你能在算力成本、任务时延与系统稳定性之间做出专业取舍,这是架构师与高级工程师的必备素养。

目录
  1. 可视化演示
  2. Shuffle机制剖析:数据搬运的物流经济学
  3. 数据倾斜成因:为什么大家都在等一个人
  4. 负载均衡策略:把爆仓的传送带拆开
  5. 大表关联优化:专线直达的广播思维
  6. Spark工程实践:聪明的调度中枢 AQE
  7. Flink流式治理:永不停歇的传送带
  8. 生产案例复盘:双十一后的战报
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态展示Shuffle全流程、三类Shuffle、数据倾斜长尾特征与Skew治理策略。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Shuffle机制剖析:数据搬运的物流经济学

双十一深夜,分拣中心的大厅里,分拣员们把包裹从各收货网点卸下的卡车上搬下来,看一眼面单上的城市,扔进对应传送带,再由配送车拉走。这套动作看似简单,其实暗藏玄机:包裹不是瞬移到传送带上的,而是要经历“装车→运输→卸货→归类→再装车”的全过程。在分布式计算里,这一整套数据搬运流程,就叫 Shuffle。

具体到技术实现,Shuffle 被拆成 Map 端和 Reduce 端两段。Map 端先把计算结果写进内存缓冲(MapOutputBuffer),缓冲区快满时溢写(spill)到本地磁盘,并在溢写过程中按目标分区做分区(partition)和排序(sort)。Reduce 端随后启动 fetch 线程,从各个 Map 节点的磁盘上拉取属于自己的分片,在内存中归并(merge)后交给 Reduce 函数处理。你可能会疑惑:明明只是把数据“搬过去”,为什么这么费劲?因为每一条数据都要经历写磁盘、网络传输、远端读磁盘这三道 IO 关卡,而 IO 恰恰是最慢的环节。

回看分拣中心:包裹从收货网点运到中央大厅再装车出发,运输和装卸环节的时间远超过分拣员“看一眼面单”的时间。同理,Shuffle 的 IO 开销远大于 CPU 计算,所以优化分布式作业的首要目标,就是让 Shuffle 搬得少、搬得快。

Ttotal=Tmap+Tshuffle+Treduce\displaystyle T_{total}=T_{map}+T_{shuffle}+T_{reduce}
一个作业的总耗时可以拆成三段:Map 计算、Shuffle 搬运、Reduce 计算。实际生产中,Shuffle 的占比往往高达 60% 以上,是绝对的性能瓶颈。
  1. 总耗时 $T_{total}$一次分布式作业从提交到完成的总时长,是衡量作业性能的最终指标。
    Ttotal\displaystyle T_{total}
  2. Map 阶段 $T_{map}$每个节点对本地产出数据做初步计算(如过滤、投影、局部聚合)的耗时。
    Tmap\displaystyle T_{map}
  3. Shuffle 阶段 $T_{shuffle}$包括 Map 端溢写磁盘、网络传输、Reduce 端拉取归并的全部耗时。
    Tshuffle\displaystyle T_{shuffle}
  4. Reduce 阶段 $T_{reduce}$Reduce 任务对拉取到的数据做最终计算并输出的耗时。
    Treduce\displaystyle T_{reduce}

数据倾斜成因:为什么大家都在等一个人

分拣中心有一个典型噩梦:某个直播间爆款商品有 90% 的订单寄往同一个城市,那这座城市对应的传送带前就会堆起一座包裹山。其他分拣员已经闲得聊天了,这座城市的分拣员却不得不加班到天亮。中心的总效率,永远被最堵的那条传送带决定。分布式计算里,这个“堵点”叫做长尾任务。

技术上的根因是数据倾斜:某个或某几个 key 的数据量远超平均值,导致哈希分区后,这些 key 所在的 task 处理量激增。常见成因有四类:一是 Key 分布本身不均衡,比如日志里某个用户 ID 占了一半;二是业务热点 Key,比如秒杀商品、头部主播的 ID;三是分区函数设计不当,例如自定的分区策略把大量冲突 key 映射到同一分区;四是关联键大量为空值,而空值哈希后常常落到同一个分区。后果就是 Spark 应用里“其他任务跑 5 分钟,某个任务跑 50 分钟”。

传送带前的包裹山不会自己消失,必须靠管理员介入。分布式作业也一样,$\alpha$ 一旦失控,加多少机器都不管用——因为所有任务都在等那个最慢的队友。

Tskew=α×Tmean,α≫1\displaystyle T_{skew}=\alpha \times T_{mean},\qquad \alpha\gg 1
倾斜任务的耗时 $T_{skew}$ 可以表示为平均任务耗时 $T_{mean}$ 乘以偏斜系数 $\alpha$。当 $\alpha$ 远大于 1 时,证明作业存在严重倾斜。
  1. 平均耗时 $T_{mean}$所有同类任务耗时的平均值,表示理想状态下单个任务应有的时长。
    Tmean\displaystyle T_{mean}
  2. 偏斜系数 $\alpha$倾斜任务耗时与平均耗时的比值,$\alpha$ 越大说明负载越不均匀。
    α\displaystyle \alpha
  3. 倾斜任务耗时 $T_{skew}$最终被最慢任务拖累的作业完成时间,它是 $\alpha$ 与 $T_{mean}$ 的乘积。
    Tskew=α×Tmean\displaystyle T_{skew}=\alpha \times T_{mean}

负载均衡策略:把爆仓的传送带拆开

面对上海方向的包裹山,聪明的分拣中心不会只让上海分拣员拼命,而是把上海包裹按城区拆成静安、浦东、徐汇等若干子堆,让空闲的分拣员每人负责一个子堆,最后在上海的配送中心再合流。这个思路对应分布式计算里的两阶段聚合与加盐打散。

两阶段聚合的核心是“先局部、后全局”:第一阶段在 Map 端或上游算子对相同 key 做一次局部聚合,大幅削减要 shuffle 的数据量;第二阶段再把局部结果按 key 汇总。如果热点 key 仍然集中,就给它加一个随机前缀(盐),使其均匀散落到更多分区,完成局部聚合后再去掉前缀做全局聚合。此外,采样定位热点 key 也很关键,通常用超几何采样或精确统计找出 Top N key,只有先知道“哪条传送带会爆仓”,才能精准拆流。

拆流再合流,如同把一座包裹山分成几座小丘,让每个分拣员都动起来。这就是负载均衡的本质:不求人人等量,但求无人堵死。

v~i=⨁j=1svij,V=⨁i=1kv~i\displaystyle \tilde{v}_i=\bigoplus_{j=1}^{s}v_{ij},\qquad V=\bigoplus_{i=1}^{k}\tilde{v}_i
第一行表示第 $i$ 组内部对 $s$ 条记录做局部聚合得到 $\tilde{v}_i$;第二行表示把 $k$ 组局部结果再做一次全局聚合得到最终结果 $V$。
  1. 组内记录 $v_{ij}$加盐后同一子分区内的第 $j$ 条数据,它们是同一个大组被拆散后的成员。
    vij\displaystyle v_{ij}
  2. 聚合算子 $\bigoplus$具体的聚合操作,可以是求和、计数、求最大值等。
    ⨁\displaystyle \bigoplus
  3. 局部聚合结果 $\tilde{v}_i$每个子分区先独立计算出的中间结果,避免原始数据直接参与全局 shuffle。
    v~i=⨁j=1svij\displaystyle \tilde{v}_i=\bigoplus_{j=1}^{s}v_{ij}
  4. 全局结果 $V$把所有局部结果再聚合一次,得到最终答案。这一步数据量已很小,shuffle 代价可忽略。
    V=⨁i=1kv~i\displaystyle V=\bigoplus_{i=1}^{k}\tilde{v}_i

大表关联优化:专线直达的广播思维

分拣中心还有一种特殊包裹——冰箱、洗衣机这类大件。它们走普通传送带容易卡住,所以通常安排专车直接从产地运往目的地,不经中央分拣。分布式计算里同样有这种“专线直达”的招数:广播小表。

当参与 join 的两张表一张很大、一张很小(例如几 MB 的维度表)时,与其让大表数据全部 shuffle 到 Reduce 端慢慢匹配,不如把小表广播到每个 Map 节点。这样每个 Map 节点都在本地完成 join,彻底跳过 shuffle。Spark 中由 spark.sql.autoBroadcastJoinThreshold 控制阈值,默认 10MB 左右。若大表无法广播,则用 Skew Join:先把倾斜的大表 key 加盐拆开,同时把小表对应 key 复制成同样数量的副本,使两边按“盐值”配对,负载自然均匀。分桶表优化则是更前置的手段,建表时按 key 分桶,join 时桶对桶直连,避免运行时 shuffle。

广播的思路与冰箱专线异曲同工:与其让数据绕着弯子挤传送带,不如直接送到该去的地方,省时又省力。

Broadcast:Tsmall  if  size(Tsmall)≤Sthreshold\displaystyle \mathrm{Broadcast:}\quad T_{small}\ \ \text{if}\ \ size(T_{small})\le S_{threshold}
当小表尺寸不超过阈值时,选择广播策略——把小表拷贝到每个任务节点,用空间换时间。
  1. 小表大小 $size(T_{small})$参与 join 的较小数据集的字节数。
    size(Tsmall)\displaystyle size(T_{small})
  2. 广播阈值 $S_{threshold}$框架允许广播的最大尺寸,超过则放弃广播。
    Sthreshold\displaystyle S_{threshold}
  3. 广播判定条件成立时把小表分发到所有 Map 节点,使其在本地完成 join。
    size(Tsmall)≤Sthreshold\displaystyle size(T_{small})\le S_{threshold}

Spark工程实践:聪明的调度中枢 AQE

分拣中心的管理员不会傻乎乎地按上周的包裹量配置传送带,他盯着大屏,实时调整:这条线爆仓了,马上加开一组传送带;那条线空转,立刻并线。Spark 3.x 的自适应执行(AQE)正是这样一个调度中枢。

开启 spark.sql.adaptive.enabled=true 后,AQE 在每个 shuffle 阶段结束后重新采集统计信息,做三件事:动态合并过小的 shuffle 分区,减少任务调度开销;动态把大分区拆分成多个小分区,让倾斜任务重新均匀;动态调整 join 策略,例如运行中发现某张表实际数据量很小,就自动从 SortMergeJoin 切换为 BroadcastJoin。工程上还需配合参数调优,如 spark.sql.adaptive.coalescePartitions.minPartitionSize 控制最小分区字节数,spark.sql.adaptive.skewJoin.skewedPartitionFactor 控制倾斜分区的判定倍率。

有了 AQE,集群就像有了实时盯盘的调度员,把每条传送带的负载都调校到最优。这也是为什么“升级到 Spark 3 并开启 AQE”会成为当前倾斜治理的第一条建议。

N∗=min⁡(Ncur, ∣D∣targetSize)\displaystyle N^{*}=\min\left(N_{cur},\ \frac{|D|}{targetSize}\right)
AQE 计算理想分区数:取当前分区数与“数据总量除以目标分区大小”之间的较小值,作为下一阶段实际使用的分区数。
  1. 当前分区数 $N_{cur}$当前 shuffle 阶段已经生成的分区个数。
    Ncur\displaystyle N_{cur}
  2. 数据总量 $|D|$该阶段所有分区数据字节数的总和,来自上一阶段的统计反馈。
    ∣D∣\displaystyle |D|
  3. 目标分区大小 $targetSize$期望每个分区承载的数据量,由管理员配置或框架自动推导。
    targetSize\displaystyle targetSize
  4. 新分区数 $N^{*}$取两者较小值,既避免小分区碎片化,又保证大分区被拆分。
    N∗=min⁡(Ncur, ∣D∣targetSize)\displaystyle N^{*}=\min\left(N_{cur},\ \frac{|D|}{targetSize}\right)

Flink流式治理:永不停歇的传送带

批处理作业像“按波次发车”的卡车,攒够一批统一运。流处理则像一台永不停歇的传送带,数据持续不断地从上游涌来——这意味着倾斜在流式环境里更加隐蔽:它不会表现为“某个 task 跑到 50 分钟”,而是让某个 subtask 的输入队列越堆越高,最终触发反压(backpressure),把压力反向传导到整个链路。

治理流式倾斜,首先要能看见热 key。借助 Flink 的 metrics 体系,按 key 统计记录数或字节数,当某个 key 的指标显著高于均值时,即可判定热点。随后对热点 key 加盐:在 KeyedProcessFunction 或窗口聚合前加入随机前缀,将其分散到多个子任务并行处理,汇聚层再去掉前缀做二次聚合。也可将热点 key 单独路由到一条轻量分支,与其他数据隔离,避免窄口堵死整条管道。键控窗口场景下,窗口函数本身会加剧倾斜,此时应优先采用两阶段窗口聚合。

流式传送带上的拥堵与批处理不同,它是“慢性病”,不会立刻让作业失败,却会持续消耗吞吐。及早发现热 key、及时加盐拆流,才能让这条永不停歇的流水线真正“永不停歇”。

λhot>β×λˉ\displaystyle \lambda_{hot}>\beta \times \bar{\lambda}
当某个 key 的到达速率 $\lambda_{hot}$ 超过平均速率 $\bar{\lambda}$ 的 $\beta$ 倍阈值时,判定为热 key,触发加盐分流。
  1. 热点 Key 速率 $\lambda_{hot}$单位时间内某个 key 到达的条数或字节数。
    λhot\displaystyle \lambda_{hot}
  2. 平均速率 $\bar{\lambda}$所有 key 的平均到达速率,作为正常基线的参考。
    λˉ\displaystyle \bar{\lambda}
  3. 判定阈值 $\beta$倍率阈值,超过即触发分流,通常取 2 到 5。
    λhot>β×λˉ\displaystyle \lambda_{hot}>\beta \times \bar{\lambda}

生产案例复盘:双十一后的战报

复盘是成长的阶梯。当年双十一之后,某物流中心的管理员把各传送带录像调出来逐帧分析,发现华东传送带爆仓的根源是某个大主播的粉丝都集中在一个仓库下单。找到了源头,方案就水到渠成。分布式作业的排查也是同样的逻辑。

某电商企业订单明细与商品大表 join 的批处理作业,每天凌晨 1 点开跑,耗时约 2 小时。现象是 Spark UI 上 stage 10 的某个 task 运行 50 分钟,其余 task 只需 5 分钟。第一步定位:查看该 stage 各 task 的输入字节数,发现最大 task 的输入量是平均值的 15 倍;第二步根因分析:对 join 键做采样统计,发现一个秒杀商品 ID 占总记录的 85%;第三步实施优化:对该热点 key 加随机前缀拆成 10 个子 key,同时把小表复制 10 份副本与之配对,并开启 AQE 动态调整分区;第四步效果验证:整作业从 2 小时降到 22 分钟,长尾消失,集群峰值 CPU 下降 40%。

复盘的意义在于把偶然变成方法论。下次再遇到长尾任务,你就能像那位管理员一样,先看“包裹从哪来”,再决定“传送带怎么拆”——定位优先于蛮干,采样优于瞎猜。

η=Tbefore−TafterTbefore×100%\displaystyle \eta=\frac{T_{before}-T_{after}}{T_{before}}\times 100\%
优化效果用时长下降百分比 $\eta$ 衡量:$\eta$ 越大,说明治理动作越有效。
  1. 优化前耗时 $T_{before}$未治理时作业的总运行时长。
    Tbefore\displaystyle T_{before}
  2. 优化后耗时 $T_{after}$治理完成后同规模作业的运行时长。
    Tafter\displaystyle T_{after}
  3. 降幅比率 $\eta$下降时长占原时长的百分比,直观量化治理收益。
    η=Tbefore−TafterTbefore×100%\displaystyle \eta=\frac{T_{before}-T_{after}}{T_{before}}\times 100\%

AI 应用场景

电商大促离线数仓的 Join 倾斜治理

阶段:数据预处理与特征工程

解决的问题:大促期间商品订单表与商品维度表 join 时,秒杀商品 key 集中导致单个 reduce task 过载,离线训练样本生成链路每天超时,直接影响次日推荐模型更新。传统靠人工加机器的办法成本高且治标不治本,AI 平台的算法工程师需要稳定、及时的样本产出。

具体案例:某电商数据团队在每日凌晨的训练样本生成任务中发现,某爆款商品 ID 占订单表记录的 70%。他们先用采样统计识别热点 key,再对订单表热点 key 加 32 种随机前缀、将维度表对应商品行复制 32 份,使 join 负载均匀分散到所有分区;同时开启 Spark AQE 动态合并小分区。优化后样本任务从 3 小时缩短至 25 分钟,峰值内存占用下降 35%。

实时风控链路中的 Flink 热 Key 分流

阶段:实时推理与在线服务

解决的问题:风控系统需要实时聚合同一设备、同一用户的行为序列。某些头部用户(如刷单团伙的聚合 ID)行为量是普通用户的数百倍,导致单个 Flink 子任务成为瓶颈并引发反压,实时告警延迟从秒级恶化到分钟级。这类倾斜问题在流式 AI 特征计算中极其常见。

具体案例:某支付风控团队在 Flink 作业中按 device_id 聚合行为计数,发现有 3 个设备 ID 占全网 60% 的流量。他们在聚合算子前增加“局部加盐”逻辑:热点 device_id 加随机后缀分散到多个子任务做预聚合,再通过下游算子汇合。同时配置了基于 metrics 的热 key 自动告警,倾斜发生时自动调整盐值个数。治理后链路 P99 延迟从 8 秒降到 800 毫秒。

Python 演示

python47 行
import randomfrom collections import Counter  def hash_partition(key, n):    return sum(key.encode()) % n  def shuffle_join(keys, n_partitions=5):    sizes = [0] * n_partitions    for key in keys:        p = hash_partition(key, n_partitions)        sizes[p] += 1    return sizes  def salted_shuffle_join(keys, n_partitions=5, salt_bits=3):    sizes = [0] * n_partitions    hot_key = Counter(keys).most_common(1)[0][0]    salt_count = 1 << salt_bits    for key in keys:        if key == hot_key:            salt = random.randint(0, salt_count - 1)            p = hash_partition(f"{key}#{salt}", n_partitions)        else:            p = hash_partition(key, n_partitions)        sizes[p] += 1    return sizes, hot_key  def main():    random.seed(42)    hot = "hot_item"    keys = [hot] * 4000 + [f"normal_{i:04d}" for i in range(2000)]    random.shuffle(keys)     raw = shuffle_join(keys)    fixed, hot_key = salted_shuffle_join(keys)     print("raw    :", raw)    print("salted :", fixed)    peak_cut = max(raw) / max(fixed)    print(f"hot key: {hot_key}, peak cut {peak_cut:.1f}x")  if __name__ == "__main__":    main()
目的:模拟 4000 条热点 key 加 2000 条普通 key 的 join 分区负载,对比加盐前后最大分区的负载变化,直观展示数据倾斜治理的效果。
  1. def shuffle_join(keys, n_partitions=5):模拟未优化版本:所有 key 直接哈希分区,热点 key 所在的区域必然被撑爆。
  2. hash_partition(key, n)用 key 的字节求和取模作为分区号,模拟真实分布式系统中的哈希分区函数,同一个 key 永远落在同一分区。
  3. Counter(keys).most_common(1)[0][0]统计出现次数最多的 key,对应生产环境中“采样定位热点 key”的第一步。
  4. if key == hot_key:只对热点 key 做加盐,普通 key 保持原样,避免无谓的额外开销。
  5. salt = random.randint(0, salt_count - 1)为热点 key 生成随机盐值,使其均匀散落到 $2^{salt\_bits}$ 个不同的子 key 上。
  6. hash_partition(f"{key}#{salt}", n_partitions)带盐 key 重新哈希分区,热点被拆散到各个分区,峰值负载大幅下降。

常见误区

误区我以为作业变慢是因为数据总量太大,于是拼命加机器、加内存,结果作业还是慢。后来才知道,真正的问题是数据分布不均,而不是总量问题。

正解倾斜的本质是“少数 key 集中”而非“总量大”。就像分拣中心里上海方向爆仓,不是因为整个快件变多了,而是因为某个主播的订单都挤在同一条传送带上。加机器只能提升整体并行度,但热点 key 所在的那一个分区仍然是瓶颈,吃掉再多资源也白搭。先看分布,再谈扩容。

为何常见:因为从 Spark UI 上看到的只是“某 task 输入量特别大”,很容易让人以为是数据量的问题,忽略了是分布不均导致了这单个 task 变大。

误区我学会了加盐大法之后,无论遇到什么倾斜都往 key 上加随机前缀,结果有些作业反而更慢了,还有的聚合结果出现了偏差。

正解加盐的本质是把一个大 key 的负载分摊到多个分区,但下游必须先做“去盐+二次聚合”。如果原始操作是去重、排序或者需要全局精确顺序的,加盐不但无益,还会增加一层昂贵的合并开销。加盐只适用于“可结合、可交换”的聚合(如 sum、count、max),且要配合小表副本或下游汇合算子一起使用。它是一剂处方药,不是万能补品。

为何常见:因为网上案例多是加盐治倾斜的正面例子,初学者很容易把它当银弹,忽略了它的适用条件和额外代价。

误区我之前以为优化目标是把 Shuffle 完全干掉,于是到处找“无 shuffle”的写法,结果发现几乎不可行,还浪费了很多时间。

正解除了极少数纯 map 任务和广播 join 场景,绝大多数分布式作业至少需要一次 Shuffle——因为数据必须按 key 汇聚才能完成归约与关联。正确目标是把 Shuffle 的数据量减到最小、把 Shuffle 的次数降到最少,而不是消灭它。好比快递不可能永远专车直达,中央分拣的规模效应在多数场景下依然是必要的。

为何常见:因为优化文章常强调“shuffle 是万恶之源”,让人误解为必须彻底消除,忽略了 map 端聚合、广播 join 只是特定条件下的替代方案。

误区我以为把分区数从 200 调到 2000,热点就不会卡住单个任务了,结果作业不但没变快,调度开销反而暴涨。

正解分区数只影响并行粒度,如果热点 key 的哈希结果仍然指向同一个分区,它照样会簇拥在同一任务里;而盲目增多分区还会引入更多小任务、更多网络连接,调度与序列化成本直线上升。正确做法是先采样定位热点 key,再用加盐或 AQE 动态调整,让负载在空间上真正散开,而不是单纯增加“格子数量”。

为何常见:因为直观上“多分几个区,每个区就能少分点数据”,却忽略了哈希分区的确定性:同一个 key 无论总分区数多少,都会被分配到同一个逻辑分区。

面试题

Q1. Shuffle 阶段最主要的性能开销来自哪里?

单选题 · easy

  • A.CPU 浮点运算
  • B.网络与磁盘 IO← 正确答案
  • C.内存对象创建
  • D.JVM 垃圾回收

答案:B。Shuffle 的本质是数据搬运,每一条数据都要经历写磁盘、网络传输、远端读磁盘三次 IO,而 IO 速度远慢于 CPU 和内存。好比传送带运输包裹耗时主要在装卸和路上,而不是分拣员看一眼面单的时间。CPU 运算和垃圾回收虽然存在,但不是主力瓶颈。

Q2. 数据倾斜最典型的运行表现是什么?

单选题 · mid

  • A.所有任务运行时长同步上升
  • B.个别任务运行时间远超中位数形成长尾← 正确答案
  • C.集群整体磁盘空间被占满
  • D.Map 端输出数据全部丢失

答案:B。倾斜发生时,热点 key 所在的分区任务负载巨大,表现为“个别长尾任务拖垮整个作业”,而其他任务早已结束。A 错在倾斜是少数任务慢而非全部同步变慢;C、D 与倾斜没有必然关联。

Q3. 广播小表适用的前提条件是什么?

单选题 · easy

  • A.join 两侧表都是大表
  • B.一侧数据量很小且低于广播阈值← 正确答案
  • C.两侧分区数完全相等
  • D.join 键全部为空

答案:B。广播小表的前提是有一张表足够小,可拷贝到每个 Map 节点做本地 join。大表广播会撑爆内存;分区数相等与否与广播无关;join 键全空本身就是需要治理的倾斜问题,不是广播的适用前提。

Q4. 下列哪些手段可以有效缓解数据倾斜?

多选题 · mid

  • A.对热点 key 加随机前缀打散← 正确答案
  • B.两阶段聚合后再做全局合并← 正确答案
  • C.关闭所有 Shuffle
  • D.开启 Spark AQE 自动优化倾斜← 正确答案

答案:ABD。加盐打散与两阶段聚合是经典的“拆流+合流”组合,AQE 是 Spark 3 的自动倾斜治理机制;C 错误,Shuffle 在多数场景无法完全消除,只能减少。

Q5. Spark AQE 开启后不会执行下面哪一项?

单选题 · mid

  • A.动态合并过小的 shuffle 分区
  • B.动态切换为广播 join
  • C.自动把 RDD 缓存到内存← 正确答案
  • D.动态拆分倾斜分区

答案:C。AQE 的自适应能力包括动态合并分区、动态切换 join 策略、动态优化倾斜 join,但缓存 RDD 是执行策略的一部分,与 AQE 的自适应统计无关。好比调度台可以调整传送带数量和路线,但不会主动帮快递员整理货架。

Q6. Flink 流作业中数据倾斜最先引起的症状是什么?

单选题 · easy

  • A.个别子任务数据堆积并触发反压← 正确答案
  • B.所有算子打印大量错误日志
  • C.checkpoint 时间自动变短
  • D.状态后端自动扩容

答案:A。流式倾斜表现为某个 subtask 输入队列持续堆积,进而反压上游,这是第一个可观测信号。错误日志、checkpoint 变短、自动扩容都不是倾斜的直接特征。

Q7. 关于两阶段聚合的正确流程是什么?

多选题 · hard

  • A.直接按原始 key 做全局聚合
  • B.给 key 加盐打散后做局部聚合← 正确答案
  • C.去掉盐值后做全局聚合← 正确答案
  • D.聚合前清空所有历史状态

答案:BC。两阶段聚合第一步给 key 加盐,使其分布到多个分区做局部聚合;第二步去掉盐值还原真实 key,再做全局聚合。A 少了加盐步骤;D 属于错误操作,清空状态会丢数据。

Q8. 生产环境某作业一个 task 运行时长是中位数的 10 倍,最优的排查路径是?

单选题 · extreme

  • A.直接增加 executor 数量
  • B.采样统计 key 分布并对比各分区负载← 正确答案
  • C.调大 shuffle 分区数两倍
  • D.关闭 AQE 避免自动优化干扰

答案:B。长尾 task 的关键是先确认是不是热点 key 导致的分区负载不均,采样统计最直接。直接加资源或调分区数都只是盲猜,可能无效甚至恶化;关闭 AQE 是倒退,放弃自动优化。

Q9. 分区函数 p = hash(key) mod n 中,某个 key 出现一万次,会带来什么后果?

单选题 · easy

  • A.该 key 所在分区数据量激增形成热点← 正确答案
  • B.该 key 会被系统自动跳过
  • C.该分区的任务数自动减少
  • D.该分区数据会被强制压缩

答案:A。哈希分区是确定性的,同一个 key 永远进入同一分区,一万次出现全部堆积在同一个地方,形成热点分区。系统不会跳过、缩任务或压缩数据,只会默默让这个分区变慢。

Q10. 以下哪些措施可以有效减少 Shuffle 数据量?

多选题 · hard

  • A.在 Map 端做局部预聚合← 正确答案
  • B.使用广播 join 替代 reduce join← 正确答案
  • C.增大 reduce 端任务并发度
  • D.过滤掉不需要的字段后再 shuffle← 正确答案

答案:ABD。Map 端预聚合能把多条同 key 记录合并后再传输,广播小表避开 shuffle,字段裁剪让传输的字节数变少,这三者都直接减少 Shuffle 的 IO 量。C 增大并发只能让数据分散到更多任务,总量不变,无法减少 Shuffle 的数据量。