跳到内容

第 7 卷 / 第 5 章 / 第 1 课

5.1 MapReduce:分片、Shuffle 与失败恢复 ​

分布式批计算把输入切成可并行任务,在节点失败时重跑确定性工作,并通过 Shuffle 把同一 key 的中间结果送到同一个 Reduce 分区。

MapReduce 的价值不只在两个函数,而在框架接管了调度、数据局部性、重试、排序和提交。

数据流 ​

text
Input splits
   ↓
Map tasks: (k1, v1) → [(k2, v2)]
   ↓ local spill / sort
Partition + Shuffle
   ↓
Reduce tasks: (k2, [v2]) → [(k3, v3)]
   ↓
Committed output

Map task 按输入 split 并行,split 是输入格式定义的逻辑工作单元,不必等于一个文件或存储 block。Partitioner 决定中间 key 去哪个 reducer;同一 key 必须路由到同一分区。Shuffle 涉及本地落盘、网络传输、归并和排序,通常是最昂贵阶段。

Combiner 需要代数性质 ​

Combiner 可以在 Map 端先聚合,减少网络字节,但框架可能不执行它,也可能执行多次,所以“有没有先合并”不能改变最终答案。操作应在输入输出类型上闭合,并满足能安全分组的结合/交换语义。

求和适合;直接求平均不适合,因为“各分片平均值再平均”会忽略样本数。正确做法是传 (sum, count),最终再除。例如分片 A 为 [0, 10]、B 为 [100],平均数再平均得到 (5 + 100) / 2 = 52.5,实际应为 110 / 3。局部输出 (10, 2) 与 (100, 1),合并成 (110, 3) 后才保留了权重。

这里的结合律是业务运算的要求;浮点加法受舍入影响,并非位级严格结合。需要可复现结果时,还要约定数值类型、精度和比较容差。

失败通过重跑恢复 ​

Worker 失败后,框架可以在其他节点重跑 Map/Reduce task。前提是任务对同一输入可重复执行,并且外部副作用受控制。

若 Mapper 直接调用外部支付或发送邮件,task 重试会重复副作用。分布式计算应写受框架提交协议管理的输出,外部效果再通过幂等消费处理。

投机执行可为慢 task 启动另一个 attempt,由提交协议选择一个成功 attempt 的输出并取消其他尝试。它能缓解偶发 straggler,却不能消除热 key 导致的固有大分区;多个副本一起算同一份巨量输入,只会让它更热闹。外部副作用仍可能重复发生。

Output commit 避免半成品 ​

Task 通常先写临时输出,由提交协议发布一个成功 attempt 的结果。但“每个 task 只发布一个结果”不等于“整个 job 的所有分区原子出现”。消费者应使用引擎约定的完成标记、manifest 或表事务。一个通用做法是每次运行写独立版本目录,全部成功后再切换读者使用的版本指针;失败的目录不成为当前版本。

对象存储上的 rename 语义与文件系统不同,具体引擎需要 manifest、commit protocol 或原子元数据切换。不能假设所有存储的目录重命名都廉价且原子。

数据倾斜决定长尾 ​

平均每分区 10 GB 不代表每个分区接近 10 GB。热门 key 可能把一个 reducer 推到数百 GB,让其余节点全部等待。

缓解方式:

  • 用采样估计 key 分布;
  • 对可拆分热点 key 加盐并二阶段聚合;
  • 广播足够小的维表,避免大 shuffle;
  • 自定义 partitioner;
  • 在业务允许时单独处理超级 key。

加盐必须配套还原 key 的第二阶段:先按 (原 key, salt) 求局部 (sum, count),再去掉 salt 合并。只加盐不再聚合,会把同一个业务结果拆成多行。MapReduce 的分区内排序也不等于所有输出分区天然全局有序。

容量与正确性指标 ​

  • 输入/输出字节与记录数;
  • shuffle read/write、spill 和网络时间;
  • task 分布、p50/p99 时长与 straggler;
  • 重试、失败和投机 attempt;
  • 数据倾斜和最大分区;
  • 输出计数、校验和及业务不变量。

下一课进入 DAG 引擎,理解 Spark 如何通过 lineage、窄/宽依赖和结构化计划优化多阶段计算。

参考资料 ​

Built with VitePress | Software Systems Atlas