6.3 调优清单与实战手法 本节摘要:调优不是背参数,而是"计数器定位瓶颈——对号入座拧旋钮——数字验证收益"的闭环。本节先立三步诊断法,再按瓶颈类型给出五组旋钮清单(Combiner 与压缩、倾斜、小文件、Shuffle 内存、并行与推测),每组都标注前置判断条件与验证指标;随后完整推演加盐两阶段聚合——倾斜问题的标准解法;最后划出"该换引擎"的判断线,为第 7 章做引。 三步诊断法:先看数,再动手 作业慢的原因只有几个藏身之处:数据搬运太多(Shuffle 大)、干活不均(倾斜)、搬的次数太多(小文件与溢写轮数)、机器没用满(并行度低)、白等慢机器(无推测)。诊断路径收敛为三步: 第一步,看总账。
本节摘要:调优不是背参数,而是"计数器定位瓶颈——对号入座拧旋钮——数字验证收益"的闭环。本节先立三步诊断法,再按瓶颈类型给出五组旋钮清单(Combiner 与压缩、倾斜、小文件、Shuffle 内存、并行与推测),每组都标注前置判断条件与验证指标;随后完整推演加盐两阶段聚合——倾斜问题的标准解法;最后划出"该换引擎"的判断线,为第 7 章做引。
作业慢的原因只有几个藏身之处:数据搬运太多(Shuffle 大)、干活不均(倾斜)、搬的次数太多(小文件与溢写轮数)、机器没用满(并行度低)、白等慢机器(无推测)。诊断路径收敛为三步:
第一步,看总账。作业计数器里三条曲线定方向:MAP_OUTPUT_RECORDS 对 REDUCE_INPUT_RECORDS(中间量级)、SHUFFLE_BYTES(网络搬运量)、各 Reduce 任务的 REDUCE_INPUT_RECORDS 分布(均衡度)。
第二步,找最慢环节。作业页面上每个任务的耗时分解——map 的溢写轮数(SPILLED_RECORDS 与输出记录数之比大于 2 说明反复溢写)、reduce 的 copy 与 merge 耗时占比。
第三步,单旋钮验证。一次只改一个参数重跑,对比计数器与总时长;不回到这一步的调优都是玄学。
| 观察到 | 指向 | 去哪组 |
|---|---|---|
| SHUFFLE_BYTES 以 TB 计 | 中间数据太大 | 第一组 压缩与预聚合 |
| 一个 Reduce 记录数是同侪百倍 | 倾斜 | 第二组 加盐 |
| Map 任务数上千、每个几秒 | 小文件 | 第三组 合并与 Combine |
| SPILLED_RECORDS 远超输出 | 内存不足反复溢写 | 第四组 缓冲参数 |
| 作业尾部一台机器拖几十分钟 | 慢节点 | 第五组 并行与推测 |
优先级最高的一组,因为收益直接乘在所有后续环节上:
开 Combiner。前提是聚合函数满足结合律且业务容忍(求和、计数、最值可以;求平均不可以——除非改发"和与个数"的复合值)。验证指标:COMBINE_INPUT_RECORDS 与 COMBINE_OUTPUT_RECORDS 之比即本地压缩率。
job.setCombinerClass(IntSumReducer.class);
开中间压缩。map 输出落盘与网络传输都吃它,snappy 是速度与压缩率的平衡点:
<property><name>mapreduce.map.output.compress</name><value>true</value></property> <property><name>mapreduce.map.output.compress.codec</name> <value>org.apache.hadoop.io.compress.SnappyCodec</value></property>
减字段。中间键值只带下游需要的字段,宽表进 Shuffle 是常见浪费——2.3 节的结论在此兑现:能在 map 端扔掉的列,别让它上车。
4.2 节留下的悬案在此收束。设 60% 行的 city 为 NULL 或某热点值,哈希分区必然把这一坨塞给单个 Reduce。解法分两阶段:
第一阶段(打散):map 对热点键加随机后缀——原键 NULL 变 NULL_0 到 NULL_9 十个桶,热点被均匀摊到十个分区,reduce 对各桶分别聚出"局部和";
第二阶段(还原):小作业把 NULL_0 到 NULL_9 的十条局部和再聚一次,后缀剥掉,得到 NULL 的真实总和。
public static class SaltMapper extends Mapper<LongWritable, Text, Text, LongWritable> { private Random rnd = new Random(42); // 固定种子 保可复现 private Text outKey = new Text(); @Override protected void map(LongWritable k, Text line, Context context) throws IOException, InterruptedException { String[] c = line.toString().split(","); String city = c[1].isEmpty() ? "NULL" : c[1]; int salt = rnd.nextInt(10); outKey.set(city + "_" + salt); // 每行随机进桶 context.write(outKey, new LongWritable(Long.parseLong(c[2]))); } }
第二阶段的 map 把 _数字 后缀剥掉再聚:
public static class UnsaltMapper extends Mapper<LongWritable, Text, Text, LongWritable> { @Override protected void map(LongWritable k, Text line, Context context) throws IOException, InterruptedException { String[] c = line.toString().split("\t"); // 阶段一输出 键 制表 值 context.write(new Text(c[0].replaceAll("_\\d+$", "")), new LongWritable(Long.parseLong(c[1]))); } }
手工复算一遍正确性:设 NULL 组总额 60000、共 600 行,盐值随机均摊后每桶约 60 行;阶段一产出 NULL_0 约 5980、NULL_1 约 6120……十条局部和;阶段二求和 5980+6120+……,无论各桶如何分,总和恒等于 60000——加盐只改分组不改数值,加法交换律保底。适用条件也要记牢:只对可拆分的聚合(和、计数、最大)成立;求中位数、去重计数(需 HyperLogLog 类可合并结构)要另想办法。盐的桶数按热点占比定:热点占比 p、Reduce 数 R,桶数取约 p×R 的量级,让热点摊薄到与普通键同量级。
另一个轻量手法是两表 Join 时的维表放大:订单表热点键加 0 到 9 盐,users 表每行复制 10 份各带一个盐——两侧同样配对,reduce 里按盐匹配,热门键的回调被切成十次。
上万个小文件各生一个 Map 任务、每个几秒就结束,调度开销反而大于计算:
根治在存储:入湖前用 SequenceFile 合并——把许多小文件打包成少数几个大块容器,键存文件名、值存内容,Map 数立刻从上万落到几十;
临时缓解:用 CombineFileInputFormat 让一个 Map 任务消费多个小分片;
输出侧配套:Reduce 数别盲目调大(每个多写一份 part 文件),并用 LazyOutputFormat 杜绝空文件(4.3 节):
job.setInputFormatClass(CombineFileInputFormat.class); LazyOutputFormat.setOutputFormatClass(job, TextOutputFormat.class);
判断是否需要这组:Map 平均运行时长低于 30 秒、任务数上千,两个条件同时成立就该治。
SPILLED_RECORDS 超过输出记录两倍,说明缓冲区太小、反复溢写。两处可拧:
<property><name>mapreduce.task.io.sort.mb</name><value>256</value></property> <property><name>mapreduce.map.sort.spill.percent</name><value>0.90</value></property> <property><name>mapreduce.reduce.shuffle.parallelcopies</name><value>10</value></property> <property><name>mapreduce.task.io.sort.factor</name><value>25</value></property>
前三者分别管 Map 缓冲大小、溢写阈值、Reduce 拷贝并发;sort.factor 同时作用于 Map 端分段归并与 Reduce 端多路归并。拧这些的前提:任务 JVM 内存配额同步跟上,否则缓冲调大直接 OOM——mapreduce.map.java.opts 设为任务容器内存的八成左右是安全线。
SPECULATIVE_TASKS_REDUCTIONS_SAVED 是否大于零判断本集群赚不赚;mapreduce.map.failures.maxpercent,配合脏行计数器决定阈值(5.3 节)。| 手法 | 前置判断 | 验证指标 |
|---|---|---|
| Combiner | 聚合可结合 | 组合器进出记录比 |
| 中间压缩 | SHUFFLE_BYTES 大 | 同项下降比例 |
| 加盐两阶段 | 单键占比畸高 | 各 Reduce 记录数方差 |
| 小文件合并 | Map 数上千且短 | 任务数与总时长 |
| 缓冲调大 | 溢写轮数多 | SPILLED_RECORDS |
| 惰性输出 | 空文件多 | 输出文件计数 |
| 推测开关 | 长尾任务 | 最慢任务与均值比 |
每行都是"条件——动作——验证"三件套,没有验证指标的调优不进清单。
以下信号出现两条以上,继续调 MapReduce 多半是沉没成本:迭代计算(每轮作业落盘、五次搬运吃掉收益);交互式延迟要求(分钟级作业启动税无法消除);复杂多阶段管道(十几个串联作业,中间产物管理失控);流式需求(范式只有批没有流)。这些正是第 7 章要正面讨论的范式边界——知道旋钮拧到头在哪,与会拧旋钮同样重要。
三幕剧在此谢幕。下一章站到剧场外,问最后一个问题:这台机器的边界在哪,边界之外是谁的舞台。