以下皆为新API。本文一小部分内容参考自:https://blog.youkuaiyun.com/wawmg/article/details/17095125 感谢原作者
本部分主要总结了MapReduce将输入数据切分为键值对的过程
一. 输入分片与记录
分片抽象类:InputSplit
分片获取抽象类:InputFormat
1. 输入分片与块
输入分片:在mapreduce中为单个map才做来处理的输入块。一个map只处理一个分片数据。以下不说明时。分片即为输入分片。
块:HDFS中文件的存储形式。默认情况下,一个分片即为一个快。
2. MR中的分片表示
输入分片在Java中表示为InputSplit
接口,包含两个方法。
public abstract long getLength()
:返回分片大小。用于排序分片,以便于优先处理。public abstract String[] getLocations()
:返回分片存储位置的一组存储位置。便于数据本地化。
3. 应用如何获取分片信息
我们通过InputFormat
接口的相关方法来获取分片,并获取分区中的键值对类型及其数据。
public abstract List<InputSplit> getSplits(JobContext var1)
:获取一个分片的列表。此列表会发送给AM用于分配map任务。public abstract RecordReader<K, V> createRecordReader(InputSplit var1, TaskAttemptContext var2)
:传入一个分片,返回一个RecordReader对象。后者类似于一个迭代器,来生成分片记录的键值对。
二. 分片的几个子类
1. FileSplit
FileSplit是InputSplit的子类,标识一块文件分区。其源码实现如下:
public class FileSplit extends InputSplit implements Writable {
// 文件对象
private Path file;
// 要处理文件地址起始位置
private long start;
// 要处理文件的字节数
private long length;
// 包含块(可能为空)的主机列表
private String[] hosts;
private SplitLocationInfo[] hostInfos;
public FileSplit() {
}
public FileSplit(Path file, long start, long length, String[] hosts) {
this.file = file;
this.start = start;
this.length = length;
this.hosts = hosts;
}
public Path getPath() {
return this.file;
}
public long getStart() {
return this.start;
}
public long getLength() {
return this.length;
}
public String toString() {
return this.file + ":" + this.start + "+" + this.length;
}
public String[] getLocations() throws IOException {
return this.hosts == null ? new String[0] : this.hosts;
}
// 省略部分数据
从源码中可以看出,FileSplit有四个属性:文件路径,分片起始位置,分片长度和存储分片的hosts。用这四项数据,就可以计算出提供给每个Mapper的分片数据。在InputFormat的getSplit()方法中构造分片,分片的四个属性会通过调用FileSplit的构造函数设置。
2. CombineFileSplit
该类主要用于小文件分片处理。其主要代码如下:
public class CombineFileSplit extends InputSplit implements Writable {
// 小文件路径列表
private Path[] paths;
private long[] startoffset;
private long[] lengths;
private String[] locations;
private long totLength;
public CombineFileSplit() {
}
public CombineFileSplit(Path[] files, long[] start, long[] lengths, String[] locations) {
this.initSplit(files, start, lengths, locations);
}
public CombineFileSplit(Path[] files, long[] lengths) {
long[] startoffset = new long[files.length];
for(int i = 0; i < startoffset.length; ++i) {
startoffset[i] = 0L;
}
String[] locations = new String[files.length];
for(int i = 0; i < locations.length; ++i) {
locations[i] = "";
}
this.initSplit(files, startoffset, lengths, locations);
}
public CombineFileSplit(CombineFileSplit old) throws IOException {
this(old.getPaths(), old.getStartOffsets(), old.getLengths(), old.getLocations());
}
public long getLength() {
return this.totLength;
}
public long[] getStartOffsets() {
return this.startoffset;
}
public long[] getLengths() {
return this.lengths;
}
public long getOffset(int i) {
return this.startoffset[i];
}
public long getLength(int i) {
return this.lengths[i];
}
public Path getPath(int i) {
return this.paths[i];
}
public Path[] getPaths() {
return this.paths;
}
public String[] getLocations() throws IOException {
return this.locations;
}
// 省略部分方法
FileSplit对应一个输入文件,用于标识文件分片。但是,在文件特别小的情况下,一个FileSplit对应的就是一个mapper,遇到大量小文件时产生的资源、性能开销是无可估量的,因此设计了CombineFileSplit类。
该类将小文件封装为一个分片,使得mapper可以处理小文件。其的属性也包含文件路径、分片分片其实位置、长度、主机等,只不过这些属性不再是一个值,而是一个列表。CombineFileSplit的getLength()方法,返回的是这一系列数据的总长度。
三. RecordReader类
RecordReader将读入到Map的数据(一个分区)拆分成<key, value>对。RecordReader也是一个抽象类。其类似于一个迭代器。
public abstract class RecordReader<KEYIN, VALUEIN> implements Closeable {
/**
* 由一个InputSplit初始化
*/
public abstract void initialize(InputSplit split, TaskAttemptContext context)
throws IOException, InterruptedException;
/**
* 顾名思义,读取分片下一个<key, value>对
*/
public abstract boolean nextKeyValue() throws IOException,
InterruptedException;
/**
* 获取当前键
*/
public abstract KEYIN getCurrentKey() throws IOException,
InterruptedException;
/**
* 获取当前值
*/
public abstract VALUEIN getCurrentValue() throws IOException,
InterruptedException;
/**
* 跟踪读取分片的进度
*/
public abstract float getProgress() throws IOException,
InterruptedException;
/**
* 关闭分片reader
*/
public abstract void close() throws IOException;
}
四. InputFormat的几个子类
附1:InputFormat类图
附2:常用FileInputFormat子类及其对应键值类型
- 1类为基础类,其负责实现文件分片,分片到记录由子类实现。
- 2-4类为文本读取类,除4类外,其余类使用的是默认分片方式。
- 5类为二进制读取类。
- 6类为数据库输入类
1. FileInputFormat类
1.1 主要功能
- 指定作业的输入文件的路径
- 将输入文件切分为分片。(将分片切割为记录由其子类实现)
1.2 输入路径
见链接,比较详细。https://www.cnblogs.com/yurunmiao/p/4514017.html。有几个注意事项。
- 默认情况下输入路径下的内容都会被认定为文件,因此目录下包含文件时,会产生错误。若想递归遍历子文件夹文件,则可设置
mapreduce.input.fileinputformat.input.dir.recursive
设置为true。 - 默认情况下,该类会过滤掉隐藏文件,该过滤器总是会执行。
1.3 输入分片
默认情况下,该类会将大于HDFS文件系统的文件进行切分。但切分也可以调整,涉及以下三个参数。
mapreduce.input.fileinputformat.split.minsize
:一个分片的最小数字。默认为1(b 字节,下同)mapreduce.input.fileinputformat.split.maxsize
:一个分片的最大数据。默认为Long.MAX_VALUEdfs.blocksize
:HDFS的块大小。默认为128M
1.4 分片大小的计算规则如下:
max(minsize, min(maxsize, blocksize))
因此在默认情况下,改函数返回的值为128M。根据以上规则,总结出如下结论:
- 要想增大一个分片的空间,可以将minsize调整大于HDFS分块大小
- 要想减小一个分片的空间,可以将maxsize调整小于HDFS分块大小
1.5 防止切分
有时候我们希望文件不用切分,而是使用一个mapper处理,此时我们可以有两种方法实现,一种是修改minsize改为maxsize的值,另一种方法即为重写FileInputFormat具体子类,并使isSplitable()
返回false。
1.6 获取分片信息
在输入源为FileInputFormat时,我们可以调用Mapper实例参数context.getInputSplit方法实现。然后将返回的inputSplit实例强制转换为FileSplit,即可通过该类查看分片信息。该类的部分方法如下:
- getPath():正在处理的输入文件路径
- getStart():分片开始出的字节偏移量
- getLength():分片的长度(字节为单位)
1.7 将一个文件所有内容作为一条记录
要达成以上目标,需满足如下两个条件:
- 该文件不能被切分,一个分片对应一个map
- 调用InputFormat的createRecordReader时返回的键值中的值时整个文件内容。
基于以上目的,我们可以通过以下方法实现: - 继承并重写FileInputFormat或其子类的isSplitable()及createRecordReader()方法。
- 继承并重写RecordReader类,并重写其相关方法。
2. TextInputFormat
2.1 主要功能
处理文本数据,是默认的InputFormat实现。
2.2 输入键值类型
键类型为LongWritable,表示该行在整个文件中的偏移量。
值类型为Text,表示该行数据。
2.3 输入分片与HDFS块
FileInputFormat定义的逻辑记录有时候不是匹配HDFS块的。因为一个文件中的行有可能被存储于多个HDFS块中(HDFS不是按行存储的,其是按字节存储的),而TextInputFormat则是按行输入的,因此“本地运行”的map可能会执行一些远程读操作。如下图,一行代表一个分区:
2.4 控制一行最长长度
通过设置mapreduce.input.linerecordreader.line.maxlength
来达成,单位为字节。hadoop会跳过字节数超过min(内存最多存储字节,maxlength)的行
3. KeyValueTextInputFormat类
3.1 主要功能
适用于那些通过分界符来产生键值对的输入文件。
3.2 键值类型
键值类型均为Text
3.3 设置分界符
通过设置属性mapreduce.input.keyvaluelinerecordreader.key.value.separator
属性指定,默认为水平制表符。
例如如下文件:
line1-> hello world
line2 -> good,what
生成的key,value序列为:
(line1, hello world)
(ine2, good,what)
4. NLineInputFormat类
对于默认的TextInputFormat
、KeyValueTextInputFormat
来说,一个map可能会得到不同行数的输入,因为他们的数据来源于分片,而一个分片中的行数是不确定的,以上两个类的分片划分方法都继承自FileInputFormat
。
如果我们希望每个map获取到固定的行数输入,我们则可以通过NLineInputFormat
实现。该类重写了分区方法,保证每个mapper收到固定数量的行数
4.1 键值类型
同TextInputFormat
4.2 设置每个mapper输入行数。
通过设置mapreduce.input.lineinputformat.linespermap
控制每个map的输入行数,默认为1。举个栗子,假设有一个文件中共有6行,同时以上属性被设置为默认值,则最后会生成6个分片,每个分片包含一行,由6个map执行;设置为2时,最后会生成3个分片,每个分片包含2行,由3个map执行。
5. SequenceFileInputFormat类
该类用于处理顺序文件,其包含SequenceFileAsTextInputFormat
、SequenceFileAsBinaryInputFormat
两个变体。
5.1 键值类型
从本节开始的图我们可以得知,SequenceFileInputFormat
没有指定输入键值类型。但是,由于输入的键值类型取决于顺序文件,因此我们只需保证map的输入类型匹配即可。(我们可以查阅job的方法列表,我们会发现我们无法设置输入的键/值类型,只可设置inputFormat
)
5.2 SequenceFileAsTextInputFormat类
该类键值类型皆为Text,其相当于在SequenceFileInputFormat
的键值上调用了toString()方法
5.3 SequenceFileAsBinaryInputFormat
该类键值类型皆为BytesWritable
,将输入键值类型转化为BytesWritable
输出给map。
6. 多个输入–>MultipleInputs类
6.1 应用场合
一个MR作业可能包含多个输入路径,我们可以使用FileInputFormat
的相关方法进行处理,此时多个路径会被同一个InputFormat
及同一个map逻辑处理。但是有些情况下,不同的输入路径可能对应着不同的文件切分规则、不同的map处理逻辑,该场景即可使用该类。
6.2 使用方法
主要涉及两个方法
void addInputPath(Job job, Path path, Class<? extends InputFormat> inputFormatClass)
void addInputPath(Job job, Path path, Class<? extends InputFormat> inputFormatClass, Class<? extends Mapper> mapperClass)
以上两个方法参数不再细说,第二个方法支持指定map。分别试用于以下场景:
- 有多种输入格式,但是使用同一套处理逻辑,此时调用通过
setMapperClass
设置的map,若未设置,则调用默认map。 - 有多种输入格式,且一种格式对应一种处理逻辑。
6.3 注意事项
由其的用法可知,其可以代替FileInputFormat.addInputPath()
和job.setMapperClass()
两个方法。
7. 数据库输入
数据库输入涉及到的输入类为DBInputFormat
,其基于JDBC连接传统关系型数据库。由于传统关系型数据库没有共享能力(理解为并行访问能力),因此该输入主要涉及到小规模数据集,且不能有过多的mapper。