InputSplit
InputSplit
是对 MapReduce 作业输入数据的一种逻辑划分。它并不直接包含数据本身,而是包含了处理一小块数据所需的信息,比如数据的位置、起始点和长度。框架会为每个 InputSplit
创建一个 Mapper
任务。
从InputSplit.java
的注释中我们可以得到核心定义:
InputSplit
represents the data to be processed by an individualMapper
. (InputSplit
代表了将被单个Mapper
处理的数据。)
它与 InputFormat
和 RecordReader
的关系如下:
InputFormat
: 负责验证作业的输入规范,并将输入数据切分 成多个逻辑上的InputSplit
。InputSplit
: 定义了一个Mapper
任务要处理的数据分片。它是一个对数据的引用,而不是数据本身。RecordReader
: 负责从InputSplit
所指向的存储中读取数据,并将其解析成<key, value>
键值对,然后喂给Mapper
。
这个流程可以在 MapReduceTutorial.md
中找到清晰的描述:
java
// ... existing code ...
The MapReduce framework relies on the `InputFormat` of the job to:
1. Validate the input-specification of the job.
2. Split-up the input file(s) into logical `InputSplit` instances,
each of which is then assigned to an individual `Mapper`.
3. Provide the `RecordReader` implementation used to glean input
records from the logical `InputSplit` for processing by the
`Mapper`.
// ... existing code ...
InputSplit
抽象类分析
InputSplit
本身是一个抽象类,它定义了所有 "分片" 都必须遵守的契约。让我们看看它的核心方法:
java
@InterfaceAudience.Public
@InterfaceStability.Stable
public abstract class InputSplit {
/**
* Get the size of the split, so that the input splits can be sorted by size.
* @return the number of bytes in the split
* @throws IOException
* @throws InterruptedException
*/
public abstract long getLength() throws IOException, InterruptedException;
/**
* Get the list of nodes by name where the data for the split would be local.
* The locations do not need to be serialized.
*
* @return a new array of the node nodes.
* @throws IOException
* @throws InterruptedException
*/
public abstract
String[] getLocations() throws IOException, InterruptedException;
/**
* Gets info about which nodes the input split is stored on and how it is
* stored at each location.
*
* @return list of <code>SplitLocationInfo</code>s describing how the split
* data is stored at each location. A null value indicates that all the
* locations have the data stored on disk.
* @throws IOException
*/
@Evolving
public SplitLocationInfo[] getLocationInfo() throws IOException {
return null;
}
}
-
public abstract long getLength()
: 这个方法返回分片的逻辑大小(以字节为单位)。这个返回值非常重要,框架可以用它来对分片进行排序(例如,优先处理大的分片),或者用于推测任务的进度。 -
public abstract String[] getLocations()
: 这是实现**数据本地性(Data Locality)**的核心。它返回一个字符串数组,其中包含了存储这份数据分片的节点的 主机名/IP地址。MapReduce 的调度器会尽量将Mapper
任务调度到getLocations()
返回的节点之一上运行,这样Mapper
就可以从本地磁盘读取数据,避免了昂贵的网络I/O。 -
public SplitLocationInfo[] getLocationInfo()
: 这是一个更现代、信息更丰富的getLocations()
版本。它不仅能返回位置信息,还能告知数据在该位置的存储状态(例如,是在内存中还是在磁盘上)。这使得调度器可以做出更智能的决策,比如优先将任务调度到数据已在内存中的节点。
InputSplit
如何创建和使用?
开发者通常不直接创建 InputSplit
。这个工作由 InputFormat
的 getSplits()
方法完成。以最常见的 FileInputFormat
为例,它负责将输入文件切分成多个 FileSplit
。
FileInputFormat.java
中的 getSplits
方法展示了其核心逻辑:
FileInputFormat.java
java
// ... existing code ...
/**
* Generate the list of files and make them into FileSplits.
* @param job the job context
* @throws IOException
*/
public List<InputSplit> getSplits(JobContext job) throws IOException {
StopWatch sw = new StopWatch().start();
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
long maxSize = getMaxSplitSize(job);
// generate splits
List<InputSplit> splits = new ArrayList<InputSplit>();
List<FileStatus> files = listStatus(job);
// ... existing code ...
for (FileStatus file: files) {
// ... existing code ...
Path path = file.getPath();
long length = file.getLen();
if (length != 0) {
BlockLocation[] blkLocations;
if (file instanceof LocatedFileStatus) {
blkLocations = ((LocatedFileStatus) file).getBlockLocations();
} else {
FileSystem fs = path.getFileSystem(job.getConfiguration());
blkLocations = fs.getFileBlockLocations(file, 0, length);
}
if (isSplitable(job, path)) {
long blockSize = file.getBlockSize();
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
long bytesRemaining = length;
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
splits.add(makeSplit(path, length-bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
bytesRemaining -= splitSize;
}
// ... existing code ...
} else { // not splittable
// ... existing code ...
splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts(),
blkLocations[0].getCachedHosts()));
}
} else {
//Create empty hosts array for zero length files
splits.add(makeSplit(path, 0, length, new String[0]));
}
}
// ... existing code ...
return splits;
}
// ... existing code ...
这个过程大致如下:
- 获取输入路径下的所有文件。
- 计算分片大小
splitSize
,它通常与 HDFS 的块大小(Block Size)有关。 - 遍历每个文件,如果文件是可切分的(例如,未压缩的文本文件),则根据
splitSize
将其切成一个或多个InputSplit
。 - 在创建
InputSplit
时,会从BlockLocation
中获取数据块所在的节点信息,并传递给InputSplit
,这就是getLocations()
的数据来源。
InputSplit
的主要实现
由于 InputSplit
是抽象的,我们需要看它的具体实现才能更好地理解。
-
FileSplit
: 最常用的一种实现,代表一个文件的一部分。它由文件路径、起始偏移量和长度定义。java// ... existing code ... /** The file containing this split's data. */ public Path getPath() { return fs.getPath(); } /** The position of the first byte in the file to process. */ public long getStart() { return fs.getStart(); } /** The number of bytes in the file to process. */ public long getLength() { return fs.getLength(); } // ... existing code ... public String[] getLocations() throws IOException { return fs.getLocations(); } // ... existing code ...
-
CombineFileSplit
: 用于将多个小文件或小文件块合并成一个单独的InputSplit
。这对于处理大量小文件的场景非常高效,因为它避免了为每个小文件启动一个Mapper
任务所带来的开销。 在TestCombineTextInputFormat.java
测试中,我们可以看到它被明确使用:java// ... existing code ... // we should have a single split as the length is comfortably smaller than // the block size assertEquals(1, splits.size(), "We got more than one splits!"); InputSplit split = splits.get(0); assertEquals(CombineFileSplit.class, split.getClass(), "It should be CombineFileSplit"); // ... existing code ...
-
TaggedInputSplit
: 当使用MultipleInputs
功能,即针对不同的输入路径使用不同的InputFormat
或Mapper
时,TaggedInputSplit
就派上用场了。它包装了一个原始的InputSplit
,并额外 "标记" 了应该由哪个InputFormat
和Mapper
类来处理它。java// ... existing code ... public class TaggedInputSplit extends InputSplit implements Configurable, Writable { private Class<? extends InputSplit> inputSplitClass; private InputSplit inputSplit; private Class<? extends InputFormat<?, ?>> inputFormatClass; private Class<? extends Mapper<?, ?, ?, ?>> mapperClass; private Configuration conf; // ... existing code ...
-
CompositeInputSplit
: 用于JOIN
操作。它本身是InputSplit
的一个集合,代表了需要连接的多个数据源的相应分片。它的getLocations()
方法会聚合所有子分片的位置信息。java// ... existing code ... /** * Collect a set of hosts from all child InputSplits. */ public String[] getLocations() throws IOException, InterruptedException { HashSet<String> hosts = new HashSet<String>(); for (InputSplit s : splits) { String[] hints = s.getLocations(); if (hints != null && hints.length > 0) { for (String host : hints) { hosts.add(host); } } } return hosts.toArray(new String[hosts.size()]); } // ... existing code ...
总结
InputSplit
是 MapReduce 并行计算模型的基础。它通过将大数据集逻辑地切分为可管理的小数据块,使得成百上千的 Mapper
任务可以并行处理。同时,通过 getLocations()
方法携带的位置信息,框架能够将计算任务移动到数据所在的位置,从而实现了"计算向数据移动"的核心思想,极大地提升了处理效率。
FileInputFormat.getSplits
方法的整体逻辑
这个方法的核心目标是将输入的文件分割成多个 InputSplit
(输入分片),每个 InputSplit
会作为一个 Map 任务的输入。理想情况下,分片的数量决定了 Map 任务的并行度。
下面是 getSplits
方法的执行流程的详细分解:
java
// ... existing code ...
public List<InputSplit> getSplits(JobContext job) throws IOException {
StopWatch sw = new StopWatch().start();
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
long maxSize = getMaxSplitSize(job);
// generate splits
List<InputSplit> splits = new ArrayList<InputSplit>();
List<FileStatus> files = listStatus(job);
boolean ignoreDirs = !getInputDirRecursive(job)
&& job.getConfiguration().getBoolean(INPUT_DIR_NONRECURSIVE_IGNORE_SUBDIRS, false);
for (FileStatus file: files) {
// ... existing code ...
if (length != 0) {
// ... existing code ...
if (isSplitable(job, path)) {
long blockSize = file.getBlockSize();
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
long bytesRemaining = length;
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
splits.add(makeSplit(path, length-bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
bytesRemaining -= splitSize;
}
if (bytesRemaining != 0) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
}
} else { // not splitable
// ... existing code ...
splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts(),
blkLocations[0].getCachedHosts()));
}
} else {
//Create empty hosts array for zero length files
splits.add(makeSplit(path, 0, length, new String[0]));
}
}
// ... existing code ...
return splits;
}
protected long computeSplitSize(long blockSize, long minSize,
long maxSize) {
return Math.max(minSize, Math.min(maxSize, blockSize));
}
// ... existing code ...
1. 计算分片大小(Split Size)
方法的第一步是确定每个分片的大小范围。这由三个值决定:
-
minSize
(最小分片大小):- 代码:
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
- 它取
getFormatMinSplitSize()
(默认为 1) 和getMinSplitSize(job)
的最大值。 getMinSplitSize(job)
读取 Hadoop 的配置参数mapreduce.input.fileinputformat.split.minsize
(旧参数为mapred.min.split.size
)。- 作用: 保证每个分片不会小于这个值。
- 代码:
-
maxSize
(最大分片大小):- 代码:
long maxSize = getMaxSplitSize(job);
- 它读取 Hadoop 的配置参数
mapreduce.input.fileinputformat.split.maxsize
(旧参数为mapred.max.split.size
)。 - 作用: 保证每个分片不会大于这个值。
- 代码:
-
blockSize
(HDFS 块大小):- 代码:
long blockSize = file.getBlockSize();
- 这是文件存储在 HDFS 上的块大小,由 HDFS 的配置
dfs.blocksize
决定。
- 代码:
最终的分片大小 splitSize
通过 computeSplitSize
方法计算得出: splitSize = Math.max(minSize, Math.min(maxSize, blockSize))
2. 遍历所有输入文件
代码会获取输入路径下的所有文件 (List<FileStatus> files = listStatus(job);
),然后逐一处理。
3. 处理单个文件
对于每个文件,程序会检查它是否可以被切分 (isSplitable(job, path)
)。
-
可切分的文件 (Splittable):
- 大多数普通文本文件都是可切分的。
- 程序会根据计算出的
splitSize
对文件进行逻辑切分。 - 它会循环创建大小为
splitSize
的分片,直到剩余的字节数不足一个splitSize
的 1.1 倍 (SPLIT_SLOP
)。 - 最后剩余的部分会形成最后一个分片。
- 关键点: 这里的切分是逻辑上的。一个分片可能跨越多个 HDFS 块,也可能一个 HDFS 块包含多个分片。
-
不可切分的文件 (Not Splittable):
- 某些压缩格式(如 Gzip)是无法从中间读取的,因此它们是不可切分的。
- 对于这类文件,整个文件会形成一个单独的分片,无论它有多大。这会导致该文件只能由一个 Map 任务处理,可能成为性能瓶颈。
4. 创建分片 (makeSplit
)
makeSplit
方法会创建一个 FileSplit
对象,它包含了分片所需的所有信息:文件路径、起始位置、长度以及数据所在的节点位置(hosts
),以便 MapReduce 框架实现"数据本地性" (Data Locality),即尽量将计算任务调度到数据所在的节点上执行,减少网络传输。
如何通过 Hadoop 参数控制 Split 数量?
理解了上面的流程,我们就可以知道如何通过调整参数来控制分片的数量了。核心就是控制 computeSplitSize
的计算结果。
splitSize = Math.max(minSize, Math.min(maxSize, blockSize))
让我们来分析不同场景:
-
默认情况 (最常见)
mapreduce.input.fileinputformat.split.minsize
(minSize) 默认为 1。mapreduce.input.fileinputformat.split.maxsize
(maxSize) 默认为Long.MAX_VALUE
(一个非常大的数)。- 此时公式变为:
splitSize = Math.max(1, Math.min(Long.MAX_VALUE, blockSize))
- 简化后得到:
splitSize = blockSize
- 结论 : 在默认情况下,分片大小就等于 HDFS 的块大小。一个 1GB 的文件,如果块大小是 128MB,那么它会被切分为
1024 / 128 = 8
个分片(大约值)。
-
增加分片数量 (调小
maxSize
)- 目的: 增加 Map 任务的并行度,适用于 CPU 密集型任务。
- 方法 : 设置
mapreduce.input.fileinputformat.split.maxsize
为一个小于blockSize
的值。 - 示例 :
blockSize
= 128MB。如果你设置maxSize
= 64MB。 - 公式变为:
splitSize = Math.max(1, Math.min(64MB, 128MB))
- 简化后得到:
splitSize = 64MB
- 结论: 每个 128MB 的块都会被切分为 2 个 64MB 的分片,分片的总数大约会翻倍。
-
减少分片数量 (调大
minSize
)- 目的: 减少 Map 任务的数量,适用于处理大量小文件或者希望合并处理逻辑的场景。
- 方法 : 设置
mapreduce.input.fileinputformat.split.minsize
为一个大于blockSize
的值。 - 示例 :
blockSize
= 128MB。如果你设置minSize
= 256MB。 - 公式变为:
splitSize = Math.max(256MB, Math.min(Long.MAX_VALUE, 128MB))
- 简化后得到:
splitSize = Math.max(256MB, 128MB)
->splitSize = 256MB
- 结论: 程序会尝试创建 256MB 大小的分片,这会将两个 128MB 的 HDFS 块合并到一个分片中,从而使分片总数减少一半。
总结
参数 | 默认值 | 作用 | 如何影响分片数 |
---|---|---|---|
mapreduce.input.fileinputformat.split.minsize | 1 | 设置分片的最小尺寸 | 调大此值(大于blockSize)可以减少分片数。 |
mapreduce.input.fileinputformat.split.maxsize | Long.MAX_VALUE | 设置分片的最大尺寸 | 调小此值(小于blockSize)可以增加分片数。 |
dfs.blocksize | (HDFS配置, 如128MB) | HDFS块大小 | 在默认情况下,分片大小等于块大小,是决定分片数的基础。 |
通过灵活配置这三个参数,你可以精确地控制 MapReduce 作业的并行度,以适应不同的业务场景和硬件资源,从而达到优化作业性能的目的。
CombineFileInputFormat 如何实现文件合并逻辑的
在 org.apache.hadoop.mapred.lib.CombineFileInputFormat
类中,getSplits
方法的实现如下:
java
public InputSplit[] getSplits(JobConf
job, int numSplits)
throws IOException {
List<org.apache.hadoop.mapreduce.
InputSplit> newStyleSplits =
super.getSplits(Job.getInstance(job));
InputSplit[] ret = new InputSplit
[newStyleSplits.size()];
for(int pos = 0; pos < newStyleSplits.
size(); ++pos) {
org.apache.hadoop.mapreduce.lib.input.
CombineFileSplit newStyleSplit =
(org.apache.hadoop.mapreduce.lib.
input.CombineFileSplit)
newStyleSplits.get(pos);
ret[pos] = new CombineFileSplit(job,
newStyleSplit.getPaths(),
newStyleSplit.getStartOffsets(),
newStyleSplit.getLengths(),
newStyleSplit.getLocations());
}
return ret;
}
这个方法实际上是调用了父类 org.apache.hadoop.mapreduce.lib.input.CombineFileInputFormat
的 getSplits
方法,然后将返回的 org.apache.hadoop.mapreduce.InputSplit
列表转换为 org.apache.hadoop.mapred.InputSplit
数组。
父类 org.apache.hadoop.mapreduce.lib.input.CombineFileInputFormat
的 getSplits
方法实现了 文件合并逻辑:
-
获取文件信息 :
-
通过
listStatus
方法获取输入路径下的所有文件信息。 -
对于每个文件,创建
OneFileInfo
对象,该对象会获取文件的块信息及其所在的主机和机架信息。
-
-
处理文件池 :
- 如果配置了文件池(pools),则先处理每个池中的文件,确保同一分片中的文件来自同一池。
-
生成分片 :
-
调用
getMoreSplits
方法处理具体的分片生成逻辑。 -
在
getMoreSplits
方法中,为每个文件创建OneFileInfo
对象,并调用createSplits
方法。
-
-
文件合并逻辑 (
createSplits
方法) :-
节点级别合并 :
-
优先在同一节点上合并块,以提高数据本地性。
-
遍历所有节点,对于每个节点上的块,累加它们的大小。
-
如果累加的大小超过
maxSplitSize
,则调用addCreatedSplit
方法创建一个分片。 -
如果剩余块大小超过
minSplitSizeNode
,也会创建一个分片。
-
-
机架级别合并 :
-
对于无法在节点级别合并的块,尝试在机架级别进行合并。
-
遍历所有机架,对于每个机架上的块,累加它们的大小。
-
如果累加的大小超过
minSplitSizeRack
,则创建一个分片。
-
-
处理剩余块 :
- 最后处理所有剩余的块,将它们合并成最后的分片。
-
-
创建分片 :
addCreatedSplit
方法会创建CombineFileSplit
对象,该对象包含了合并后的文件信息(路径、起始偏移量、长度等)。
通过这种方式,getSplits
方法实现了我们之前描述的文件合并逻辑,将大量小文件合并成较大的分片,以减少 Map 任务的数量,提高 MapReduce 作业的执行效率。
CombineFileInputFormat 中 getSplits 方法的实现分析
getSplits
方法是 CombineFileInputFormat
类的核心,负责将多个小文件合并成较少的输入分片,以减少 Map 任务的数量,提高作业执行效率。
首先,该方法从作业配置中获取分片大小的相关参数:
-
minSizeNode
: 节点级别的最小分片大小 -
minSizeRack
: 机架级别的最小分片大小 -
maxSize
: 分片的最大大小
这些参数可以通过 set 方法显式设置,或者从配置中读取。方法还会进行参数验证,确保它们之间的关系合理。
分片生成 (getMoreSplits
方法)
getMoreSplits
方法是分片生成的核心:
-
创建 OneFileInfo 对象:
为每个文件创建
OneFileInfo
对象,该对象包含文件的所有块信息。javafiles[i] = new OneFileInfo(stat, conf, isSplitable(job, stat.getPath()), rackToBlocks, blockToNodes, nodeToBlocks, rackToNodes, maxSize);
-
文件块处理:
在
OneFileInfo
构造函数中,根据文件是否可分割,将文件拆分成多个块 (OneBlockInfo
)。-
不可分割文件: 创建一个包含整个文件的块
-
可分割文件: 根据
maxSize
参数将文件拆分成多个块,使用启发式方法避免创建过小的块
-
-
构建映射关系:
通过
populateBlockInfo
方法建立块与节点、机架之间的映射关系。 -
调用 createSplits:
使用建立的映射关系创建实际的分片。
分片创建 (createSplits
方法)
createSplits
方法实现了基于节点和机架的分片合并策略:
-
节点级合并:
-
遍历所有节点,尝试将同一节点上的块合并成一个分片
-
当累积大小达到
maxSize
时,创建一个分片 -
如果剩余块大小超过
minSizeNode
,也创建一个分片;否则将这些块放回待处理池
-
-
机架级合并:
-
处理节点级合并后剩余的块
-
尝试将同一机架上的块合并成一个分片
-
当累积大小达到
maxSize
时,创建一个分片 -
如果剩余块大小超过
minSizeRack
,也创建一个分片;否则放入overflowBlocks
-
-
溢出块处理:
-
处理机架级合并后剩余的块 (
overflowBlocks
) -
继续累积直到达到
maxSize
,创建分片 -
处理最后剩余的块
-
populateBlockInfo建立的映射关系及Hadoop支持
populateBlockInfo 方法建立了以下三种关键的映射关系,这些关系是实现文件合并策略的基础:
-
- 块到节点的映射 (blockToNodes)
java// add this block to the block --> node locations map blockToNodes.put(oneblock, oneblock. hosts);
这个映射关系记录了每个块(OneBlockInfo)所在的节点(hosts)列表,用于实现节点级别的数据本地性优化。
-
- 机架到块的映射 (rackToBlocks)
java// add this block to the rack --> block map for (int j = 0; j < racks.length; j++) { String rack = racks[j]; List<OneBlockInfo> blklist = rackToBlocks.get(rack); if (blklist == null) { blklist = new ArrayList<OneBlockInfo> (); rackToBlocks.put(rack, blklist); } blklist.add(oneblock); }
这个映射关系将每个机架(rack)与位于该机架上的所有块关联起来,用于实现机架级别的数据本地性优化。
-
- 节点到块的映射 (nodeToBlocks)
java// add this block to the node --> block map for (int j = 0; j < oneblock.hosts. length; j++) { String node = oneblock.hosts[j]; Set<OneBlockInfo> blklist = nodeToBlocks.get(node); if (blklist == null) { blklist = new LinkedHashSet<OneBlockInfo>(); nodeToBlocks.put(node, blklist); } blklist.add(oneblock); }
这个映射关系记录了每个节点(node)上存储的所有块,用于在创建分片时快速查找特定节点上的所有块。
-
- 机架到节点的映射 (rackToNodes)
javaif (!racks[j].equals(NetworkTopology. DEFAULT_RACK)) { // Add this host to rackToNodes map addHostToRack(rackToNodes, racks[j], oneblock.hosts[j]); }
这个映射关系记录了每个机架上包含的节点列表,用于在机架级别合并时确定分片的位置信息。
Hadoop提供的支持信息
Hadoop通过以下机制为这些映射关系的建立提供了必要的信息支持:
-
- BlockLocation API 在 OneFileInfo 构造函数中,通过调用 getFileBlockLocations 方法获取文件块的位置信息:
javaBlockLocation[] locations; if (stat instanceof LocatedFileStatus) { locations = ((LocatedFileStatus) stat). getBlockLocations(); } else { FileSystem fs = stat.getPath(). getFileSystem(conf); locations = fs.getFileBlockLocations (stat, 0, stat.getLen()); }
BlockLocation 类提供了以下关键信息:
-
getHosts() : 返回存储该块的所有节点的主机名
-
getTopologyPaths() : 返回节点的网络拓扑路径,包含机架信息
-
- 网络拓扑结构 Hadoop使用网络拓扑来组织集群中的节点,通过 NetworkTopology 类管理机架信息。在 OneBlockInfo 构造函数中,如果文件系统没有提供机架信息,则会使用默认机架:
java// if the file system does not have any rack information, then // use dummy rack location. if (topologyPaths.length == 0) { topologyPaths = new String[hosts.length]; for (int i = 0; i < topologyPaths. length; i++) { topologyPaths[i] = (new NodeBase (hosts[i], NetworkTopology. DEFAULT_RACK)). toString(); } } // The topology paths have the host name included as the last // component. Strip it. this.racks = new String[topologyPaths.length]; for (int i = 0; i < topologyPaths.length; i++) { this.racks[i] = (new NodeBase (topologyPaths[i])).getNetworkLocation (); }
通过这些机制,Hadoop能够提供详细的块位置信息,包括块存储在哪些节点上以及这些节点属于哪个机架,从而支持CombineFileInputFormat实现高效的数据本地性优化和文件合并策略。
InputFormat
InputFormat
描述了 MapReduce 作业的输入规范。它像一个数据处理的"总管",负责在作业开始时,告诉框架数据在哪里、如何划分,以及怎样逐条读取。
InputFormat.java
的注释中,清晰地定义了它的三大核心职责:
java
// ... existing code ...
* <p>The Map-Reduce framework relies on the <code>InputFormat</code> of the
* job to:<p>
* <ol>
* <li>
* Validate the input-specification of the job.
* <li>
* Split-up the input file(s) into logical {@link InputSplit}s, each of
* which is then assigned to an individual {@link Mapper}.
* </li>
* <li>
* Provide the {@link RecordReader} implementation to be used to glean
* input records from the logical <code>InputSplit</code> for processing by
* the {@link Mapper}.
* </li>
* </ol>
// ... existing code ...
- 验证输入规范 (Validate the input-specification):在作业运行前,检查输入配置是否合法。例如,对于文件输入,它会检查指定的输入路径是否存在。
- 切分输入 (Split-up the input) :这是
InputFormat
最核心的功能。它将输入数据源(如一个或多个大文件)逻辑上切分成多个InputSplit
。每个InputSplit
将由一个单独的Mapper
任务来处理,这是实现并行计算的基础。 - 提供
RecordReader
(Provide the RecordReader) :InputFormat
本身不读取数据。它会创建一个RecordReader
的实例。RecordReader
才是真正负责从InputSplit
指向的数据源中读取数据,并将其解析成<key, value>
键值对,然后传递给Mapper
。
InputFormat
抽象方法分析
InputFormat
是一个抽象类,它定义了所有子类必须实现的两个核心方法:
java
// ... existing code ...
public abstract class InputFormat<K, V> {
/**
* Logically split the set of input files for the job.
* ...
*/
public abstract
List<InputSplit> getSplits(JobContext context
) throws IOException, InterruptedException;
/**
* Create a record reader for a given split.
* ...
*/
public abstract
RecordReader<K,V> createRecordReader(InputSplit split,
TaskAttemptContext context
) throws IOException,
InterruptedException;
}
-
public abstract List<InputSplit> getSplits(JobContext context)
: 这个方法在客户端提交作业时被调用。它的任务是根据输入数据总量、文件块大小、配置参数等,计算出应该如何对数据进行逻辑切分,并返回一个InputSplit
的列表。列表中的每个InputSplit
对象都定义了一个Mapper
任务的数据处理范围。 -
public abstract RecordReader<K,V> createRecordReader(InputSplit split, TaskAttemptContext context)
: 这个方法在Mapper
任务所在的 TaskTracker 节点上被调用。当一个Mapper
任务被分配到一个InputSplit
后,框架会调用这个方法来创建一个RecordReader
。这个RecordReader
将负责从该InputSplit
中读取数据,并将其转换成Mapper
能处理的<Key, Value>
对。
InputFormat
的主要实现
Hadoop 提供了多种 InputFormat
的实现,以适应不同的数据源和应用场景。
a. FileInputFormat
这是所有基于文件的 InputFormat
的基类,它实现了文件切分的通用逻辑。默认情况下,它会根据 HDFS 的块大小(Block Size)来切分文件。你可以在 FileInputFormat.java
中看到切分大小的计算逻辑,它会考虑文件的块大小以及用户设置的最小和最大分片大小。
java
// ... existing code ...
/**
* Set the minimum input split size
* @param job the job to modify
* @param size the minimum size
*/
public static void setMinInputSplitSize(Job job,
long size) {
job.getConfiguration().setLong(SPLIT_MINSIZE, size);
}
// ... existing code ...
/**
* Set the maximum split size
* @param job the job to modify
* @param size the maximum split size
*/
public static void setMaxInputSplitSize(Job job,
long size) {
job.getConfiguration().setLong(SPLIT_MAXSIZE, size);
}
// ... existing code ...
FileInputFormat
还定义了一个重要的方法 isSplitable
,用于判断一个文件是否可以被切分。例如,使用 Gzip 压缩的文件是流式压缩,无法从中间开始读取,因此是不可切分的。
java
// ... existing code ...
protected boolean isSplitable(JobContext context, Path file) {
final CompressionCodec codec =
new CompressionCodecFactory(context.getConfiguration()).getCodec(file);
if (null == codec) {
return true;
}
return codec instanceof SplittableCompressionCodec;
}
// ... existing code ...
b. TextInputFormat
这是 MapReduce 作业默认 的 InputFormat
。它继承自 FileInputFormat
,用于读取纯文本文件。它的 RecordReader
(LineRecordReader
) 会将文件中的每一行解析为一个记录。
- Key :
LongWritable
类型,表示该行在文件中的起始字节偏移量。 - Value :
Text
类型,表示该行的内容。
c. CombineFileInputFormat
这个 InputFormat
用于解决"小文件问题"。如果输入中包含大量的小文件,使用默认的 FileInputFormat
会为每个小文件启动一个 Mapper
,造成巨大的系统开销。CombineFileInputFormat
可以将多个小文件(或文件块)打包成一个 InputSplit
,从而由一个 Mapper
处理,大大提高了效率。 在 TestCombineTextInputFormat.java
中可以看到它的使用场景:
java
// ... existing code ...
// we should have a single split as the length is comfortably smaller than
// the block size
assertEquals(1, splits.size(), "We got more than one splits!");
InputSplit split = splits.get(0);
assertEquals(CombineFileSplit.class, split.getClass(),
"It should be CombineFileSplit");
// ... existing code ...
d. NLineInputFormat
这个 InputFormat
确保每个 InputSplit
包含固定行数(N行)的输入。这在某些需要按记录数而不是按字节大小来均衡负载的场景下很有用。
e. DBInputFormat
用于从关系型数据库(如 MySQL)中读取数据。它的 getSplits
方法不是按文件大小切分,而是根据表的数据范围(如主键范围)生成查询语句,每个 InputSplit
对应一个 SELECT
查询。
f. CompositeInputFormat
用于处理 JOIN
操作。它可以将来自不同数据源、使用不同 InputFormat
的数据组合在一起,进行连接操作。
java
// ... existing code ...
/**
* Convenience method for constructing composite formats.
* Given operation (op), Object class (inf), set of paths (p) return:
* {@code <op>(tbl(<inf>,<p1>),tbl(<inf>,<p2>),...,tbl(<inf>,<pn>)) }
*/
public static String compose(String op, Class<? extends InputFormat> inf,
String... path)
// ... existing code ...
如何使用 InputFormat
在编写 MapReduce 作业时,你可以通过 Job
对象来指定要使用的 InputFormat
:
java
Job job = Job.getInstance(conf);
// ...
job.setInputFormatClass(TextInputFormat.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
// ...
如果不指定,默认就会使用 TextInputFormat
。
总结
InputFormat
是 MapReduce 数据输入端的"指挥官"。它定义了数据如何被并行化处理的蓝图(通过 getSplits
),并提供了读取这份蓝图的具体工具(通过 createRecordReader
)。从最基础的文件切分到复杂的数据源连接,InputFormat
的不同实现为 MapReduce 提供了强大的数据接入能力,使其能够灵活高效地处理各种类型的数据。