Hadoop MapReduce 的类型与格式 (MapReduce Types and Formats)
1 MapReduce 类型 (MapReduce Types)
Hadoop 的 MapReduce 中的 map 和 reduce 函数遵循如下一般性格式:
map: (K1, V1) → list(K2, V2)
reduce: (K2, list(V2)) → list(K3, V3)
一般来说, map 函数输入的键和值类型(K1, V1)不同于 map 的输出类型(K2, V2)。然而, reduce 函数的输入必须与 map 的输出具有相同的类型,尽管 reduce 的输出类型可能与之再次不同 (k3,v3)。
映射到 Java API 的一般形式:
public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
public class Context extends MapContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
// ...
}
protected void map(KEYIN key, VALUEIN value,
Context context) throws IOException, InterruptedException {
// ...
}
}
public class Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
public class Context extends ReducerContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
// ...
}
protected void reduce(KEYIN key, Iterable<VALUEIN> values,
Context context) throws IOException, InterruptedException {
// ...
}
}
context 对象用于发送 key-value 对,并且由输出类型参数化,因此 write() method 的签名:
public void write(KEYOUT key, VALUEOUT value) throws IOException, InterruptedException
由于 Mapper 和 Reducer 是不同的类,类型参数具有不同的作用域,实际的类型参数 KEYIN 在 Mapper 中可能不同于在 Reducer 中的同名的类型参数 KEYIN 。
类似地,纵然 map 的输出类型和 reduce 的输入类型必须匹配,但这在 Java 编译器上并不是强制的。
类型参数( type parameters )命名上不同于抽象类型( KEYIN 对 K1,等待 ),但它们在形式上是一致的。
如果使用 combiner function ,它与 reduce function 具有相同的形式(它是 Reducer 的一个实现),除了它的输出类型是中间的键值类型(K2,V2),因此它们可以输给 reduce function :
map: (K1, V1) → list(K2, V2)
combiner: (K2, list(V2)) → list(K2, V2)
reduce: (K2, list(V2)) → list(K3, V3)
combiner 和 reduce function 通常是一样的,这种情况下 K3 与 K2 相同, V3 与 V2 相同。
partition function 在中间结果的键值类型上操作,返回分区索引。实践上,分区独自由 key 决定( value 被忽略 ):
partition: (K2, V2) → integer
Java 的方式:
public abstract class Partitioner<KEY, VALUE> {
public abstract int getPartition(KEY key, VALUE value, int numPartitions);
}
输入类型( input types )由输入格式( input format)设置,例如,TextInputFormat 产生 LongWritable 类型的 key 和 Text 类型的值。
其他类型由调用 Job 的方法显式设置。 如果没有显式设置,中间类型默认和输出类型一样,默认为 LongWritable 和 Text 。因此,如果 K2 和 K3 一样,
就不需要调用 setMapOutputKeyClass() ,因为它会回退到调用 setOutputKeyClass() 的类型。类似地,如果 V2 和 V3 一样,仅仅需要 setOutputValueClass() 。
类型冲突是在作业被执行的过程中检测出来的,由于这个原因,一个比较明智的做法是先用少量的数据跑一下测试作业,发现并修正任何类型不兼容问题。
Configuration of MapReduce types in the new API
+-------------------------------------------------------+-------------------------------+---------------+-----------------------+---------------+
| | | | | |
| 属性 | Job setter method | Input types | Intermediate types | Output types |
| | | K1 V1 | K2 V2 | K3 V3 |
+-------------------------------------------------------+-------------------------------+---------------+-----------------------+---------------+
| | | | | |
| Properties for configuring types: | | | | |
| | | | | |
+-------------------------------------------------------+-------------------------------+---------------+-----------------------+---------------+
| mapreduce.job.inputformat.class | setInputFormatClass() | * * | | |
+-------------------------------------------------------+-------------------------------+---------------+-----------------------+---------------+
| mapreduce.map.output.key.class | setMapOutputKeyClass() | | * | |
+-------------------------------------------------------+-------------------------------+---------------+-----------------------+---------------+
| mapreduce.map.output.value.class | setMapOutputValueClass() | | * | |
+-------------------------------------------------------+-------------------------------+---------------+-----------------------+---------------+
| mapreduce.job.output.key.class | setOutputKeyClass() | | | * |
+-------------------------------------------------------+-------------------------------+---------------+-----------------------+---------------+
| mapreduce.job.output.value.class | setOutputValueClass() | | | * |
+-------------------------------------------------------+-------------------------------+---------------+-----------------------+---------------+
| |
| Properties that must be consistent with the types: |
| |
+-------------------------------------------------------+-------------------------------+---------------+-----------------------+---------------+
| mapreduce.job.map.class | setMapperClass() | * * | * * | |
+-------------------------------------------------------+-------------------------------+---------------+-----------------------+---------------+
| mapreduce.job.combine.class | setCombinerClass() | | * * | |
+-------------------------------------------------------+-------------------------------+---------------+-----------------------+---------------+
| mapreduce.job.partitioner.class | setPartitionerClass() | | * * | |
+-------------------------------------------------------+-------------------------------+---------------+-----------------------+---------------+
| mapreduce.job.output.key.comparator.class | setSortComparatorClass() | | * | |
+-------------------------------------------------------+-------------------------------+---------------+-----------------------+---------------+
| mapreduce.job.output.group.comparator.class | setGroupingComparatorClass() | | * | |
+-------------------------------------------------------+-------------------------------+---------------+-----------------------+---------------+
| mapreduce.job.reduce.class | setReducerClass() | | * * | * * |
+-------------------------------------------------------+-------------------------------+---------------+-----------------------+---------------+
| mapreduce.job.outputformat.class | setOutputFormatClass() | | | * * |
+-------------------------------------------------------+-------------------------------+---------------+-----------------------+---------------+
默认的 MapReduce 作业 (The Default MapReduce Job)
-----------------------------------------------------------------------------------------------------------------------------------------------
如果不指定 mapper 或 reducer 会发生什么呢?
public class MinimalMapReduce extends Configured implements Tool {
@Override
public int run(String[] args) throws Exception {
if (args.length != 2) {
System.err.printf("Usage: %s [generic options] <input> <output>\n",
getClass().getSimpleName());
ToolRunner.printGenericCommandUsage(System.err);
return -1;
}
Job job = new Job(getConf());
job.setJarByClass(getClass());
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
return job.waitForCompletion(true) ? 0 : 1;
}
public static void main(String[] args) throws Exception {
int exitCode = ToolRunner.run(new MinimalMapReduce(), args);
System.exit(exitCode);
}
}
The only configuration that we set is an input path and an output path. We run it over a subset of our weather data with the following:
% hadoop MinimalMapReduce "input/ncdc/all/190{1,2}.gz" output
输出目录里得到名为 part-r-00000 的输出文件。输出文件的每一行以整数开始,接着制表符( tab ),然后是一段原始气象数据记录。
下面的代码和前面完成的事情一模一样,但它显式地将作业环境设置为默认值:
public class MinimalMapReduceWithDefaults extends Configured implements Tool {
@Override
public int run(String[] args) throws Exception {
Job job = JobBuilder.parseInputAndOutput(this, getConf(), args);
if (job == null) {
return -1;
}
job.setInputFormatClass(TextInputFormat.class);
job.setMapperClass(Mapper.class);
job.setMapOutputKeyClass(LongWritable.class);
job.setMapOutputValueClass(Text.class);
job.setPartitionerClass(HashPartitioner.class);
job.setNumReduceTasks(1);
job.setReducerClass(Reducer.class);
job.setOutputKeyClass(LongWritable.class);
job.setOutputValueClass(Text.class);
job.setOutputFormatClass(TextOutputFormat.class);
return job.waitForCompletion(true) ? 0 : 1;
}
public static void main(String[] args) throws Exception {
int exitCode = ToolRunner.run(new MinimalMapReduceWithDefaults(), args);
System.exit(exitCode);
}
}
public class JobBuilder{
public static Job parseInputAndOutput(Tool tool, Configuration conf,
String[] args) throws IOException {
if (args.length != 2) {
printUsage(tool, "<input> <output>");
return null;
}
Job job = new Job(conf);
job.setJarByClass(tool.getClass());
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
return job;
}
public static void printUsage(Tool tool, String extraArgsUsage) {
System.err.printf("Usage: %s [genericOptions] %s\n\n",
tool.getClass().getSimpleName(), extraArgsUsage);
GenericOptionsParser.printGenericCommandUsage(System.err);
}
}
默认的输入格式是 TextInputFormat ,它产生的键类型是 LongWritable ( 文件中行首偏移量),值类型是 Text (文本行),这就解释了最后输出的整数的含义:行偏移量。
默认的 mapper 仅仅是 Mapper 类, 它把输入键和值原封不动地写到输出。
public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
protected void map(KEYIN key, VALUEIN value,
Context context) throws IOException, InterruptedException {
context.write((KEYOUT) key, (VALUEOUT) value);
}
}
Mapper 是一个泛型类型( generic type ),它可以和任何键和值类型工作。本例中, map 的输入,输出键类型是 LongWritable ,值的输入输出类型是 Text 。
默认的 partitioner 是 HashPartitioner , 它对每条记录的键进行哈希操作以确定该记录应该属于哪个分区。每个分区由一个 reduce 任务处理,因此分区的数量与作业的 reduce 任务数量相等。
public class HashPartitioner<K, V> extends Partitioner<K, V> {
public int getPartition(K key, V value,
int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
默认情况下,只有一个 reducer ,因此,也就只有一个分区,这种情况下,因为所有数据放入一个分区,partitioner 的行为也就无关紧要了。然而但有多个 reduce 任务时,理解
HashPartitioner 的行为就非常重要了。假设基于键的散列函数足够好,记录会被均匀地分到若干个 reduce 任务中,所有具有相同键的记录会由同一个 reduce 任务处理。
我们并没有设置 map 任务的数量,原因是这个数量等于文件被划分成的分块数( splits ),这取决于输入( input )的大小和文件块的大小( file's block size)(如果此文件在 HDFS 中)。
选择 reducer 数量:
--------------------------------------------------------------------------------------------------------------------
对于 Hadoop 新手而言,单个 reducer 的默认配置很容易上手。几乎所有现实世界的作业会设置成非常大的数字,否则由于所有的中间数据都流入一个单一的 reduce 任务,作业会非常慢。
为一个作业选择 reducer 数量艺术性高于科学性。提高 reducer 数量使 reduce 阶段用时更短,因为获得了更多的并发。然而,如果过度这么做,会产生大量的小文件,这是不理想的。
一个提示性原则是计划 每个 reducer 运行 5 分钟左右,并且产生至少一个 HDFS 块的有价值的输出。
默认的 reducer 是 Reducer 类,也是一个泛型类型,只是简单地把它的所有输入写到输出。
public class Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
protected void reduce(KEYIN key, Iterable<VALUEIN> values, Context context
Context context) throws IOException, InterruptedException {
for (VALUEIN value: values) {
context.write((KEYOUT) key, (VALUEOUT) value);
}
}
}
对于这个作业来说,输出键是 LongWritable 类型,输出值是 Text 类型。对于大多数的 MapReduce 程序来说,不会这个过程都使用相同的键和值类型,因此需要配置作业声明使用的类型。
记录发送给 reducer 之前会由 MapReduce 系统进行排序,这个例子,键是按数字排序的,因此来自输入文件中的行会被交叉放入一个合并后的输出文件。
默认的输出格式是 TextOutputFormat ,它将键和值转换成字符串并用制表符隔开,然后一条记录一行地写到输出。
默认的 Streaming 作业 ( The default Streaming job )
-----------------------------------------------------------------------------------------------------------------------------------------------
Streaming 模式下,默认的作业与 Java 的相似,但也由差别,基本形式如下:
% hadoop jar $HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-*.jar \
-input input/ncdc/sample.txt \
-output output \
-mapper /bin/cat
当使用非 Java 的 mapper ( non-Java mapper ),实际的效果是默认为文本模式( -io text), Streaming 有些特殊的地方。它不向 mapper 传递 key ,只是传递 value
对于其他输入格式,设置 stream.map.input.ignoreKey 为 true 可获得相同的效果。这是非常有用的,因为 key 仅仅是文件的行偏移量,而 value 才是行中的数据( value is the line ),也就是大多数应用程序关心的部分。
这个作业的效果是执行一次输入排序。
将更多的默认值列出来,命令行看起来如下所示( 注意 Streaming 使用了老版本的 MapReduce API 类):
% hadoop jar $HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-*.jar \
-input input/ncdc/sample.txt \
-output output \
-inputformat org.apache.hadoop.mapred.TextInputFormat \
-mapper /bin/cat \
-partitioner org.apache.hadoop.mapred.lib.HashPartitioner \
-numReduceTasks 1 \
-reducer org.apache.hadoop.mapred.lib.IdentityReducer \
-outputformat org.apache.hadoop.mapred.TextOutputFormat
-io text
-mapper 和 -reducer 选项参数使用了一个命令或 Java class 。 可选的 combiner 可以由 -combiner argument 选项指定。
Streaming 中的键和值 ( Keys and values in Streaming )
-----------------------------------------------------------------------------------------------------
Streaming 应用程序可以控制分隔符( separator )的使用,分隔符用于将一个键-值对通过标准输入转换成字节序列传递给 map 或 reduce 进程。
默认是 tab 字符,但如果键或值本身中含有 tab 字符,能将分隔符改成其他符号是很有用的。
类似地,当 map 或 reduce 写输出 key-value 对时,也需要可配置的分隔符进行分隔。更进一步,来自输出的 key 可以由多个字段组合:它由最开始的 n 个字段组成(
由 stream.num.map.output.key.fields 或 stream.num.reduce.output.key.fields 属性定义),value 由剩下的字段组成。
例如: Streaming 进程的输出是 a,b,c ( 由逗号分隔 ) , n 是 2, 那么 key 会被解析为 a,b 并且 value 是 c 。
mapper 和 reducer 的分隔符是单独配置的:
Streaming 分隔符属性
+---------------------------------------+-----------+-------+-------------------------------------------------------------------------------+
| 属性名称 | 类型 | 默认值| 描述 |
+---------------------------------------+-----------+-------+-------------------------------------------------------------------------------+
| | | | |
| stream.map.input.field.separator | String | \t | The separator to use when passing the input key and |
| | | | value strings to the stream map process as a stream of bytes |
+---------------------------------------+-----------+-------+-------------------------------------------------------------------------------+
| | | | |
| stream.map.output.field.separator | String | \t | The separator to use when splitting the output from the |
| | | | stream map process into key and value strings for the map output |
+---------------------------------------+-----------+-------+-------------------------------------------------------------------------------+
| | | | |
| stream.num.map.output.key.fields | int | 1 | The number of fields separated by |
| | | | stream.map.output.field.separator to treat as the map output key |
+---------------------------------------+-----------+-------+-------------------------------------------------------------------------------+
| | | | |
| stream.reduce.input.field.separator | String | \t | The separator to use when passing the input key and |
| | | | value strings to the stream reduce process as a stream of bytes |
+---------------------------------------+-----------+-------+-------------------------------------------------------------------------------+
| | | | |
| stream.reduce.output.field.separator | String | \t | The separator to use when splitting the output from the |
| | | | stream reduce process into key and value strings for the final reduce output |
+---------------------------------------+-----------+-------+-------------------------------------------------------------------------------+
| | | | |
| stream.num.reduce.output.key.fields | int | 1 | The number of fields separated by |
| | | | stream.reduce.output.field.separator to treat as the reduce output key |
+---------------------------------------+-----------+-------+-------------------------------------------------------------------------------+
这些设置与输入输出格式不相关的。例如, stream.reduce.output.field.separator 设置成冒号( colon ),就是说, reduce 流进程把 a:b 写到标准输出, Streaming 的 reduce 会知道抽取出 a 为 key , b 为 value 。
使用标准的 TextOutputFormat ,这条记录会由 tab 字符分隔 a 和 b 写出到输出文件。可以通过设置 mapreduce.output.textoutputformat.separator 改变 TextOutputFormat 的分隔符。
*
*
*
2 输入格式 (Input Formats)
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
从一般的文本文件到数据库, Hadoop 可以处理很多不同类型的数据格式。
1. 输入分片与记录 (Input Splits and Records)
------------------------------------------------------------------------------------------------------------------------------------------------------------------
一个输入分片是一个由一个单独的 map 处理的输入数据块。每个 map 处理一个单独的分片。每个分片被分割成若干条记录, map 处理每一条记录 ———— 一个 key-value 对,依次执行。
分片和记录是逻辑上的概念,不必把它们对应到文件,尽管其常见形式为文件。在数据库环境下,一个分片可能对应一个数据表的一定范围的行,一条记录对应到此范围内的一行。
DBInputFormat 就是这么做的,一种从关系数据块读取数据的输入格式。
输入分片在 Java 中表示为 InputSplit 接口,位于 org.apache.hadoop.mapreduce 包:
public interface InputSplit {
public abstract long getLength() throws IOException, InterruptedException;
public abstract String[] getLocations() throws IOException,
InterruptedException;
}
InputSplit 有一个以字节为单位的长度和一组存储位置,一组主机名字符串。注意,分片本身并不含有输入数据,它只是一个对数据的引用。
存储位置供 MapReduce 系统使用,以便尽可能把 map 任务置于分片数据较近的地方,分片大小用来排序分片以便先处理最大的分片,从而最小化作业的运行时间,这是贪吃近似算法的一个实例。
作为 MapReduce 应用程序员,不需要直接处理 InputSplits ,因为它是由一个 InputFormat 负责创建的( 一个 InputFormat 负责创建输入分片并把它们分割成记录)。
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() method 获得那个分片的 RecordReader 。RecordReader 就像记录的迭代器, map 任务利用它生成记录的 key-value 对,传递给 map function ,
看下 Mapper 的 run() method 我们可以观察到这一点:
public void run(Context context) throws IOException, InterruptedException {
setup(context);
while (context.nextKeyValue()) {
map(context.getCurrentKey(), context.getCurrentValue(), context);
}
cleanup(context);
}
运行 setup() 之后,在 Context 上反复调用 nextKeyValue() method(委托给 RecordReader 的同名方法)操作 mapper 的 key 和 value 对象。
通过 Context 从 RecordReader 获取到 key 和 value 传递给 map 方法使其工作。当 reader 读到流的末尾时, nextKeyValue() 方法返回 false ,
map 任务运行它的 cleanup() method 然后结束。
最后,注意到 Mapper 的 run() method 是 public 的,可以由用户自定义。MultithreadedMapper 就是一个实现,
它可以使用可配置的线程数量来并发运行多个 mapper ( 由 mapreduce.mapper.multithreadedmapper.threads 设置 )。对大多数的数据处理任务来说,默认的实现没什么优势,
但对于需要长时间处理单条记录的 mapper 来说,———— 因为它们需要连接外部服务器,它允许多个 mapper 在同一个 JVM 上以很小的竞争运行。
FileInputFormat 类
------------------------------------------------------------------------------------------------------------------------------
FileInputFormat 是所有使用文件作为数据源的 InputFormat 实现的基类。它提供两个功能:一个用于指出作业输入文件的位置,一个是把输入文件生成分片的实现。把分片分割成记录的工作由子类实现。
FileInputFormat 的输入路径( FileInputFormat input paths )
------------------------------------------------------------------------------------------------------------------------------
作业的输入被设定为一组路径,这为指定输入提供了很大的灵活性。FileInputFormat 为设置作业的输入路径提供了四个静态便利方法:
static void addInputPath(JobConf conf, Path path)
static void addInputPaths(JobConf conf, String commaSeparatedPaths)
static void setInputPaths(JobConf conf, Path... inputPaths)
static void setInputPaths(JobConf conf, String commaSeparatedPaths)
addInputPath() 和 addInputPaths() 方法添加一个路径或多个路径到输入列表( list of inputs ),可以反复调用这些方法构建路径列表。
两个 setInputPaths() 方法可以一次设定完整的路径列表(替换掉之前调用的任何路径)。
一个路径可以表示为一个文件,一个目录,或者使用一个 glob ,多个文件和目录的集合。一个路径表示为目录的话,包含目录内的所有文件都作为作业的输入。
警示:
----------------------------------------------------------------------------------------------------
一个指定为输入路径的目录,它的内容不会递归处理,实际上,目录应该只包含文件,如果目录包含子目录,它被解释为文件,这会导致错误。
处理这种情况的办法是使用一个 file glob 或者一个过滤器基于名称模式( name pattern ) 只选择目录内的文件。
另一种办法,设置 mapreduce.input.fileinputformat.input.dir.recursive 值为 true 来强制输入目录递归读取。
add 和 set 方法允许指定的只是包含的文件,要从输入排除特定的文件,可以在 FileInputFormat 上用 setInputPathFilter() 方法设置一个过滤器。
static void setInputPathFilter(JobConf conf, Class<? extends PathFilter> filter)
即便没有设置过滤器, FileInputFormat 使用一个默认的过滤器来排除隐藏文件(文件名以 “.” 或 “_” 开始的文件)。如果通过调用 setInputPathFilter() 设置了一个过滤器,
它是作为默认过滤器的添加物存在,换句话说,只有非隐藏文件可被自定义的过滤器接收到。
路径和过滤器也可以通过配置属性设置,这对 Streaming 作业非常方便。 Streaming 接口可以通过 -input 选项设置路径,因此直接的手动设置路径是不需要的。
Input path and filter properties
+-------------------------------------------+-----------------------+-----------+
| 属性名称 | 类型 | 默认值 |
+-------------------------------------------+-----------------------+-----------+
| mapreduce.input.fileinputformat.inputdir | 逗号分隔的路径 | 无 |
+-------------------------------------------+-----------------------+-----------+
| mapreduce.input.pathFilter.class | PathFilter classname | 无 |
+-------------------------------------------------------------------+-----------+
FileInputFormat 的输入分片 ( FileInputFormat input splits )
------------------------------------------------------------------------------------------------------------------------------
给定一组文件, FileInputFormat 如何把它们转换成分片呢? FileInputFormat 只分割大文件。这里的 “大” 指的是文件超过 HDFS 块( HDFS block )的大小。
分片的大小通常是 HDFS 块的大小( size of an HDFS block )这对大多数应用是合理的;然而,这个值也可以通过设置不同的 Hadoop 属性来控制。
控制分片大小的属性
+-----------------------------------------------+-----------+-------------------+-------------------------------+
| 属性名称 | 类型 | 默认值 | 描述 |
+-----------------------------------------------+-----------+-------------------+-------------------------------+
| mapreduce.input.fileinputformat.split.minsize | int | 1 | The smallest valid size in |
| | | | bytes for a file split |
+-----------------------------------------------+-----------+-------------------+-------------------------------+
| mapreduce.input.fileinputformat.split.maxsize | long | Long.MAX_VALUE | The largest valid size in |
| | | | bytes for a file split |
+-----------------------------------------------+-----------+-------------------+-------------------------------+
| dfs.blocksize | long | 128MB | The size of a block in |
| | | | HDFS in bytes |
+-----------------------------------------------+-----------+-------------------+-------------------------------+
分片大小由以下公式计算:
max(minimumSize, min(maximumSize, blockSize))
默认情况下:
minimumSize < blockSize < maximumSize
因此分片大小为 : blockSize
小文件与 CombineFileInputFormat ( Small files and CombineFileInputFormat )
------------------------------------------------------------------------------------------------------------------------------
相对于大批量的小文件,Hadoop 处理少量的大文件工作得更好。一个原因是 FileInputFormat 以这种方式产生的分片是整个文件的全部或者一个文件的一部分。
如果文件非常小( small ,“小”的意思是指比 HDFS 块小得多 )并且非常多,每个 map 任务处理非常少的输入,会有很多的这样的 map 任务(每个文件一个),每个都会造成额外的开销。
对比一个 1 GB 的文件分解成 8 个 128MB 的块和10000个100KB 的文件,10000个文件每个文件使用一个 map , 作业的时间要比相同大小的一个单一的输入文件占用 8 个 map 任务的作业的时间慢数十倍或数百倍。
这种场景下使用 CombineFileInputFormat 可以稍微缓解一些,它设计用于很好地与小文件工作。在 FileInputFormat 为每个文件创建一个分片的地方, CombineFileInputFormat 打包多个文件到每个分片,
这样每个 mapper 就有更多的处理。关键的是,决定哪些块放入同一个分片时,CombineFileInputFormat 会考虑到节点和机架的因素,所以在典型的 MapReduce 作业中不会造成速度的下降。
当然,如果可能,避免多个小文件的情况仍然是个好主意,因为 MapReduce 以集群内磁盘的传输速率操作时工作得最好,而运行一个作业,处理很多小文件会增加寻道次数 。还有,在 HDFS 中存储大量的小文件
对 namenode 的内存是一种浪费。
另一种避免大量小文件的方法是利用一个顺序文件( sequence file )合并这些小文件到几个大文件里,用这种方法,使用文件名作为键( 或者,如果不需要的话,可以使用一个常量,例如 NullWritable),文件内容作为值。
但如果在 HDFS 中已经存在了大量的小文件,那么 CombineFileInputFormat 值得一试。
提示:
----------------------------------------------------------------------------------------------------------------
CombineFileInputFormat 不仅可以很好地处理小文,处理大文件也有好处。因为它每个节点生成一个分片,可能由多个块组成。
本质上, CombineFileInputFormat 使 mapper 处理的数据数量与 HDFS 中文件的块大小之间解耦。
阻止切分 ( Preventing splitting )
------------------------------------------------------------------------------------------------------------------------------
有些应用不希望文件切分,这允许一个 mapper 整个地处理一个输入文件。例如,检查一个文件中所有的记录是否有序,一个简单的方法是检查每条记录是否不小于前一条记录。
将它实现为一个 map 任务,只有使用一个 map 来处理整个文件这个算法才能工作。
有几种方法可以确保现存的文件不被切分。第一种方法(快速但不优雅)是增加最小切分尺寸大于最大文件大小,设置成最大值, Long.MAX_VALUE 也是有效的。
第二种方法是子类化你要使用的 FileInputFormat 的具体子类,重写 isSplitable() method 让它返回 false 。例如下面的不可切分的 TextInputFormat:
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
public class NonSplittableTextInputFormat extends TextInputFormat {
@Override
protected boolean isSplitable(JobContext context, Path file) {
return false;
}
}
mapper 中的文件信息 ( File information in the mapper)
------------------------------------------------------------------------------------------------------------------------------
Mapper 处理一个文件分片能够通过在 Mapper 的 Context 对象上调用 getInputSplit() method 获取分片的信息。当输入格式来自于 FileInputFormat 时,
这个方法返回的 InputSplit 能够强制转换为 FileSplit ,以此访问它的下面列表的文件信息。
File split proper
+-------------------+-------------------------------+---------------+-------------------------------------------+
| FileSplit method | 属性名 | 类型 | 描述 |
+-------------------+-------------------------------+---------------+-------------------------------------------+
| getPath() | mapreduce.map.input.file | Path/String | The path of the input file being |
| | | | processed |
+-------------------+-------------------------------+---------------+-------------------------------------------+
| getStart() | mapreduce.map.input.start | long | The byte offset of the start of the split |
| | | | from the beginning of the file |
+-------------------+-------------------------------+---------------+-------------------------------------------+
| getLength() | mapreduce.map.input.length | long | The length of the split in bytes |
+-------------------+-------------------------------+---------------+-------------------------------------------+
把整个文件作为一条记录处理 ( Processing a whole file as a record )
------------------------------------------------------------------------------------------------------------------------------
有时候会有这样的需求, mapper 必须访问整个文件的内容。不切分文件只是完成了部分工作,你还需要一个 RecordReader 提交所有文件内容作为记录的 value 。
下面的 WholeFileInputFormat 展示了这样一种做法:
// An InputFormat for reading a whole file as a record
public class WholeFileInputFormat
extends FileInputFormat<NullWritable, BytesWritable> {
@Override
protected boolean isSplitable(JobContext context, Path file) {
return false;
}
@Override
public RecordReader<NullWritable, BytesWritable> createRecordReader(
InputSplit split, TaskAttemptContext context) throws IOException,
InterruptedException {
WholeFileRecordReader reader = new WholeFileRecordReader();
reader.initialize(split, context);
return reader;
}
}
WholeFileInputFormat 定义了一个格式, 没有使用 key ,表现为 NullWritable , value 是文件内容,表现为 BytesWritable 实例。
这个格式定义了两个方法,首先,它很小心地指定了输入文件不会被切分,通过重写 isSplitable() 并返回 false 实现。
第二,实现 createRecordReader() 方法来返回自定义实现的 RecordReader
// The RecordReader used by WholeFileInputFormat for reading a whole file as a record
class WholeFileRecordReader extends RecordReader<NullWritable, BytesWritable> {
private FileSplit fileSplit;
private Configuration conf;
private BytesWritable value = new BytesWritable();
private boolean processed = false;
@Override
public void initialize(InputSplit split, TaskAttemptContext context)
throws IOException, InterruptedException {
this.fileSplit = (FileSplit) split;
this.conf = context.getConfiguration();
}
@Override
public boolean nextKeyValue() throws IOException, InterruptedException {
if (!processed) {
byte[] contents = new byte[(int) fileSplit.getLength()];
Path file = fileSplit.getPath();
FileSystem fs = file.getFileSystem(conf);
FSDataInputStream in = null;
try {
in = fs.open(file);
IOUtils.readFully(in, contents, 0, contents.length);
value.set(contents, 0, contents.length);
} finally {
IOUtils.closeStream(in);
}
processed = true;
return true;
}
return false;
}
@Override
public NullWritable getCurrentKey() throws IOException, InterruptedException {
return NullWritable.get();
}
@Override
public BytesWritable getCurrentValue() throws IOException,
InterruptedException {
return value;
}
@Override
public float getProgress() throws IOException {
return processed ? 1.0f : 0.0f;
}
@Override
public void close() throws IOException {
// do nothing
}
}
WholeFileRecordReader 负责保存 FileSplit 并把它转换成一条单独的 record , 这条记录由一个 key 为 null 并且 value 含有文件的所有字节组成。
因为仅有一条 record , WholeFileRecordReader 要么处理了要么没处理,因此它维护了一个 boolean 类型的 processed 变量来表示记录是否被处理过。
nextKeyValue() method 被调用时如果文件还没有被处理,就打开文件,创建一个长度为文件长度的字节数组,并利用 Hadoop 的 IOUtils 类 把全部
文件内容放入到字节数组中,然后把数组设置到 BytesWritable 的实例 value 并返回 true 标志记录已被读取。
其他方法是些直接的簿记方法,用于访问当前的 key 和 value 类型,获取 reader 的读取进度,以及一个 close() 方法,当 reader 完成时由 MapReduce framework 调用。
为了演示 WholeFileInputFormat 怎样使用, 考虑一个打包小文件到 sequence file 的 MapReduce job ,key 为源文件名, value 为 文件的内容。
// A MapReduce program for packaging a collection of small files as a single SequenceFile
public class SmallFilesToSequenceFileConverter extends Configured
implements Tool {
static class SequenceFileMapper
extends Mapper<NullWritable, BytesWritable, Text, BytesWritable> {
private Text filenameKey;
@Override
protected void setup(Context context) throws IOException,
InterruptedException {
InputSplit split = context.getInputSplit();
Path path = ((FileSplit) split).getPath();
filenameKey = new Text(path.toString());
}
@Override
protected void map(NullWritable key, BytesWritable value, Context context)
throws IOException, InterruptedException {
context.write(filenameKey, value);
}
}
@Override
public int run(String[] args) throws Exception {
Job job = JobBuilder.parseInputAndOutput(this, getConf(), args);
if (job == null) {
return -1;
}
job.setInputFormatClass(WholeFileInputFormat.class);
job.setOutputFormatClass(SequenceFileOutputFormat.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(BytesWritable.class);
job.setMapperClass(SequenceFileMapper.class);
return job.waitForCompletion(true) ? 0 : 1;
}
public static void main(String[] args) throws Exception {
int exitCode = ToolRunner.run(new SmallFilesToSequenceFileConverter(), args);
System.exit(exitCode);
}
}
因为输入格式是一个 WholeFileInputFormat ,这个 mapper 只需要为文件分片查找文件名。 它做到这一点是从 context 获取 InputSplit 并强制转换为 FileSplit ,FileSplit 有一个获取文件路径的方法 。
作为 key ,路径存储在一个 Text 对象中 。 reducer 是完全相同的(没有明确设置),并且输出格式是 SequenceFileOutputFormat.
下面是在一些小文件上运行的情况,我们选择了 2 个 reducer ,因此获得 2 个输出的顺序文件:
% hadoop jar hadoop-examples.jar SmallFilesToSequenceFileConverter \
-conf conf/hadoop-localhost.xml -D mapreduce.job.reduces=2 \
input/smallfiles output
创建了两个 part 文件,每一个都是一个顺序文件( sequence file ),可以利用文件系统 shell 的 -text 选项观察到:
% hadoop fs -conf conf/hadoop-localhost.xml -text output/part-r-00000
hdfs://localhost/user/tom/input/smallfiles/a 61 61 61 61 61 61 61 61 61 61
hdfs://localhost/user/tom/input/smallfiles/c 63 63 63 63 63 63 63 63 63 63
hdfs://localhost/user/tom/input/smallfiles/e
% hadoop fs -conf conf/hadoop-localhost.xml -text output/part-r-00001
hdfs://localhost/user/tom/input/smallfiles/b 62 62 62 62 62 62 62 62 62 62
hdfs://localhost/user/tom/input/smallfiles/d 64 64 64 64 64 64 64 64 64 64
hdfs://localhost/user/tom/input/smallfiles/f 66 66 66 66 66 66 66 66 66 66
提示:
--------------------------------------------------------------------------------------------
至少有一种方法能够改进这个程序,之前提过,一个文件一个 mapper 是低效的,
因此子类化 CombineFileInputFormat 替换 FileInputFormat 会是更好的方法。
2. 文本输入 ( Text Input )
------------------------------------------------------------------------------------------------------------------------------------------------------------------
Hadoop 非常擅长处理非结构化的文本数据
TextInputFormat
--------------------------------------------------------------------------------------------------------------------------------------------------------------
TextInputFormat 是默认的输入格式,每条记录是输入的一行。 key 是 LongWritable ,是行首在文件中字节偏移量。 value 是行的内容,不包括行终止符(例如 \n 或 \r ),
并且打包成 Text 对象。因此,一个文件包含下面的文本:
On the top of the Crumpetty Tree
The Quangle Wangle sat,
But his face you could not see,
On account of his Beaver Hat.
被切分成4条记录的一个分片,记录( record ) 被解释成下面的 key-value 对:
(0, On the top of the Crumpetty Tree)
(33, The Quangle Wangle sat,)
(57, But his face you could not see,)
(89, On account of his Beaver Hat.)
显然, key 不是行号,行号通常是不可能实现的,因为文件是按字节边界切分而不是按行切分的。分片都是单独处理。行号实际上是一个顺序的标记,处理的行的时候必须保留一个行的计数,
因此在一个分片内知道行号是可能的,但在文件里就不行了。
然而,文件中每一行的偏移量在每个分片上是独立确定的,而不需要了解其他分片,因为每个分片知道它前面分片的大小,只把这个值加到分片内的偏移量上就可以构成整个文件的全局偏移量。
一般对于需要每个行需要一个唯一的标识符的应用来说,偏移量就足够了。联合文件名,这在整个文件系统上就是唯一的了。
Controlling the maximum line length
-----------------------------------------------------------------------------------------------------------------------------------------
如果正在使用一种文本输入格式,可以设置一个最大期望行长度以防护损坏的文件。文件损坏表现为一个特别长的行,这能导致内存溢出错误然后任务失败。
通过设置 mapreduce.input.linerecordreader.line.maxlength 一个字节单位的内存上合适的值(比正常输入数据行长度充裕些),能够确保 record reader 跳过那些损坏的行,不会令任务失败。
KeyValueTextInputFormat
-----------------------------------------------------------------------------------------------------------------------------------------
TextInputFormat 的键,即每一行在文件中的字节偏移量,一般来说并不是特别有用。通常情况下,文件中的每一行是一个 key-value 对 ,使用某个分隔符分隔,比如 tab 符号。
例如, 由 TextOutputFormat 产生的输出就是这一类型, 它是Hadoop 的默认 OutputFormat 。正确地解析这样的文件,KeyValueTextInputFormat 更适合。
通过设置 mapreduce.input.keyvaluelinerecordreader.key.value.separator 属性值可以指定分隔符,默认是 tab 字符。
考虑下面的文件,→ 表示水平方向的 tab 字符:
line1→On the top of the Crumpetty Tree
line2→The Quangle Wangle sat,
line3→But his face you could not see,
line4→On account of his Beaver Hat.
得到的输入是一个4条记录构成的单独的分片, key 是每一行 tab 符号前面的 Text 序列:
(line1, On the top of the Crumpetty Tree)
(line2, The Quangle Wangle sat,)
(line3, But his face you could not see,)
(line4, On account of his Beaver Hat.)
NLineInputFormat
-----------------------------------------------------------------------------------------------------------------------------------------
使用 TextInputFormat 和 KeyValueTextInputFormat, 每个 mapper 收到的输入行数是不定的。行数依赖于分片的大小和行的长度。
如果想要 mapper 收到固定数量行数的输入, 用 NLineInputFormat 作为 InputFormat 。就像 TextInputFormat , key 是文件内的字节偏移量,value 是行本身。
N 指的是每个 mapper 接收的输入的行数, 设置 N 为 1 (默认值),每个 mapper 只接收一行输入。 mapreduce.input.lineinputformat.linespermap 属性控制着 N 的值。
作为例子,考虑下面四行:
On the top of the Crumpetty Tree
The Quangle Wangle sat,
But his face you could not see,
On account of his Beaver Hat.
如果 N 是 2 ,那么每个分片包含两行。一个 mapper 会收到开始的两行 key-value 对:
(0, On the top of the Crumpetty Tree)
(33, The Quangle Wangle sat,)
另外一个 mapper 会收到第二个两行的 key-value 对:
(57, But his face you could not see,)
(89, On account of his Beaver Hat.)
key 和 value 与 TextInputFormat 产生的一样,不同之处在于分片的构成。
通常,少量输入行的 map 任务是低效的(因为设置任务需要开销),但有些应用使用少量的输入数据运行一个扩展计算,然后产生它们的输出。
仿真计算是很好的例子,创建一个输入文件提供输入参数,一行一个,可以执行参数扫描:并发运行一组仿真实验,看模型是如何随参数不同而变化的。
另一个例子是用 Hadoop 引导从多个数据源加载数据,比如数据库。可以建立一个 “种子” 输入文件,列出数据源,一行一个。然后每个 mapper 分配一个数据源,
并从数据源加载数据到 HDFS 。这个作业不需要 reduce 阶段,因此 reducer 数设为 0 (通过在 Job 上调用 setNumReduceTasks() )。
更进一步, MapReduce job 可以运行来处理数据加载到 HDFS 。
XML
-----------------------------------------------------------------------------------------------------------------------------------------
大多数 XML 解析器会处理 XML 文档,因此如果一个大型的 XML 文档由多个输入分片组成,单独分析它们确实是一种挑战。
当然,可以用一个 mapper 处理整个 XML 文档(如果它不是特别大),使用在 Processing a whole file as a record 里的技术。
大型 XML 文档由一些连续的 records ( XML 文档片段 )组成,可以使用简单的字符串或正则表达式来查找记录的开始标签和结束标签,分解成多个记录。
这可以缓解文档被 framework 切分的时候造成的问题,因为简单地从分片的开始部分扫描,下一个记录的开始标签很容易找到,就像 TextInputFormat 查找新行边界。
Hadoop 为这一目的提供了称为 StreamXmlRecordReader 的类(在 org.apache.hadoop.streaming.mapreduce 包)。使用它,通过把输入格式设置为 StreamInputFormat ,
并且设置 stream.recordreader.class 属性值为 org.apache.hadoop.streaming.mapreduce.StreamXmlRecordReader 。
reader 的配置通过设置作业的属性告诉它开始和结束标签的模式( patterns),详细信息查看类文档。
3. 二进制输入 ( Binary Input )
------------------------------------------------------------------------------------------------------------------------------------------------------------------
Hadoop MapReduce 不仅仅限于处理文本数据,它也支持二进制格式的数据
SequenceFileInputFormat
-----------------------------------------------------------------------------------------------------------------------------------------
Hadoop 的顺序文件格式存储二进制的键值对的序列。顺序文件非常适合 MapReduce 数据的格式,因为它们是可切分的(它们有同步点以至于 reader 可以在文件的任意位置同步记录的边界,例如分片的开始位置),
它们支持压缩作为格式的一部分,它们使用各种序列化框架可以存储任意类型。
要使用顺序文件的数据作为 MapReduce 输入,可以使用 SequenceFileInputFormat , key 和 value 由顺序文件决定,而且要确保对应的 map 输入类型。
例如,如果顺序文件由 IntWritable 类型的 key 和 Text 类型的 value ,那么 mapper 的签名应该这样:
Mapper<IntWritable, Text, K, V>
K 和 V 是 map 的输出 key 和 value 类型。
SequenceFileAsTextInputFormat
-----------------------------------------------------------------------------------------------------------------------------------------
SequenceFileAsTextInputFormat 是 SequenceFileInputFormat 的一个变体,将文件的 key 和 value 转换成 Text 对象。这种转换是通过在 key 和 value 上调用 toString() 实现的。
SequenceFileAsBinaryInputFormat
-----------------------------------------------------------------------------------------------------------------------------------------
SequenceFileAsBinaryInputFormat 是 SequenceFileInputFormat 的一个变体,将接收到的顺序文件的 key 和 value 作为二进制对象,它们被封装成 BytesWritable 对象,
应用程序可以按自己的方式任意解释底层的字节数组。结合使用 SequenceFile.Writer’s appendRaw() method 或 SequenceFileAsBinaryOutputFormat 创建顺序文件,
这提供了一种在 MapReduce 中使用任意二进制数据类型的方法(打包到一个顺序文件),虽然如此,插入到 Hadoop 序列化机制通常是一个更明确的方法。
FixedLengthInputFormat
-----------------------------------------------------------------------------------------------------------------------------------------
FixedLengthInputFormat 是为了从文件读取固定宽度( fixed-width )二进制记录,记录没有分隔符分隔。记录大小必须通过下面属性设置:
fixedlengthinputformat.record.length
4. 多个输入 ( Multiple Inputs )
------------------------------------------------------------------------------------------------------------------------------------------------------------------
虽然一个 MapReduce 作业可以由多个输入文件组成输入(联合使用文件 glob , filter , 以及纯粹的 path),所有的输入由同一个 InputFormat 解释 同一个 mapper 处理。
然而,数据格式往往会随时间而演变,这是经常发生的,因此必须必须写自己的 mapper 来应对所有的遗留的数据格式。或者有这样的数据源,提供相同的类型但有不同的数据格式。
例如,一个可能是 tab 分隔的纯文本( tab-separated plain text ),另一个是二进制的顺序文件。即便它们有相同的格式,也可能有不同的表现形式,因此需要分别解析。
这种场景可以由 MultipleInputs 类优雅地处理,它允许在路径基础上( on a per-path basis )指定 InputFormat 和 mapper 。例如:
MultipleInputs.addInputPath(job, ncdcInputPath,
TextInputFormat.class, MaxTemperatureMapper.class);
MultipleInputs.addInputPath(job, metOfficeInputPath,
TextInputFormat.class, MetOfficeMaxTemperatureMapper.class);
这段代码取代了通常调用 FileInputFormat.addInputPath() 和 job.setMapperClass() 。
MultipleInputs 类有个重载的 addInputPath() ,这个方法不使用 mapper:
static void addInputPath(JobConf conf, Path path, Class<? extends InputFormat> inputFormatClass)
当只有一个 mapper (由 Job 的 setMapperClass() 方法设置)但有多个输入格式的时候,这个方法很有用。
5. 数据库输入 ( Database Input (and Output) )
------------------------------------------------------------------------------------------------------------------------------------------------------------------
DBInputFormat 这种输入格式用于使用 JDBC 从关系数据库读取数据。因为它没有任何碎片化能力,需要非常小心,运行太多的 mapper 从数据库上读取数据可能使数据库受不了。
出于这个原因,最好使用载入相对小的数据集(loading relatively small datasets),利用 MultipleInputs 将它与 HDFS 大数据集连接。
与输出格式对应的 DBOutputFormat ,用于将作业的输出(中等规模的数据)转储到数据库(dumping job outputs (of modest size) into a database)。
另一个在关系型数据库和 HDFS 之间移动数据的方法是使用 Sqoop
HBase 的 TableInputFormat 设计用来允许一个 MapReduce 程序操作存储在 HBase 表内的数据。ableOutputFormat 用于将 MapReduce 输出写入到 HBase 表。
*
*
*
3 输出格式 ( Output Formats )
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Hadoop 针对输入格式都有对应的输出数据格式。
1. 文本输出(Text Output)
------------------------------------------------------------------------------------------------------------------------------------------------------------------
默认的输出格式, TextOutputFormat, 把记录按行写入文本。它的 key 和 value 可以是任意类型,因为 TextOutputFormat 通过调用它们的 toString() 方法转换成字符串。
每个 key-value 对由一个 tab 字符分隔,但可以通过 mapreduce.output.textoutputformat.separator 属性改变。与 TextOutputFormat 对应的输入格式是 KeyValueTextInputFormat ,
因为它通过基于一个可配置的分隔符把行切分成 key-value 对。
可以利用 NullWritable 类型让输出忽略 key 或 value (或者两个都忽略,使输出格式等同于 NullOutputFormat ,什么也不输出)。这也会导致没有分隔符的写出。这使得输出适合于
使用 TextInputFormat 读取。
2. 二进制输出 (Binary Output)
------------------------------------------------------------------------------------------------------------------------------------------------------------------
SequenceFileOutputFormat
-----------------------------------------------------------------------------------------------------------------------------------------
SequenceFileOutputFormat 将它的输出写为一个顺序文件。如果输出是作为后续深入处理的 MapReduce 作业的输入使用这是一个很好的选择,因为它是紧凑的并且方便压缩。
压缩由 SequenceFileOutputFormat 的静态方法控制。
static void setOutputCompressionType(JobConf conf, org.apache.hadoop.io.SequenceFile.CompressionType style)
SequenceFileAsBinaryOutputFormat
-----------------------------------------------------------------------------------------------------------------------------------------
SequenceFileAsBinaryOutputFormat 与 SequenceFileAsBinaryInputFormat 对应,将 key 和 value 以原始二进制的格式写入到一个顺序文件容器。
MapFileOutputFormat
-----------------------------------------------------------------------------------------------------------------------------------------
MapFileOutputFormat 将 map 文件作为输出。map 文件中的 key 必须有序添加,因此需要确保 reducer 输出的 key 是排过序的。
注意:
--------------------------------------------------------------------------------------------------------------------
reduce 输入的 key 一定是有序的,但输出的键是由 reduce function 控制的,在通用 MapReduce 协议中没有任何规定 reduce 输出的 key 必须是排序的。
reduce 输出的 key 要求排序, 这个额外的强制约束只有 MapFileOutputFormat 需要。
3. 多输出 (Multiple Outputs)
------------------------------------------------------------------------------------------------------------------------------------------------------------------
FileOutputFormat 及其子类产生的文件放在输出目录里。每个 reducer 一个文件,并且文件由分区号命名:part-r-00000, part-r-00001, and so on.
有时候需要对文件命名进行更多的控制或者每个 reducer 产生多个文件。 MapReduce 为此提供了 MultipleOutputs 类来帮助做到这些。
一个范例:划分数据 (Partitioning data)
-----------------------------------------------------------------------------------------------------------------------------
考虑这样一个问题,按气象站(weather station)分割气象数据集(weather dataset)。我们应该运行这样一个作业,它的输出是每个气象站对应的一个文件,每个文件包含那个站的所有记录。
一种方法是每个气象站对应一个 reducer 。为此,我们需要做两件事:第一,写一个分区器( partitioner )把同一个气象站的数据放到同一个分区内。
第二,设置这个作业的 reducer 数量和气象站的数量相同。 这个 partitioner 看起来这样:
public class StationPartitioner extends Partitioner<LongWritable, Text> {
private NcdcRecordParser parser = new NcdcRecordParser();
@Override
public int getPartition(LongWritable key, Text value, int numPartitions) {
parser.parse(value);
return getPartition(parser.getStationId());
}
private int getPartition(String stationId) {
...
}
}
getPartition(String stationId) method 没有给出实现,它把气象站 ID 转换成分区索引。这需要一个所有气象站 ID 的列表,然后返回列表里的气象站 ID 的索引。
这个方法有两个缺点:
第一个是由于作业运行之前需要知道分区数,因此也需要知道气象站的数量。
第二个缺点有些微妙,一般来说由应用程序严格地限定分区数量并不好,因为这样可能导致很小的或大小不均匀的分区。让很多 reducer 去做很少量的工作不是一个高效的作业组织方法。
更好的方法是用更少的 reducer 去做更多的工作,那样运行任务的开销就会降低。分区不均匀的情况也是很难避免的。
提示:
--------------------------------------------------------------------------------------------------------------------------
有两种特殊的情况让应用程序设定分区数量( 等同于 reducer 的数量)是有意义的:
① 0 个 reducer : 这是一种空的情况,没有分区,因为应用程序只需要运行个 map 任务。
② 1 个 reducer : 运行一些小作业把前面作业的输出合并成一个单一的文件,这是很方便的。前提是数据量要足够小以以便一个 reducer 可以轻松处理。
最好让集群决定作业的分区数量:思想是,越多的可用集群资源,作业完成得越快。这就是默认的 HashPartitioner 表现如此出色的原因。它可以处理任意数量的分区,
并且确保每个分区含有很好的 key 的混合,从而导致大小非常均匀的分区。
回到案例里来,如果使用 HashPartitioner ,每个分区会包含多个气象站,因此,每个站创建一个文件,我们需要安排每个 reducer 写到多个文件。这就 MultipleOutputs 出现的地方。
MultipleOutputs
-------------------------------------------------------------------------------------------------------------------------------
MultipleOutputs 允许把数据写到多个文件,文件的名称源自于输出的键和值, 或者实际上源自于任意的字符串。这允许每个 reducer (或者在 map-only job 里的 mapper ) 创建多个文件。
采用 name-m-nnnnn 形式的文件名用于 map 输出,name-r-nnnnn 形式的文件名用于 reduce 输出, name 是由程序设置的任意的名字, nnnnn 是一个整数表示分区号( the part number ),起始于 00000 。
分区号确保输出在名字相同时,从不同分区( mapper 或 reducer )写的文件不会冲突。
使用 MultipleOutputs 按气象站划分数据代码:
// Partitioning whole dataset into files named by the station ID using MultipleOutputs
public class PartitionByStationUsingMultipleOutputs extends Configured
implements Tool {
static class StationMapper
extends Mapper<LongWritable, Text, Text, Text> {
private NcdcRecordParser parser = new NcdcRecordParser();
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
parser.parse(value);
context.write(new Text(parser.getStationId()), value);
}
}
static class MultipleOutputsReducer
extends Reducer<Text, Text, NullWritable, Text> {
private MultipleOutputs<NullWritable, Text> multipleOutputs;
@Override
protected void setup(Context context)
throws IOException, InterruptedException {
multipleOutputs = new MultipleOutputs<NullWritable, Text>(context);
}
@Override
protected void reduce(Text key, Iterable<Text> values, Context context)
throws IOException, InterruptedException {
for (Text value : values) {
multipleOutputs.write(NullWritable.get(), value, key.toString());
}
}
@Override
protected void cleanup(Context context)
throws IOException, InterruptedException {
multipleOutputs.close();
}
}
@Override
public int run(String[] args) throws Exception {
Job job = JobBuilder.parseInputAndOutput(this, getConf(), args);
if (job == null) {
return -1;
}
job.setMapperClass(StationMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setReducerClass(MultipleOutputsReducer.class);
job.setOutputKeyClass(NullWritable.class);
return job.waitForCompletion(true) ? 0 : 1;
}
public static void main(String[] args) throws Exception {
int exitCode = ToolRunner.run(new PartitionByStationUsingMultipleOutputs(),
args);
System.exit(exitCode);
}
}
4. 延迟输出 ( Lazy Output )
------------------------------------------------------------------------------------------------------------------------------------------------------------------
FileOutputFormat 子类会创建输出( part-r-nnnnn )文件,即使它们是空的。 有些应用倾向于不创建空文件,这时,LazyOutputFormat 就派上用场了。
它是输出格式的封装,确保输出文件只有在分区的第一条记录输出的时候才创建。使用它,调用它的 setOutputFormatClass() method ,提供 JobConf 和 底层的 OutputFormat 即可。
5. 数据库输出 ( Database Output )
------------------------------------------------------------------------------------------------------------------------------------------------------------------
输出格式写到关系数据库和 Hbase 参见之前的章节 —— Database Input (and Output)