一个输入分片(split)就是一个由单个map操作来处理的输入块。每一个map操作只处理一个输入分片。每个分片被划分为若干个记录,每条记录就是一个键-值对,map一个接一个地处理记录。输入分片和记录都是逻辑概念,不必将它们对应到文件,尽管其常见形式都是文件。在数据库场景中,一个输入分片可以对应于一个表上的若干行,而一条记录对应到一行(如同DBInputFormat,这种输入格式用于从关系型数据库读取数据)。
输入分片在Java中表示为InputSplit接口(在org.apache.hadoop.mapreduce包中)。
public abstract class InputSplit {
public abstract long getLength() throws IOException, InterruptedException;
public abstract String[] getLocations() throws IOException, InterruptedException;
}
InputSplit包含一个以字节为单位的长度和一组存储位置(即一组主机名)。注意,分片并不包含数据本身,而是指向数据的引用(reference)。存储位置供MapReduce系统使用以便将map任务尽量放在分片数据附近,而分片大小用来排序分片,以便优先处理最大的分片,从而最小化作业运行时间(这也是贪婪近似算法的一个实例)。
MapReduce应用开发人员不必直接处理InputSplit,因为它是由InputFormat创建的(InputFormat负责创建输入分片并将它们分隔成记录)。在我们探讨InputFormat的具体例子之前,先简单看一下它在MapReduce中的用法。接口如下:
public abstract class InputFormat<K,V> {
public abstract List<InputSplit> getSplits(JobContext context)
throws IOException, InterruptedException;
public abstract RecordReader<K,V> createRecordReader(InputSplit split, TaskAttemptContext context)
throws IOException, InterruptedException;
}
运行作业的客户端通过调用getSplits()计算分片,然后将它们发送到application master,application master使用其存储位置信息来调度map任务从而在集群上处理这些分片数据。map任务把输入分片传给InputFormat的createRecordReader()方法来获取这个分片的RecordReader。RecordReader就像是记录上的迭代器,map任务用一个RecordReader来生成记录的键-值对,然后再传递给map函数。查看Mapper的run()方法可以看到这些情况:
public void run(Context context) throws IOException, InterruptedException {
setup(context);
while (context.netKeyValue()) {
map(context.getCurrentKey(), context.getCurrentValue(), context);
}
cleanup(context);
}
运行setup()之后,再重复调用Context上的nextKeyValue()(委托给RecordReader的同名方法)为mapper产生键-值对象。通过Context,键/值从RecordReader中被检索出并传递给map()方法。当reader读到stream的结尾时,nextKeyValue()方法返回false,map任务运行其cleanup方法,然后结束。
尽管这段代码没有显示,由于效率的原因,RecordReader程序每次调用getCurrentKey()和getCurrentValue()时将返回相同的键-值对象。只是这些对象的内容被reader的nextKeyValue()方法改变。用户对此可能有些惊讶,他们可能希望键/值是不可变的且不会被重用。在map()方法之外有对键/值的引用时,这可能引起问题,因为它的值会在没有警告的情况下被改变。如果确实需要这样的引用,那么请保存你想保留的对象的一个副本,例如,对于Text对象,可以使用它的复制构造函数:new Text(value)。
这样的情况在reducer中也会发生。reducer迭代器中的值对象被反复使用,所以,在调用迭代器之间,一定要复制任何需要保留的任何对象。
最后,注意Mapper的run()方法是公共的,可以由用户定制。MultithreadedMapRunner是另一个MapRunnable接口的实现,它可以使用可配置个数的线程来并发运行多个mapper(用mapreduce.mapper.multithreadedmapper.threads设置)。对于大多数数据处理任务来说,默认的执行机制没有优势。但是,对于因为需要连接外部服务器而造成单个记录处理时间比较长的mapper来说,它允许多个mapper在同一个JVM下以尽量避免竞争的方式执行。
FileInputFormat类的输入和分片
FileInputFormat是所有使用文件作为其数据源的InputFormat实现的基类,它提供两个功能:一是用于指出作业的输入文件位置;二是为输入文件生成分片的代码实现。把分片分割成记录的作业由其子类来完成。
假设有一组文件,FileInputFormat如何把它们转换为输入分片呢?FileInputFormat只分隔大文件。这里的“大”指的是文件超过HDFS块的大小。分片通常与HDFS块大小一样,这在大多应用中是合理的;然而,这个值也可以通过设置不同的Hadoop属性来改变,如下表所示:
属性名称 | 类型 | 默认值 | 描述 |
mapreduce.input.fileinputformat.split.minsize | int | 1 | 一个文件分片最小的有效字节数 |
mapreduce.input.fileinputformat.split.maxsize | long | Long.MAX_VALUE,即9223372036854775807 | 一个文件分片中最大的有效字节数(以字节算) |
dfs.blocksize | long | 128MB,即134217728 | HDFS中块的大小(按字节) |
最小的分片大小通常是1个字节,过某些格式可以使分片大小有一个更低的下界。例如,顺序文件在流中每次插入一个同步入口,所以,最小的分片大小不得不足够大以确保每个分片有一个同步点,以便reader根据记录边界进行重新同步。
应用程序可以强制设置一个最小的输入分片大小:通过设置一个比HDFS块更大一些的值,强制分片比文件块大。如果数据存储在HDFS上,那么这样做是没有好处的,因为这样做会增加对map任务来说不是本地文件的文件块数。
最大的分片大小默认是由Java的long类型表示的最大值。只有把它的值被设置成小于块大小才有效果,这将强制分片比块小。
分片的大小由以下公式计算,参见FileInputFormat的computeSplitSize()方法:
max(minimumSize, min(maximumSize, blockSize))
在默认情况下:
mininumsize < blockSize < maximumSize
所以分片的大小就是blocksize。这些参数的不同设置及其如何影响最终分片大小,见下表说明:
最小分片大小 | 最大分片大小 | 块的大小 | 分片大小 | 说明 |
1(默认值) | Long.MAX_VALUE(默认值) | 128MB(默认值) | 128MB | 默认情况下,分片大小与块大小相同 |
1(默认值) | Long.MAX_VALUE(默认值) | 126MB | 256MB | 增加分片大小最自然的方法是提供更大的HDFS块,通过dfs.blocksize或在构建文件时以单个文件为基础进行设置 |
256MB | Long.MAX_VALUE(默认值) | 128MB(默认值) | 256MB | 通过使最小分片大小的值大于块大小的方法来增大分片大小,但代价是增加了本地操作 |
1(默认值) | 64MB | 128MB(默认值) | 64MB | 通过使最大分片大小的值大于块大小的方法来减少分片大小 |
完毕。