2.1 输入分片机制 本节摘要:输入分片是 MapReduce 第一幕的核心概念——它是逻辑计算单位,与 HDFS 的物理存储单位"块"相互独立又彼此对齐。分片数决定 Map 任务数,分片大小由目标尺寸、块大小与剩余长度三者推导。本节给出完整推导、小文件问题的账本与治理手段。 先算一道题 一个 500MB 的文本文件存进 HDFS(块大小 128MB),会被切成 4 个块(128+128+128+116)。现在提交一个 MapReduce 作业处理它,Map 任务有几个? 直觉答案是 4,实际也是 4——但这只是巧合于"分片默认等于块大小"的配置。若把分片目标尺寸调成 200MB,答案变成 3;调成 50MB,答案变成 10。
本节摘要:输入分片是 MapReduce 第一幕的核心概念——它是逻辑计算单位,与 HDFS 的物理存储单位"块"相互独立又彼此对齐。分片数决定 Map 任务数,分片大小由目标尺寸、块大小与剩余长度三者推导。本节给出完整推导、小文件问题的账本与治理手段。
一个 500MB 的文本文件存进 HDFS(块大小 128MB),会被切成 4 个块(128+128+128+116)。现在提交一个 MapReduce 作业处理它,Map 任务有几个?
直觉答案是 4,实际也是 4——但这只是巧合于"分片默认等于块大小"的配置。若把分片目标尺寸调成 200MB,答案变成 3;调成 50MB,答案变成 10。Map 任务数不是由文件或块直接决定的,而是由分片规划决定的,而分片规划是一段纯逻辑代码,允许你干预。
这道题算完,第一幕的机制就露出了骨架。
两个单位名字都带"切",但服务对象完全不同:
| 维度 | 块 Block | 分片 Split |
|---|---|---|
| 归属 | HDFS 存储层 | MapReduce 计算层 |
| 性质 | 物理切块,实际占有磁盘 | 逻辑区间,仅记录起始偏移与长度 |
| 大小 | 固定,由集群配置(常见 128MB) | 可调,由作业配置决定 |
| 是否真的切文件 | 是 | 否,只是元数据 |
| 数量关系 | 与分片无强制绑定,默认对齐 | 决定 Map 任务数 |
分片在代码里的样子就是一个记录"文件名 + 起始字节 + 长度"的三元组。框架拿到分片列表后,为每个分片启动一个 Map 任务,任务的 RecordReader 从起始字节读到长度用尽为止。这就是"分片数等于 Map 任务数"的全部机制。
Hadoop 对每个文件按以下规则规划分片(以 FileInputFormat 为准):
goalSize = 文件总长 / 期望分片数(由 mapreduce.input.fileinputformat.split.minsize 等配置间接影响) minSize = 最小分片尺寸(默认 1,调大可强制分片更大) blockSize = 文件所在块大小(默认 128MB) splitSize = max minSize 与 min goalSize 与 blockSize
然后从文件头开始,每次切下 splitSize 字节,最后不足 splitSize 的 1.1 倍的剩余部分并入最后一个分片(这个"尾巴合并"规则避免了产生一个只处理几字节的迷你任务)。
拿开头的例子验算:文件 500MB,minSize=1,blockSize=128MB。

每个 Map 任务有固定开销:调度、启动 JVM、初始化输入格式,加起来秒级。设固定开销 10 秒、每 MB 纯计算 0.1 秒,比较三种规划处理 500MB 的总时延(假设机器足够多,任务全部并行,取最慢任务近似):
| splitSize | 任务数 | 单任务计算 | 单任务开销 | 最慢任务耗时 |
|---|---|---|---|---|
| 200MB | 2 | 30 秒 | 10 秒 | 40 秒 |
| 128MB | 4 | 约 17.8 秒 | 10 秒 | 27.8 秒 |
| 50MB | 10 | 5 秒 | 10 秒 | 15 秒 |
| 5MB | 100 | 0.5 秒 | 10 秒 | 10.5 秒但调度排队时间飙升 |
下探到 5MB 时,账面最慢任务 10.5 秒,但 100 个任务的排队与调度本身会侵占集群,集群小的时候反而更慢;再小,任务开销占比超过一半,纯浪费。实践中把 Map 任务平均时长控制在分钟级、任务总数与可用槽位同量级,是常见经验。
什么时候调大:每个记录处理极轻(如纯过滤),任务数过多时合并分片减少固定开销。什么时候调小:单个记录处理很重(复杂解析、外部查询),需要更高并行度摊薄长尾。
块边界不会对齐行边界。一行日志很可能前半段在块 3、后半段在块 4。分片规划对此的答案很优雅:分片的结束边界允许越过。每个分片读到最后会多读下一个字节,若发现停在一行中间,就继续读完这一行;相应地,下一个分片会先跳过开头那段不完整的行,从完整行起点开始读。每个逻辑记录恰好被处理一次。
这层机制藏在 LineRecordReader 里,用户无感,但面试常考"行跨块会不会被处理两次或漏掉"——答案是不会,靠的正是读取端的越界与跳过配合。
分片机制的前提是"文件够大"。一堆小文件是它的天敌:
治理思路有三条:
// 使用 CombineFileInputFormat 治理小文件的核心配置 job.setInputFormatClass(CombineFileInputFormat.class); // 设定一个分片最多打包的数据量 如 128MB CombineFileInputFormat.setMaxSplitSize(job, 134217728); // 设定一个分片最多包含的路径数 防止元数据扫描过慢 CombineFileInputFormat.setMinSplitSizeNode(job, 10485760);
分片决定了"谁在哪块数据上开工",下一节看框架如何让任务尽量在数据所在地开机。