Hadoop中的MapReduce的祥解

Hadoop中的MapReduce的整体讲解

如果将 Hadoop 比作一头大象的话,那么 MapReduce 就是那头大象的大脑。 MapReduce是 Hadoop 核心编程模型。在 Hadoop 中,其数据处理核心为 MapReduce 程序设计模型。

 

MapReduce 把数据处理和分析分成两个主要阶段,即 Map 阶段和 Reduce 阶段。 Map 阶段主要是对输入进行整合,通过定义的输入格式获取文件信息和类型,并且确定读取方式,最终将读取的内容以键值对的形式保存。而 Reduce 是用来对结果进行后续处理,通过对你 Map 获取内容中的值进行二次处理和归并排序从而计算最终结果。

在 MapReduce 处理过程中,首先对数据进行分块处理,其后将数据信息交给 Map 任务去进行读取,对数据进行分类后写入,根据不同的键产生相应的键值对数据。之后进入Reduce 阶段,而 Reduce 的任务是执行定义的 Reduce 方法。使用具有相同键的值从多个数据表中被集合在一起进行分类处理,最终结果输出到相应的磁盘空间中。

 

框架分析与执行过程详解

以一个示例来讲解:

学校图书馆里有5个书架,每个书架上有3类图书(法律、医学、政治),每类图书中的书本个数不同。现在需要统计整个图书馆中这三类图书的数量总和。

 

为了完成这项工作,需要有三类角色的工作人员来进行处理:

角色

任务

管理员

安排人员工作,并监控工作情况

整理员

分别负责一个书架,并将不同类的图书进行分类

统计员

统计每个整理员整理后的书本数量

 

管理员先查看书架的数量,发现安排5个管理员,每人负责1个书架效率最高。再安排3个统计员来统计整理员完成的分类结果,第1个统计员负责第1个和第2个整理员的分类结果,第2个统计员负责第3个和第4个整理员的分类结果,第3个统计员负责第5个整理员的分类结果。

 

管理员同时监控每个人员的工作情况,如果有不认真工作的,警告多次无效,将踢出任务组,然后将任务重新分配。

 

正式框架流程图如下:


从图中我们看到, Hadoop 为每个创建的 Map 任务被分配输入文件的一部分,这部分称为 split。由每个分配的 split 来运行用户自定义的 Map 从而能够根据用户需要来处理每个 split中的内容。

一般情况下,一次 Map 任务的执行分成两个阶段:

   Map 读取 split 内容后,将其解析成键值对( key/Value)的形式进行运算,并将 Map定义的算法应用至每一条内容,而内容范围的确定可以根据用户自定义来确定。

   当使用 Map 中定义的算法处理完 split 中的内容后, Map 向 TaskTracker 报告,然后通知 JobTracker 任务执行完毕可以接受新的任务。

 

我们这里有必要介绍一下 split,对于将大数据分成若干个 split 来说,处理每个单独的split 内容所耗费的时间远远小于处理整个文件的时间。因此根据“木桶效应”,整个 Map 处理的速度则是有由集群中所有运行 Map 任务的节点计算机速度最慢的那个节点决定。如果将 split 分成较为细粒度的数据大小,而同时对不同的节点计算机根据其速度分配 split 个数,可以获得更好的负载均衡。大多数的 split 大小被设置成 64M,与前一章中的 HDFS 中 Block大小相同,这样做的好处是使得 Map 可以在存储有当前数据的节点上运行本地任务而不需要通过网络进行跨节点的数据调度。如果一个 Map 中需要的 split 大小超过 64M,则部分数据极大可能会存储在其他节点上,需要通过网络传输, Map 将增加等待时间从而降低效率,而 split 分的较小的话,则会对当前节点中 Block 中的容量进行浪费,并增加了 split 的个数,Map 对 Split 进行计算并上报结果,关闭当前计算及打开新的 split 均需要耗费大量资源,这样做的话也会降低 Map 的处理效率。

 

现在让我们回到上图,对于Reduce 来说,通常一个MapReduce 任务过程中,可以有若干个 Map 任务,但只有一个 Reduce 任务,其作用是接受并处理所有 Map 任务发送过来的输出。排过序的 Map 任务输出结果通过网络传输方式将结果报送给 Reduce 节点,所有输出结果在 Reduce 端合并,而对于 Reduce 的结果,则会存储成 3 个副本进行备份性质存储。

一次 Reduce 任务的执行分成三个阶段:

   Reduce 获取 Map 输入的处理结果。

   将拥有相同键值对的数据进行分组。

   Reduce 将用户定义的 Reduce 算法应用到每个键值对确定的列表中。

 

下面我们分析一下这个过程,在数据的输入阶段, Reduce 通过向 TaskTracker 发送请求来获取 Map 的任务数据, JobTracker 将主机的 Map 输出的每个 TaskTracker 位置传递到执行Reduce 任务的 TaskTracker。在接收到所有 Map 输出的数据后, Reduce 进入分组和排序阶段,每个 Map 输出的结果此时应该已经应该按照键的大小排列。 Reduce 根据键的同一性将这些结果进行合并,成为一个键对应多个值得一系列数据记录,然后通过自定义的算法对排序结果进行处理。

 

MapReduce 输入输出与源码分析

下面我们开始学习 MapReduce 源码。

首先我们查看 MapReduce 中 Mapper 方法的源码,源码内容如下所示:

publicclassMapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {

        publicclass Context extends MapContext<KEYIN, VALUEIN, KEYOUT,VALUEOUT> {

                 public Context(Configuration conf,

                                   TaskAttemptIDtaskid, // 设定 context 接受的内容

                                   RecordReader<KEYIN,VALUEIN>reader, RecordWriter<KEYOUT, VALUEOUT>writer,

                                   OutputCommittercommitter, StatusReporter reporter,InputSplit split) throws IOException,

                                   InterruptedException{

                          super(conf, taskid, reader, writer, committer, reporter, split); //调用父类构造方法

                 }

        }

 

        // setup方法可以为 Map 方法提供预处理的一些内容。

        protectedvoid setup(Context context) throwsIOException, InterruptedException {

        }

 

        protectedvoid Map(KEYIN key, VALUEIN value, Context context) throws IOException, InterruptedException {

                 context.write((KEYOUT) key, (VALUEOUT) value); // 处理结果加载进 context

        }

 

        protectedvoid cleanup(Context context) throws IOException, InterruptedException {

        }

 

        publicvoid run(Context context) throws IOException,InterruptedException {

                 setup(context); // 启动预处理方法

                 while (context.nextKeyValue()) { // 未到数据结尾

                          Map(context.getCurrentKey(), context.getCurrentValue(),context); // 进行键值对的处理

                 }

                 cleanup(context); // 进行扫尾工作

        }

}

 

当我们写 MapReduce 程序的时候,我们写的任何一个 Mapper 都要继承这个 Mapper 类,通常我们会重写 Map()方法,在 Map 方法中定义了三个参数,首先我们来看前两个参数,分别是作为输入的 key 和 value,因此从这里我们也可以看到 MapReduce 的运作完全基于基本的<key,value>对,即数据的输入是一批<key,value>对,生成的结果也是一批<key,value>对,只是有时候它们的类型不一样而已。 Key 和 value 的类由于需要支持被序列化( serialize)操作,所以它们必须要实现我们在上一章中学习的 Writable 接口,而且 key 的类有时还必须实现 WritableComparable 接口,使得可以让 MapReduce 对数据输出的结果上执行排序操作。

 

最后我们看看 Mapper.class 中的 run()方法,它相当于 Mapper 类的驱动,我们可以看到run() 方法首先调用 setup() 进行初始操作,然后对每个 context.nextKeyValue() 获取的<key,value>对调用 Map()函数进行处理,最后调用 cleanup()做最后的处理。对于 Context 类的分析我们在后面章节的 MapReduce 工作机制中进行分析。

 

而对于 Reducer 类来说,其源码如下:

publicclassReducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {

        publicclass Context extends ReduceContext<KEYIN, VALUEIN, KEYOUT,VALUEOUT> {

                 // 获取从 context 中传递进来的内容

                 public Context(Configuration conf,TaskAttemptID taskid, RawKeyValueIterator input,

                                   CounterinputKeyCounter, Counter inputValueCounter, RecordWriter<KEYOUT, VALUEOUT>output,

                                   OutputCommittercommitter, StatusReporter reporter,RawComparator<KEYIN>comparator,

                                   Class<KEYIN>keyClass, Class<VALUEIN>valueClass) throws IOException, InterruptedException {

                          //调用父类构造方法生成context 实例

                          super(conf, taskid, input, inputKeyCounter, inputValueCounter, output, committer, reporter,

                                           comparator, keyClass, valueClass);

                 }

        }

 

        // 预执行设定

        protectedvoid setup(Context context) throwsIOException, InterruptedException {

        }

 

        // Reduce核心内容,将value值进行迭代获取从而为下文的写入提供数据

        protectedvoid Reduce(KEYIN key, Iterable<VALUEIN>values, Context context) throwsIOException,

                          InterruptedException{

                 for (VALUEIN value : values) { //迭代获取数据

                          context.write((KEYOUT) key, (VALUEOUT) value); // 开始写入结果

                 }

        }

 

        // 执行扫尾工作

        protectedvoid cleanup(Context context) throws IOException, InterruptedException {

        }

 

        // 驱动方法

        publicvoid run(Context context) throwsIOException, InterruptedException {

                 setup(context); // 开始内容

                 while (context.nextKey()) { // 确认读取到结尾

                          Reduce(context.getCurrentKey(), context.getValues(),context);

                 }

                 cleanup(context); // 扫尾工作

        }

}

 

从Reducer 类中的源代码可以看到, setup 与 cleanup 分别提供了预执行和扫尾工作的支持,而 Reducer 类中核心方法 Reduce 其中接受的 key 为自定义的 key 类型,值得我们注意的是,对于 Map 和 Reduce 方法来说,其要求输出和输入的 key 与 value 类型一致。例如对于Map 产生的数据结果类型为<IntWritable, Text>,那么根据 context 存入的数据类型也是一样。

 

而 Reduce 在对输入数据进行处理时,因为传递过来的数据类型已经由 Map 确定,因此在获取数据时,也必须根据 Map 传递过来的数据类型进行类型的转换。Reduce 方法将传递过来的 value 值根据键的相同与不同进行重排序,从而对于 value 来说形成了一个列表,列表的构成是根据 Map 结果具有相同 key 的值合并而成的。通过对列表的迭代可以让 Reduce 获得每一个 key 对应的 value 值,进而对所有数据进行计算。

 

通过以上分析我们可以了解到,一个 Map 任务的执行过程以及数据输入输出形式如下所示:

Map :<k1,v1> ——>list<k2,v2>

Map 接受输入的数据,并采用键值对的形式存储(k1,v1),然后通过自定义的算法,将符合的数据进行分类,根据相同 key 值生成若干条列表,此列表存储着具有相同 key 值的 value 组成的键值对(list<k2,v2>)。

 

而 Map 方法制定的第三个参数,通过指定一个 Context 实例,可以认为是系统内部上下文环境,用来存储 Map 方法处理后产生的输出记录。供后续使用。

下面我们来看下 Reduce 方法,与 Map 方法不同的是,对于接收数据, Reduce 自动对其进行分组,因为 Reduce 可能接受若干个 Map 计算结果组成的一个数据集,而这样数据集在经过接受阶段后,正式使用 Reduce 算法对需要处理的数据进行处理之前已经进行初步预处理,即根据相同的键将值组成一个队列进行分类。

一个 Reduce 任务执行过程以及数据的输入输出形式如下所示:

Reduce:<k2,list<v2>>——><k3,v3>

 

MapReduce 中 Job 类详解

Job 类是 Hadoop 中一个特殊的类,其主要作用是给予设计设计人员对 MapReduce 过程的一个细节上操控的作用。通过对 Job 的设定,我们可以很容易配置任务、获取任务配置配置、以及提交任务的功能,以及跟踪任务进度和控制任务过程。 Job 其实就是 Job 类继承于JobContext 类。而 JobContext 本质上就是提供了获取任务配置的功能,如任务 ID,任务的Mapper 类, Reducer 类,输入格式,输出格式等等,它们除了对任务 ID 设置之外,都是只读的。 Job 类在 JobContext 的基础上,提供了设置任务配置信息的功能、跟踪进度,以及提交作业的接口和控制作业的方法。 Job 类的源码如下所示:

 

publicclassJob extends JobContext {

        publicstaticenum JobState {

                 DEFINE, RUNNING

        }; // 设置运行状态

 

        private JobState state = JobState.DEFINE; //设置默认的 Job 状态

        private JobClient jobClient; //设置一个任务运行服务实例

        private RunningJob info; //RunningJob 为获取运行时信息提供帮助

 

        // 根据环境变量获取 Job的一个实例。

        public Job(Configuration conf) throws IOException {

                 super(conf, null); // 调用父类构造方法

                 // 构造一个任务运行服务实例

                 jobClient = new JobClient((JobConf) getConfiguration());

        }

 

        // reducr 任务数进行设置

        publicvoid setNumReduceTasks(inttasks) throwsIllegalStateException {

                 ensureState(JobState.DEFINE); //确认状态

                 conf.setNumReduceTasks(tasks); // 进行环境变量设置

        }

 

        publicvoid setInputFormatClass(Class<? extends InputFormat>cls) throwsIllegalStateException {

                 ensureState(JobState.DEFINE); //确认状态

                 conf.setClass(INPUT_FORMAT_CLASS_ATTR,cls, InputFormat.class); // 环境中设置输入类型

        }

 

        publicvoid setOutputFormatClass(Class<? extends OutputFormat>cls) throws IllegalStateException {

                 ensureState(JobState.DEFINE); //确认状态

                 conf.setClass(OUTPUT_FORMAT_CLASS_ATTR,cls, OutputFormat.class); // 环境中设置输出类型

        }

 

        publicvoid setJarByClass(Class<?>cls) { // 设置Jar包执行类

                 conf.setJarByClass(cls); // 在环境变量中设置

        }

 

        publicvoid setReducerClass(Class<? extendsReducer>cls) throws IllegalStateException {

                 ensureState(JobState.DEFINE); //确认状态

                 conf.setClass(REDUCE_CLASS_ATTR,cls, Reducer.class); //设置 Reducer 类型

        }

 

        publicvoid setOutputKeyClass(Class<?>theClass) throws IllegalStateException {

                 ensureState(JobState.DEFINE); //确认状态

                 conf.setOutputKeyClass(theClass); // 设置输出的键类型

        }

 

        publicvoid setOutputValueClass(Class<?>theClass) throws IllegalStateException {

                 ensureState(JobState.DEFINE); //确认状态

                 conf.setOutputValueClass(theClass); // 设置输出值类型

        }

 

        // 结束当前任务

        publicvoid killTask(TaskAttemptID taskId) throws IOException {

                 ensureState(JobState.RUNNING); //确认状态

                 info.killTask(org.apache.hadoop.Mapred.TaskAttemptID.downgrade(taskId), false);

        }

 

        public Counters getCounters() throwsIOException {

                 ensureState(JobState.RUNNING); //确认状态

                 returnnew Counters(info.getCounters()); // 获取计数器

        }

 

        publicvoid submit() throws IOException, InterruptedException,ClassNotFoundException {

                 ensureState(JobState.DEFINE); //确认状态

                 setUseNewAPI();// 确认使用新 API

                 info = jobClient.submitJobInternal(conf); // 对任务运行状态生成一个实例

                 state = JobState.RUNNING; // 改变默认的运行状态

        }

 

        publicboolean waitForCompletion(booleanverbose) throwsIOException, InterruptedException,

                          ClassNotFoundException{

                 if (state == JobState.DEFINE) { //确认状态,为运行则启动任务

                          submit();

                 }

                 if (verbose) { //决定是否输出信息至用户处

                          jobClient.monitorAndPrintJob(conf, info);

                 } else {

                          info.waitForCompletion();

                 }

                 return isSuccessful(); // 确认返回结果

        }

}

 

通过分析源代码可知,一个 Job 对象实例有两种状态, DEFINE 和 RUNNING, Job 对象被创建时的状态时 DEFINE,当且仅当 Job 对象处于 DEFINE 状态,才可以用来设置作业的一些配置,如 ReduceTask 的数量, InputFormat 类,工作的 Mapper 类, Partitioner 类等等,这些设置是通过设置配置信息 conf 来实现的;

 

当一个 MapReduce 通过 submit()被提交,就会将这个 Job 对象的状态设置为 RUNNING,这时候作业已经提交了,就不能再设置上面那些参数了,作业处于调度运行阶段。处于 RUNNING 状态的作业我们可以获取作业、 Map task 和Reduce task 的进度,通过代码中的 Progress()获得,这些函数是通过 info 来获取的, info 是RunningJob 对象,它是实际在运行的作业的一组获取作业情况的接口,如 Progress。

 

在 waitForCompletion()中,首先用 submit()提交作业,然后等待 info.waitForCompletion()返回作业执行完毕。 verbose 参数用来决定是否将运行进度等信息输出给用户。 submit()首先会检查是否正确使用了 new API,这通过 setUseNewAPI()检查旧版本的属性是否被设置来实现的,设置是否使用 new API 是因为执行 Task 时要根据使用的 API 版本来执行不同版本的MapReduce,在后面讲 MapTask 时会说到。接着就 connect()连接 JobTracker 并提交。实际提交作业的是一个 JobClient 对象,提交作业后返回一个 RunningJob 对象,这个对象可以跟踪作业的进度以及含有由 JobTracker 设置的作业 ID。

 

getCounter()函数是用来返回这个作业的计数器列表的,计数器被用来收集作业的统计信息,比如失败的 Map task 数量, Reduce 输出的记录数等等。它包括内置计数器和用户定义的计数器,用户自定义的计数器可以用来收集用户需要的特定信息。计数器首先被每个 task定期传输到 TaskTracker,最后 TaskTracker 再传到 JobTracker 收集起来。这就意味着,计数器是全局的。后面我们会介绍计数器的使用。

 

而对于 Job 类的使用,通常是在 main 函数里建立一个 Job 对象,设置它的 JobName,然后配置输入输出路径,设置我们的 Mapper 类和 Reducer 类,设置 InputFormat 和正确的输出类型等等。然后我们会使用 job.waitForCompletion()提交到 JobTracker,等待 job 运行并返回,这就是一般的 Job 设置过程。 JobTracker 会初始化这个 Job,获取输入分片,然后将一个一个的 task 任务分配给 TaskTrackers 执行。 TaskTracker 获取 task 是通过心跳的返回值得到的,然后 TaskTracker 就会为收到的 task 启动一个 JVM 来运行。

 

MapReduce 的计数应用

首先我们在 HDFS 中建立我们所需要处理的文本文件,我们采用建立文件文件的方式是在 HDFS 中创建一个新的文件,然后将文字内容写入 HDFS 中,代码如下:

 

publicclassPreTxt {

        static String text = "hello world goodbye world \n" + //设置一个待写入字符串

                          "hello hadoop goodbye hadoop";

        publicstaticvoid main(String[] args) throws Exception {

                 Configurationconf = new Configuration(); // 获取环境变量

                 Pathfile = new Path("hdfs://localhost:9000/wufan/preTxt.txt");

                 FileSystemfs = file.getFileSystem(conf);

                 FSDataOutputStreamfsout = fs.create(file); // 创建写入内容

                 try {

                          fsout.write(text.getBytes());// 将字符串写入

                 } finally {

                          IOUtils.closeStream(fsout); // 关闭输出流

                 }

        }

}

 

MapReduce 过程分析

在前文已经说过, MapReduce 对数据进行处理的时候,分成两个主要阶段,分别是 Map阶段和 Reduce 阶段。而对于每个阶段来说,虽然处理顺序以及处理方法有所不同,但是有一点是相同,就是都需要键值对作为其基本输入输出的数据基本类型,而键值对的具体类型则又是由程序设计人员制定的。

 

除此之外,我们在上一小节中中的 Map 和 Reduce 的源代码分析中看到,对于任何继承Mapper 或者 Reducer 的类来说,都有一些公共的方法需要去继承或者实现,例如 setup 与chean,而其中最重要的就是 Map 方法与 Reduce 方法。

 

对于 Map 阶段来说,其作用是将原始数据输入到 MapReduce 处理系统内,为了简单起见,我们选择文本格式( txt)作为我们的输入文件格式,以便 Map 在输入时候可以根据文件的每一行进行作为一个单独的键值进行数据输入。我们在定义 Map 方法的时候,对于 key 的类型是定义为 LongWritable,这个是默认的 key 类型,指的是该行起始位置相对于整个文件位置的偏移量。

 

我们的待分析的文件很简单,就是传进去一个字符串,计算字符串中有多少个单词。为了达成这这个功能,一般我们只对一种属性感兴趣,那就是单词本身。所以只需要分别取出单词即可。

 

为了更好地了解 Map 过程,首先我们先观察我们准备的数据:

helloworld goodbye world

hellohadoop goodbye hadoop

数据是两行长度不同的字符串,通过换行符进行换行,前面我们已经说过,对于 Map 方法来说,由于设定的键值对的类型为<LongWritable,Text>,那么数据在传递到 Map 方法时表示如下:

(0 , hello world goodbye world )

(25,hello hadoop goodbye hadoop )

每一行的数字部分,也就是单词的前面数字是文件每行起始第一个单词的偏移量,使用LongWritable 类型进行定义。我们在这个例子中并不需要这些信息,所以可不对其进行处理。

 

我们通过自定义 Map 方法,将每行分割成一个一个单独单词,使用单词作为其 key 值,而计数 1 作为其 value 值,处理结果如下所示:

(hello, 1 )

(world, 1 )

(goodbye, 1 )

(world,1 )

(hello, 1 )

(hadoop, 1 )

(goodbye,1 )

(hadoop, 1 )

这样数据结果在被 Map 阶段处理后,最后被发送 Reduce 阶段记性下一步处理。在 Reduce处理阶段前,还有一个 shuffle 过程,此过程对输送过来的数据根据键值对中 key 的具体数值进行重新排序和分组,我们忽略了具体细节,对于 Reduce 方法,所看到的结果如下所示:

(hello, [1,1 ])

(world, [1,1 ])

(goodbye, [1,1 ])

(hadoop, [1,1 ])

每一个单词作为 key 的后面紧跟着一个 list, list 中的内容就是所有 Map 处理结果中具有相同 key 值得 value 集合,下面的任务就是使用 Reduce 方法来遍历整个 value 列表从而求出所有计数的和。结果如下所示:

(hello, 2)

(world, 2)

(goodbye, 2)

(hadoop, 2)

这个是最终输出,对每个单词出现的次数进行计数。

 

计数程序的 MapReduce 实现

我们主要要完成两个方面的东西,一个是实现自己的 Mapper 类,另一个就是实现自己的 Reducer 类,下面我们分部来看。

 

对于 Mapper 类来说,我们需要继承自 Hadoop 自带的 Mapper 类,这个类包含在org.apache.hadoop.MapReduce.Mapper包中,具体实现代码如下:

publicclassTxtMapper extends Mapper<LongWritable, Text, Text,IntWritable> {

        protectedvoid Map(LongWritable key, Text value, Context context) throwsjava.io.IOException,

                          InterruptedException{

                 // 将字符串分割成字符串数组,每个元素为一个独立单词

                 String[]strs = value.toString().split(" ");

                 for (String str : strs) { //遍历字符数组

                          context.write(new Text(str), new IntWritable(1));

                 }

        };

}

 

该Mapper 类是一个泛型类,具有四个基本形参类型,分别用来指定 Map 方法的输入键,输入值,输出键,输出值的类型,这个类型具体形式没有规定,可以是基本类型也可以是上一章中实现了 Writable 类型的自定义数据类型。就我们使用的例子来说,默认输入键是LongWritable 类型,输入值是 Text 类型,输出键是 Text 类型,输出值是 IntWritable。

 

在程序中,我们自定义的 Map 方法,其输入是一个键和一个值。 Text 类型的变量所具有的的使用方法较少,因此我们可以首先通过 Text 类型的 toString 方法将 Text 类型转换成 String 类型,之后使用 split 方法将一行字符串分割成一个字符数组,以待后续的写入工作使用。

 

而对于生成的字符数组, Map 方法提供了一个 Context 实例以便进行在系统内部的上下文中写入。同时我们需要注意的是我们在自定义的 Mapper 类中已经定义了输出键值对的类型,所以需要对写入上下文的类型进行重新包装,将字符串数组中的元素依次取出包装成一个 Text 类型,同时生成一个包装成 IntWritable 类型的数字 1 一并写入相对于的键值对中。

 

Map 结束后,生成的结果如下所示:

(hello , 1 )

(world , 1 )

(goodbye , 1 )

(world, 1 )

(hello , 1 )

(hadoop , 1 )

(goodbye, 1 )

(hadoop , 1 )

 

可以很明确的看到,在 Map 中是提取单独的单词作为 key,并将其值统一赋值为 1,表明其输入数据中此单词已出现一次。

此后在 Map 和 Reduce 过程之间还有一个 Shuffle 过程,其将具有相同 key 的值组成一个列表。此行为为 Hadoop 框架自动完成,无需干预。

 

Shuffle 结果如下所示:

(hello , [1,1 ])

(world , [1,1 ])

(goodbye , [1,1 ])

(hadoop , [1,1 ])

 

下面需要实现的自定义 Reducer 的类,具体如下程序所示:

 

publicclassTxtReducer extends Reducer<Text, IntWritable, Text,IntWritable> {

        protectedvoid Reduce(Text key, Iterable<IntWritable>values, Context context)

                          throws java.io.IOException, InterruptedException {

                 intsum = 0; // 设置辅助求和值

                 Iterator<IntWritable>it = values.iterator(); //获取迭代实例

                 while (it.hasNext()) { // 迭代开始

                          IntWritablevalue = it.next(); // 获取当前元素值并进行类型转换

                          sum += value.get(); // 进行计数求和

                 }

                 context.write(key, newIntWritable(sum)); // 重新将值写入

        }

}

 

与Mapper 类型相类似, Reducer 也是一个泛型类,其定义了四个参数用于指定其输入类型与输出类型。在这里我们需要注意的是, Reduce 的输入类型已经由 Mapper 定义。而Reducer 是对输入数据进行再次处理,所以 Reducer 中输入数据的类型必须与 Mapper 中输出类型相一致。在此例中 Mapper 定义的输出类型是 Text 与 IntWritable,所以相应的处理类Reducer 的输入类型也必须是 Text 与 IntWritable。而输出类型则可以根据我们需要进行重新的自定义。在这里 Reducer 的作用是对输入的键值对计数,因此输出值也应该是一个计数结果,所以我们采用的也是 Text 与 IntWritable。一定请读者注意的是, Reducer 中的输出键值对类型并不一定要和其本身输入键值对类型相同,而是根据情况进行选择使用。

 

在 Reduce 方法中, values 是一个实现了 Iterable 接口的实际类型,这里使用了多态表明了传递进来的 values 类型能够被迭代获取其中的具体元素。其实际上是一个由多个具有相同key 值得 value 值组成的列表。每个列表中包含一系列的元素。此例中的元素值就是我们在Map 过程中赋给每个 key 的 value 值。进过类型转换,想相关的值转为 int 类型后进行求和操作,最终将求得的计算结果重新写入 context 实例中。

 

计数程序的 main 方法

对于 MapReduce 运行的任务来说,任何一个 MapReduce 都是通过使用 Job 进行驱动的,其通过生成一个 Jar 包的形式将代码包装在一起并上传到 Hadoop 集群执行中,然后通过节点进行执行。

 

在上一节 Job 的源码分析中我们可以知道,对于 Job 类来说,我们需要设置专门的输入和输出的路径,输入路径是供给 Map 使用,而输出路径可以供给 Map 使用也可以供给 Reduce使用(如果不执行 Reduce 任务的话)。

 

输入的设定就是使用 FileInputFormat 类中的静态addInputPath 方法。而输出的设定是是使用 FileOutputFormat 中的 setOutputPath,同样也是静态的,这两个方法采用的是一种工厂设计模式,将路径直接设置。需要了解的是FileInputFormat 类中还有一个 addInputPaths,从名称上看是设置多个输入路径供 Map 使用。

 

对于输出输入的格式设定需要调用 Job 类中的 setOutputKeyClass 与 setOutputValueClass,这点要与在设定 Map 中的输出的键值对格式一样。

 

在设置完一些基本的环境属性后,即可开始运行我们第一个 MapReduce 程序,通过调用 waitForCompletion()即可执行此 MapReduce 完整的程序。 waitForCompletion 中的属性用以判断是否将结果显示到输出控制台上,这里我们建议设置为 true。

publicclassTxtCounter {

        publicstaticvoid main(String[] args) throws Exception {

                 Pathfile = new Path("preTxt.txt"); // 设置 Map 输入的文件路径

                 PathoutFile = new Path("countResult"); // 设置输出文件路径

                 Jobjob = new Job(); // 创建一个新的任务

                 job.setJarByClass(TxtCounter.class); // 设置主要工作类

                 FileInputFormat.addInputPath(job, file); //添加输入路径

                 FileOutputFormat.setOutputPath(job, outFile); //添加输出路径

                 job.setMapperClass(TxtMapper.class); // 设置Mapper

                 job.setReducerClass(TxtReducer.class); // 设置Reduce

                 job.setOutputKeyClass(Text.class); // 设置输出 key 格式

                 job.setOutputValueClass(IntWritable.class); // 设置输出 value 格式

                 job.waitForCompletion(true); // 运行任务

        }

}

 

Mapper 中的 Combiner 详解

我们知道,网络带宽总是一种稀缺资源,集群上的可用带宽限制了 MapReduce 任务的数量,因此我们运行 MapReduce 任务最重要的一点是尽量避免 Map 任务和 Reduce 任务之间的数据传输,从而大大提高效率。采用的方法很多,例如压缩数据,改变数据类型等。

 

Hadoop 允许用户针对 Map 任务的输出指定一个合并方法(文中有时也称作 combiner,就像Mapper 和 Reducer 一样)合并方法的输出作为 Reduce 方法的输入。值得注意的是由于合并方法是一个优化方案,所以 Hadoop 无法确定针对 Map 任务输出中任一条记录需要调用多

少次合并函数(如果需要)。换言之,不管调用合并函数多少次, 0 次、 1 次或多次, Reducer的输出结果都应一致。

 

下面我们回头看下我们关于单词计数的例子,在本例中, Map 输出值为:

(hello, 1 )

(world, 1 )

(goodbye, 1 )

(world,1 )

(hello, 1 )

(hadoop, 1 )

(goodbye,1 )

(hadoop, 1 )

 

这里有 8 行数据,如果我们在将数据传入 Reduce 之前,进行一次 Combiner,调用的是 Reducer的处理方法,那么结果如下:

(hello, 2)

(world, 2)

(goodbye, 2)

(hadoop, 2)

这样就可以直接在 Mapper 中预先对数据进行处理,使用 Combiner 方法对相同 key 值得数据进行计数。具体代码如下所示

job.setCombinerClass(TxtReducer.class);

大家自行添加。

 

Map 相关子类介绍

Mapper 是一个预处理的类,其功能是从文件中读取数据,并按给定的方法对数据进行预处理,处理的结果遵循一定的方式传递给 Reducer 进行后续处理。对于自定义 Mapper 类来说,继承自默认的 Mapper 就可以解决大多数的处理问题,但是有时候需要采用一些特殊的方法来对数据进行处理,这里我们介绍一下 Mapper 的三个子类。

 

TokenCounterMapper类

TokenCounterMapper 类的源码如下所示:

 

publicclassTokenCounterMapper extends Mapper<Object, Text, Text,IntWritable> {

        privatefinalstatic IntWritable one = newIntWritable(1); // 定义默认数字 1

        private Text word = new Text(); // 生成辅助的 Text 类型

 

        @Override

        publicvoid Map(Object key, Text value, Context context) throws IOException, InterruptedException {

                 StringTokenizeritr = new StringTokenizer(value.toString());// 分割字符串

                 while (itr.hasMoreTokens()) { // 获取字符串

                          word.set(itr.nextToken()); // 对默认的 Key 进行设置

                          context.write(word, one); //写入

                 }

        }

}

 

用此类Map完全可以取代上面的计数程序中的MapClass。

 

InverseMapper类

publicclassInverseMapper<K, V>extends Mapper<K, V, V, K> {

        @Override

        publicvoid Map(K key, V value, Context context) throws IOException, InterruptedException {

                 context.write(value, key); //键值对互换

        }

}

 

这个类更加简单,它仅仅是调换 Key 和 Value,然后直接分发出去。举个例子:Key是订单,Value是物品,可以交换后查询物品都在哪些订单中存在。

 

MultithreadedMapper类

对于多线程来说, MultithreadedMapper 更适合应用于多线程环境下,它是使用多线程来执行一个 Mapper,其主要是对 run 方法进行了重写,使之支持多线程,使用默认的线程数来执行任务,源代码如下所示:

 

publicclassMultithreadedMapper<K1, V1, K2, V2>extendsMapper<K1, V1, K2, V2> {

        private Class<? extends Mapper<K1, V1, K2, V2>>MapClass;

        private Context outer; //设置一个 Context 引用

        private List<MapRunner>runners; // 创建一个线程容器

 

        publicstaticint getNumberOfThreads(JobContext job) { // 设置默认线程数

                 // 设置线程数为10

                 returnjob.getConfiguration().getInt("Mapred.Map.multithreadedrunner.threads", 10);

        }

 

        // 重写run方法,提供多线程能力

        publicvoid run(Context context) throwsIOException, InterruptedException {

                 runners = new ArrayList<MapRunner>(numberOfThreads);// 实例化多线程容器

                 for (inti =0; i<numberOfThreads; ++i) { // 按线程数添加实例

                          MapRunnerthread = new MapRunner(context); // 生成新的 Mapper 由每个线程负责调度

                          thread.start(); // 线程启动

                          runners.add(i, thread); //将线程添加进容器中

                 }

                 for (inti =0; i<numberOfThreads; ++i) { // 进行确认

                          MapRunnerthread = runners.get(i); // 获取线程实例

                          thread.join(); // 等待启动完毕

                 }

        }

}

从源代码可见, MultithreadedMapper 具有一个单独 MapClass 引用,这个引用指定另一个 Mapper 类[暂称 workMapper,由Mapred.Map.multithreadedrunner.class 设置],实际干活的其实是这个 Mapper 类而不是 MultithreadedMapper。 runnsers 是运行的线程的容器。

 

从上面的代码我们可以看到,首先它设置运行上下文 context 和 workMapper,然后启动多个 MapRunner子线程(由 Mapred.Map.multithreadedrunner.threads设置),然后使用 join()等待子线程都执行完毕。

MapRunner 继承了 Thread,它包含了一个独享的 Context: subcontext,以及用 Mapper

指定了 workMapper,然后 throwable 是在 MultithreadMapper 的 run()中进行综合的异常处理的。

MultithreadedMapper 适用于 CPU 密集型的任务,采用多个线程处理后,一个线程可以在另外的线程在执行时读取数据并执行,这样就使用了更多的 CPU 周期来执行任务,从而提高吞吐率。注意读写操作都是线程安全的,因此不难想象对于 IO 密集型的作业,采用MultithreadedMapper 会适得其反,因为会有多个线程等待 IO, IO 成为限制吞吐率的关键。

 

对于 IO 密集型的任务,我们应该采用增多 task 数量的方法来解决,因为这样在 IO 上就是并行的。除非 Map()的确是 CPU 密集型的,否则不推荐使用 MultithreadedMapper,而建议采用更多的 Map task。

 

MapReduce 配置与测试

在上一章中,我们学习并实现了第一个自定义的 MapReduce 应用程序,也体验了使用 MapReduce 来进行计算的便捷性,同时也初步了解了 MapReduce 的模型框架。但是读者可能已经注意到,我们在写应用程序的时候,所使用的配置及对驱动程序的设置都是默认的设置。在这一章中,我们将学习 MapReduce 的高级设置,这有助于我们编写更为强大的MapReduce 应用程序。

 

MapReduce 环境变量配置详解

配置新的配置文件

我们集群搭建是提供了四个配置属性,这四个属性均包括在 conf 文件夹下的 xml 文件,Hadoop 通过载入 xml 文件的方式读取其中的内容,而此种读取方式又是通过实现一个Configuration 的实例来完成,通过读取 xml 中每个属性对应的名称来获取设置的值。在使用 Hadoop 中的程序代码读取 xml 文件之前,我们首先要学习下如何配置一个新的xml 文件。请打开$HADOOP_HOME/conf 文件夹,也就是 Hadoop 目录文件夹下的 conf

文件夹,新建一个文件命名为 myConfiguration.xml,然后在其中写入如下内容:

<?xmlversion="1.0"?>

<configuration>

        <property>

                 <name>flower</name>

                 <value>rose</value>

                 <description>flower</description>

        </property>

        <property>

                 <name>color</name>

                 <value>red</value>

                 <description>color</description>

        </property>

        <property>

                 <name>blossom</name>

                 <value>is</value>

                 <final>true</final>

                 <description>blossom</description>

        </property>

</configuration>

 

从实例代码我们能够看到, xml 文件的定义就是通过简单的结构体定义的一种键值对映射。其中每个属性的名称都是由一个自定义的 String 类型来确定。因此我们完全可以通过 Hadoop读取属性的方式将属性信息读取出来。需要注意的是,对于第三个属性 blossom 来说,有一个 final 属性,其标示此属性不能被覆盖,默认值为 false,对于不能覆盖的属性要显式的制定为 true。

 

首先我们来看下 Configuration 的部分相关源代码,源代码如图下所示:

        publicvoid addResource(String name) {

                 addResourceObject(name); // 调用添加属性文件

        }

 

        privatesynchronizedvoid addResourceObject(Object resource) {

                 resources.add(resource); // 添加属性文件

                 reloadConfiguration();// 重载环境变量

        }

 

Configuration 在源代码中定义了 addResource 方法,而 addResource 方法又重新调用了addResourceObject 方法用以将自定义的配置文件加载到属性变量中,并且重载了环境变量设置。

 

对于读取属性文件, Hadoop 中的 Configuration 为我们准备了读取文件的方法,源代码如下所示:

        public String get(String name) { // 读取配置文件

                 return substituteVars(getProps().getProperty(name)); // 调用配置方法

        }

 

Configuration.get()会调用Configuration的私有方法substituteVars(),该方法会完成配置的属性扩展。substituteVars()中进行的属性扩展,不但可以使用保存在Configuration对象中的键-值对,而且还可以使用Java虚拟机的系统属性。

 

具体代码如下所示:

publicclassXmlReader {

        publicstaticvoid main(String[] args) {

                 Configurationconf = new Configuration(); // 设置环境变量实例

                 conf.addResource("myConfiguration.xml"); //添加自定义配置文件

                 System.out.println("flower name is " + conf.get("flower")); // 获取属性值

                 System.out.println("flower color is " + conf.get("color")); // 获取属性值

                 System.out.println("blossomed ? " + conf.get("blossom")); // 获取属性值

        }

}

 

运行命令行代码如下所示:

$ HadoopJar XmlReader.Jar XmlReader

输出结果如下所示:

flowername is rose //显示 name 属性

flowercolor is red //显示 clolr 属性

blossomed? Is //显示 blossom 属性

 

修改已有的配置文件

当使用 MapReduce 进入集群工作的时候,对于属性的设置于更改则属于家常便饭。因此当我们需要重新配置多个属性文件,并且属性文件中的部分属性值有冲突时,那么会发生什么情况呢?针对此种问题,我们先看一个例子,假如说我们在配置 myConfiguration.xml 的同一文件夹中配置一个新的 xml 文件, myConfiguration2.xml,内容如下所示:

<?xmlversion="1.0"?>

<configuration>

        <property>

                 <name>color</name>

                 <value>blue</value>

                 <description>color</description>

        </property>

        <property>

                 <name>blossom</name>

                 <value>no</value>

                 <final>true</final>

                 <description>blossom</description>

        </property>

</configuration>

 

如图可见,我们更改了属性的内容, color 属性被重新定义为 blue,而 blossom 被定义为 no,下面我们同样使用 addResourceObject 方法添加多个配置文件并打印属性。程序如下所示:

publicclassXmlReader {

        publicstaticvoid main(String[] args) {

                 Configurationconf = new Configuration(); // 设置环境变量实例

                 conf.addResource("myConfiguration.xml"); //添加自定义配置文件

                 conf.addResource("myConfiguration2.xml"); //添加自定义配置文件 2

                 System.out.println("flower name is " + conf.get("flower")); // 获取属性值

                 System.out.println("flower color is " + conf.get("color")); // 获取属性值

                 System.out.println("blossomed ? " + conf.get("blossom")); // 获取属性值

        }

}

 

运行命令行代码如下所示:

$ HadoopJar XmlReader2.JarXmlReader2

对于运行此程序后,输出结果如下所示:

13/04/2214:30:25 WARN conf.Configuration: myConfiguration2.xml:a attempt to override final

parameter:blossom; Ignoring. //忽略对 final 的覆盖

flowername is rose //显示 name 属性

flowercolor is blue //显示 clolr 属性

blossomed? Is //显示 blossom 属性

 

首先是一个警告,对于 myConfiguration2.xml 试图去覆盖一个 final 标记的属性 blossom,结果是对此次覆盖的忽略。

下面对于输出结果部分, color 属性已经被重新覆盖为在第二个配置文件中配置的属性,也就是结果变成 blue。而对于 blossom 来说,其标记为 final 属性,确保此属性不能够被覆盖,因此,尽管在第二个配置文件中对其进行覆盖,但是结果依旧是第一个属性文件中配置的属性,第二个配

置文件中的覆盖被忽略。输出结果依旧是“ is”。

 

对于部分文件通过 xml 文件进行配置是方便可行的。但是有些程序一旦涉及到集群处理事情就变得有趣了。

 

我们就拿一个小一点的例子来说,试想一个集群中有 1000 台普通计算机进行协同处理,给每台机器上的同样 conf 文件夹中添加修改相应的 xml 文件将会成为一个浩大的工程。对于此状况,我们可以使用在属性文件或者其他属性中定义某个配置文件,使用其对属性文件的引用来进行覆盖。属性文件如下所示:

myConfiguration3.xml

<?xmlversion="1.0"?>

<configuration>

        <property>

                 <name>flower</name>

                 <value>rose</value>

                 <description>flower</description>

        </property>

        <property>

                 <name>color</name>

                 <value>red</value>

                 <description>color</description>

        </property>

        <property>

                 <name>blossom</name>

                 <value>is</value>

                 <final>true</final>

                 <description>blossom</description>

        </property>

        <property>

                 <name>color-blossom</name>

                 <value>${flower}color is ${color},and ${blossom} blossom!</value>

        </property>

</configuration>

 

在配置文件中我们可以看到,我们使用 ${flower}指向对 flower 属性的引用,使用 ${color}指向对 color 的引用,使用 ${blossom}指向对 blossom 的引用,一般情况下这里的引用直接指向配置文件所对应的属性的引用,但是我们知道,对于属性来说, System 系统属性的配置高于一般属性,则可以使用 System.setProperty 来设置属性文件,然后通过引用来使用。具体可见程序:

 

publicclassXmlReader3 {

        publicstaticvoid main(String[] args) {

                 System.setProperty("color", "pink"); //设置系统属性

                 Configurationconf = new Configuration(); // 设置环境变量实例

                 conf.addResource("myConfiguration3.xml"); //添加自定义配置文件

                 System.out.println("flower name is " + conf.get("flower")); // 获取属性值

                 System.out.println("flower color is " + conf.get("color")); // 获取属性值

                 System.out.println("blossomed ? " + conf.get("blossom")); // 获取属性值

                 System.out.println(conf.get("color-blossom")); //打印引用类型属性

        }

}

 

运行命令行代码如下所示:

$ HadoopJar XmlReader3.JarXmlReader3

输出结果如下所示:

flowername is rose

flowercolor is red

blossomed? is

rosecolor is pink,and is blossom!

 

辅助类 ToolRunner、 Configured 详解

为了实现更多自定义的功能, Hadoop 提供了一些辅助类供我们在编写程序使用,例如Tool、 Configured 等。

Configured 源代码如下所示:

 

publicclassConfigured implements Configurable {

        private Configuration conf; //设置一个环境变量的引用

 

        public Configured(Configuration conf) { // 构造方法

                 setConf(conf); // 调用 set 方法给 conf 赋值

        }

 

        publicvoid setConf(Configuration conf) { // conf 的赋值

                 this.conf = conf;

        }

 

        public Configuration getConf() { // 获取 conf 实例

                 returnconf;

        }

}

 

由源代码可见, Configured 是继承自 Configurable 接口的一个实现类, Configurable 接口如下所示:

publicinterface Configurable {

        void setConf(Configuration conf);

        ConfigurationgetConf();

}

 

Configurable 提供了两个方法,对环境变量的实例进行获取。而在 Configured 实现类中我们可以通过构造方法对环境变量的引用进行赋值。

看到这里读者可能会问,这两个类与我们编写 MapReduce 有什么关系。

 

在解答这个问题之前我们来看一下另外一个辅助类 ToolRunner,这个辅助类在org.apache.hadoop.util 包下, ToolRunner 的源代码如下所示:

publicclassToolRunner {

        publicstaticint run(Configuration conf, Tool tool, String[] args) throws Exception{

                 GenericOptionsParserparser = new GenericOptionsParser(conf, args); //命令行解释权实例

                 tool.setConf(conf); //设置环境变量

                 String[]toolArgs = parser.getRemainingArgs(); // 获取对应命令行字符串

                 returntool.run(toolArgs); // 执行命令行

        }

 

        publicstaticint run(Tool tool, String[] args) throws Exception {

                 returnrun(tool.getConf(), tool, args); // 使用默认值调用重载的run 方法

        }

}

 

从源码可见, ToolRunner 中有两个静态 run 方法,第一个 run 方法首先调用 setConf对内部定义的环境变量引用进行赋值,之后调用的 tool 类中的默认处理方法对传进来的参数通过 GenericOptionsParser 进行解释,解释后调用 Tool 中的也就是实现了 Tool 接口的那个我们自定义类中由我们自己写的 run 方法运行。

而第二个 run 方法则直接以当前对象中configuration 中的环境变量作为默认值进行赋值。

 

下面我们通过一个例子来讲解 ToolRunner、 Configured 的使用,程序如下:

// 自定义的类继承自Configured并实现了Tool接口,其中 Configured Tool 提供了Configuration 的引用。

publicclassToolRunnerTest extends Configured implementsTool {

        @Override

        publicint run(String[] args) throws Exception{

                 Configurationconf = getConf(); // Configured 中获取环境变量实例

                 System.out.println(conf.get("flower")); // 获取属性中的 flower 属性

                 // 获取属性中的fs.default.name

                 System.out.println(conf.get("fs.default.name"));

                 // 获取属性中的Mapred.job.tracker属性

                 System.out.println(conf.get("Mapred.job.tracker"));

                 return 0; // 正常返回值为 0

        }

 

        publicstaticvoid main(String[] args) throws Exception {

                 Configurationconf = new Configuration(); // 获取当前对象的环境变量

                 conf.addResource("myConfiguration.xml"); //添加自定义的配置文件

                 // 使用ToolRunnerrun方法对自定义的类型进行处理

                 ToolRunner.run(conf, new ToolRunnerTest(), args);

        }

}

 

从程序中我们可以看到,首先我们自定义了一个实现类,继承自 Configured 并实现了Tool接口,其中Configured为Tool提供了Configuration 的引用。其中的 run 方法首先可以获取当前对象的环境变量,然后从中读取一个自定义的属性配置以及我们写入 core-site.xml 中的属性配置文件。

 

程序中的 main 方法首先定义了环境变量,并且添加了自定义的配置文件,通过ToolRunner中的run方法传递了三个相关参数,分别是 Configuration,实现了 Tool 接口的ToolRunnerTest 对象,此对象提供了我们自定义的 run 方法,并且传递相关的字符串参数可以使用解释器对字符串进行解释。

 

可以使用如下命令行进行执行:

$ HadoopJar ToolRunnerTest.JarToolRunnerTest

执行结果如下所示:

rose

hdfs://localhost:9000

null

这里可以看到,我们在自定义的 myConfiguration.xml 设置了名为 flower 的配置属性映射为rose ,在 Hadoop 的默认设置中我们设置了 fs.default.name 的映射属性为hdfs://localhost:9000,此配置文件在 core-site.xml 中进行定义。第三条打印语句是打印出 Mapred.job.tracker 的内容,因为我们没有运行任何有关的 MapReduce 程序,所以打印内容为 null。

 

通过输出信息进行 MapReduce 测试

使用计数器的 MapReduce 程序设计

hadoop 计数器的主要价值在于可以让开发人员以全局的视角来审查程序的运行情况,及时做出错误诊断并进行相应处理。下面内容就是介绍如何在 MapReduce 程序使用计数器来对信息进行检测。

 

通过查看 Context 源码我们可以看到 Context 父类中具有对 counter 计数的方法,源代码如下:

        public Counter getCounter(Enum<?>counterName) {

                 return reporter.getCounter(counterName);

        }

 

此代码通过传入一个枚举类型的自定义说明而在计数器中进行计数。而在返回值 Counter 中又可以使用如下代码进行计数的添加:

        publicsynchronizedvoid increment(longincr) {

                 value+= incr;

        }

因此我们可以知道,可以使用一个具体的计数程序对我们设定的某个输出变量进行计数。下面我们通过设置修改我们以前定义的单词计数 Mapper 获取对计数器的使用。代码如下:

packagecom.shawn.hadoop.mr.counter;

importjava.io.IOException;

importjava.util.Iterator;

importjava.util.StringTokenizer;

importorg.apache.hadoop.io.IntWritable;

importorg.apache.hadoop.io.LongWritable;

importorg.apache.hadoop.io.Text;

importorg.apache.hadoop.mapreduce.Mapper;

importorg.apache.hadoop.mapreduce.Reducer;

classTxtCounter { // 创建计数类

        staticenum ReportTest { //设置枚举作为错误归类

                 Male, Female// 枚举值

        }

 

        // 自定义的Mapper

        publicstaticclass TxtMapper extendsMapper<Object, Text, Text, IntWritable> {

                 @Override

                 protectedvoid map(Object key, Text value,Mapper<Object, Text, Text, IntWritable>.Context context)

                                   throws IOException, InterruptedException {

                          Textword = new Text();

                          IntWritableone = new IntWritable(1);

 

                          StringTokenizeritr = new StringTokenizer(value.toString());

                          Stringstr;

                          while (itr.hasMoreTokens()) {

                                   str = itr.nextToken();

                                   if (str.equals("Male")){ // 对其中的值进行判断

                                           context.setStatus("Male"); //写入状态

                                           context.getCounter(ReportTest.Male).increment(1); // 计数增加

                                   }elseif (str.equals("Female")) { // 对其中的值进行判断

                                           context.setStatus("Female"); //写入状态

                                           context.getCounter(ReportTest.Female).increment(1); // 计数增加

                                   }

 

                                   word.set(str);

                                   context.write(word, one);

                          }

                 }

        }

 

        // 自定义的Reduce

        publicstaticclass TxtReducer extendsReducer<Text, IntWritable, Text, IntWritable> {

                 @Override

                 protectedvoid reduce(Text key, Iterable<IntWritable>values,

                                   Reducer<Text,IntWritable, Text, IntWritable>.Context context) throws IOException,

                                   InterruptedException{

                          intsum = 0; // 辅助计数

                          Iterator<IntWritable>it = values.iterator(); //获取值

                          while (it.hasNext()) { // 遍历

                                   IntWritablevalue = it.next(); // 取出值

                                   sum += value.get(); // 进行增加

                          }

                          context.write(key, newIntWritable(sum));

                 }

        }

}

 

packagecom.shawn.hadoop.mr.counter;

importorg.apache.hadoop.conf.*;

importorg.apache.hadoop.fs.*;

importorg.apache.hadoop.io.IntWritable;

importorg.apache.hadoop.io.Text;

importorg.apache.hadoop.mapreduce.Job;

importorg.apache.hadoop.mapreduce.lib.input.FileInputFormat;

importorg.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

importorg.apache.hadoop.util.Tool;

importorg.apache.hadoop.util.ToolRunner;

publicclassToolRunnerTest extends Configured implements Tool{

        @Override

        publicint run(String[] args) throws Exception{ // 驱动方法

                 Configurationconf = getConf(); //Configured中获取Configuration 实例

                 Jobjob = new Job(conf); // 创建任务实例

                 job.setJarByClass(ToolRunnerTest.class); // 创建工作类

 

                 Pathin_path = new Path("hdfs://localhost:9000/wufan/data4");

                 Pathout_path = new Path("hdfs://localhost:9000/wufan/out");

 

                 FileSystemfs = out_path.getFileSystem(conf);

                 fs.delete(out_path, true); //删除已存在的目录

 

                 FileInputFormat.addInputPath(job, in_path); // 建立输入文件路径

                 FileOutputFormat.setOutputPath(job, out_path); //建立输出文件路径

 

                 job.setMapperClass(TxtCounter.TxtMapper.class); // 设置自定义的 Mapper工作类

                 job.setReducerClass(TxtCounter.TxtReducer.class); // 设置自定义的 Reducer

 

                 job.setOutputKeyClass(Text.class); // 设置输出中键的类型

                 job.setOutputValueClass(IntWritable.class); // 设置输出中值的类型

                 job.waitForCompletion(true); // 开始程序运行

                 return 0;

        }

 

        publicstaticvoid print(Tool tool) throws Exception { //读取输出结果的方法

                 Pathpath = new Path("hdfs://localhost:9000/wufan/out/part-r-00000"); //创建读取文件路径

                 FileSystemfs = path.getFileSystem(tool.getConf());

                 FSDataInputStreamfsin = fs.open(path); // 打开文件

                 intlength = 0; // 设置辅助变量

                 byte[] buff = newbyte[128]; // 设置辅助变量

                 while ((length = fsin.read(buff, 0, 128))!= -1) { // 开始读取文件

                          System.out.println(new String(buff, 0, length)); //输出读取的内容

                 }

        }

 

        publicstaticvoid main(String[] args) throws Exception { //主方法

                 Tooltool = new ToolRunnerTest(); // 创建实现了 Tool 接口的实现类

                 ToolRunner.run(tool, args); // 运行驱动

                 print(tool); // 打印结果

        }

}

 

这样就可以在控制台看到Male和Female的统计结果

 

MapReduce 运行流程详解

经典 MapReduce 任务的工作流程

MapReduce 的工作流程如图所示:


 

从流程图我们可以看到,整个 MapReduce 从任务开始到任务结尾,其运行范围可以分为4个部分,其依次是下面四个部分。

1. JobClient:用于供客户端向 Hadoop 框架提交 MapReduce 任务。

2. JobTracker:主要是用于在 Hadoop 框架内调度 MapReduce 任务的运行。

3. TaskTracker:在 Hadoop 框架任务节点上运行的任务执行器,主要是运行由JobTracker 分配到本地节点上的任务。

4. HDFS: Hadoop 框架用于存放数据的磁盘空间。

从上面对一个任务的完整分配来看,一个 M,apReduce 任务可以分为任务的提交、任务的分配、任务执行、任务报告这四个主要阶段。下面我们对着四个阶段分别来详细研究一下。

 

任务的初始化

首先我们来看图中的第一个过程 runJob。在此过程中, MapReduce 程序通过调用submit 方法或者 waitForCompletion 方法向 JobClient 提交一个任务的请求。但与通常的任务进行有区别的是, MapReduce 任务不止是提交后就完成了其历史使命。而在提交后,waitForCompletion 方法仍旧每秒会通过 JobClient 查询一次任务进度,如果发现任务的程度或者状况发生变化,则将其输送到用户控制台中进行报告。当任务完成时,如果程序正常完成,就会生成一系列的报告,包括我们在上一节中对信息进行记录的报告。而如果任务失败或者有错误发生,则会产生一个异常,同时 JobClient 会将其进行报告。

 

在第二个过程中, JobClient 申请一个新的独一无二的任务 ID,并反馈给用户。需要说明的是,在这一过程中,若输出目录已经存在,则任务的创建失败,这也是我们在以前经常见到的输出目录已存在的异常报告。同时检查输入路径,并对输入的文件大小进行测量,根据片区的大小进行分片(分片的确定我们在后面章节中中学习。),如果不能够对输入路径进行分片,则任务创建失败,同样返回创建失败的异常到用户控制台中。

 

一旦前两个步骤完成以后, Hadoop 就将运行任务所需要的 Jar 控制文件,属性配置等一些基本信息复制到以第二个过程中任务 ID 命名的文件系统中。并通过第四个步骤向Hadoop 报告任务基本配置完毕,可以执行。

 

而 JobTracker 作为 Hadoop 框架内的任务调度器,会在调度器内部设置一个内部队列,对依次传过来的任务进行调度。一旦开始任务执行,就会初始化任务,创建一个可运行对象,并跟踪发布的任务状态和步骤。这个过程被标记为步骤五。

 

初始化任务对象后,开始任务的运行。这时 JobTracker 会查询任务信息,获取输入路径及已经分片完毕的任务数据,并创建相应的 Map 任务对输入的数据进行预处理。为了提高Hadoop 框架的应用效率, Reduce 任务数量可以由程序设计人员自行确定,在后面章节中我们会学习对 Reduce 任务数的设置方法。

 

任务的分配

在步骤五我们提到,任务一旦开始运行, JobTracker 作为 Hadoop 框架的调度器则需要定时查询任务的进度与状况。从图中我们可以看到,作为任务的具体执行节点 TaskTracker则每隔一段时间发送一个信号给调度器,从而对任务进度和状态的汇报。这个这个汇报我们称为“心跳( heart beat) ”。如步骤七所示。在这个过程中, TaskTracker 发送心跳给 JobTracker这一主任务调度器,并且向其汇报任务执行情况。并在任务结束通知 JobTracker 是否已经准备好运行下一个任务, JobTracker 会根据具体情况给其分配任务。

 

在为 TaskTracker 选择任务(task)之前, JobTracker 首先要选定任务运行的节点。对于Map 任务和 Reduce 任务运行地点的选择使用的是 TaskTracker“任务槽”模式,即可运行的任务数。一个 TaskTracker 有固定数量的任务槽,即一个 TaskTracker 可能同时运行两个 Map任务和 Reduce 任务。默认调度器在处理 Reduce 任务之前会填满空闲的 Map 任务。因此,如果 TaskTracker 至少有一个空闲的 Map 任务槽, JobTracker 会为它选择一个 Map 任务,否则选择一个 Reduce 任务。

 

在选择 Reduce 任务的时候, JobTracker 简单的从待运行的 Reduce 任务列表中选取下一个来执行,用不着考虑数据的本地化。对于一个 Map 任务 JobTracker 会考虑 TaskTracker 的网络位置,并选取一个距离与其输入分片最近的 TaskTracker。最理想的情况是数据本地化的(任务运行在和输入分片在同一个机器上)。次之是是选择机架本地化的。关于副本的存放,我们在 HDFS 中有过介绍。

 

任务的执行

现在, TaskTracker 已经被分配了一个任务,下一步是运行任务。首先通过共享文件系统将作业的 JAR 复制到 TaskTracker 所在的文件系统,从而实现 JAR 文件本地化。同时,TaskTracker 将程序所需的全部文件从分布式缓存复制到本地磁盘,这里从图中步骤八可以看到。之后 TaskTracker 为任务新建一个本地工作目录,并把 JAR 文件解压到这个文件夹下,TaskTracker 会新建一个 TaskRunner 实例来运行该任务。

 

在步骤九 TaskRunner 启动一个新的 JVM 来运行每个任务,以便用户定义的 Map 和Reduce 函数的任何软件问题都不会影响到 TaskTracker(例如导致崩溃或挂起等)。但是在不同的任务间共享 JVM 是可能的。子进程通过 umbilical 接口与父进程进行通信。任务的子进程每隔几秒便告诉父进程它的进度,直到步骤十任务完成。

 

任务的完成与状态更新


 

当 JobTracker 收到作业最后一个任务完成的通知后,便把作业的状态设为“成功”。然后, JobClient 查看作业状态时,便知道任务已完成,于是 JobClient 打印一条消息告知用户,然后从 submit()方法返回。最后 JobTracker 清空作业的工作状态,指示 TaskTracker 也清空工作状态(如删除中间输出等)。

 

自此为止,一个完整的 MapReduce 任务完成。但是有些细节需要我们知道,一个MapReduce 任务任务是长时间运行的批量任务,是一个很长的时间段,对于用户而言,能够得知作业进展是很重要的。一个作业和它的每个任务都有一个状态( status) , 包括作业或任务的状态(如运行状态、成功完成、失败状态)、 Map 和 Reduce 的进度、作业计数器的值、状态信息或描述(可以由用户代码来设置)。

 

任务在运行时,对其进度保持追踪。对 Map 任务,任务进度是已处理输入所占的比例。对 Reduce 任务,情况稍微复杂,但系统仍然会估计已处理 Reduce 输入的比例。比如,如果Reduce 任务已经执行 Reducer 一半的输入,那么任务的进度便是 5/6。因为已经完成复制和排序阶段(各 1/3),并且已经完成 Reduce 阶段的一半( 1/6)。

 

如果任务报告了进度,便会设置一个标志以表明状态变化将被发送到 TaskTracker。JobTracker 中有一个独立的线程每隔 3 s 检查一次,此标志是否被设置成打开状态,如果已被设置,则告知 TaskTracker 当前任务状态。同时, TaskTracker 每隔 5 s 发送心跳到 JobTracker(5s这个间隔是最小值,心跳间隔实际上由集群的大小来决定,更大的集群,间隔会更长一些),并且将 TaskTracker 运行的所有任务的状态发送至 JobTracker。JobTracker 将这些更新状态合并起来,生成一个表明所有运行作业及其所含任务状态的全局视图。同时,JobClient 通过查询 JobTracker 来获取最新状态。客户端也可以使用 JobClient的 getJob()方法来得到一个 RunningJob 的实例,后者包含作业的所有状态信息。

 

经典 MapReduce 任务异常处理详解

在上一节中我们看到 MapReduce 任务运行的全部过程,一些都是按照正常程序流程执行。但是我们知道,任何一个正常的程序设计人员都不可能认为自己能够一次性写出可以运行的程序,特别是较为大型程序程序。在这一节中,我们主要对 MapReduce 任务失败或者异常做出解释。

 

MapReduce任务异常的处理方式

首先我们来 MapReduce 任务情况。一般情况下最常见的情况是 Map 或 Reduce 任务中的某些代码抛出不可运行的异常。如果发生这种情况, Hadoop 用来执行任务的子 JVM 就会强行退出,并向任务调度器 TaskTracker 进行汇报。汇报结果将被写入用户日志中以供可能的查询。而同时在任务的状态栏中,此任务记录中的状态被标记为 failed。具体可在前面章节中的 web 查看任务记录一章中了解。

 

除此之外,对于一些其他异常报告并使得 JVM 退出的情况,用户代码会造成某些特殊原因的 JVM 不可预知的 bug,因此也会造成 MapReduce 特殊中断,并中止向 TaskTracker 发送心跳报告, TaskTracker 会在一段时间后,直接将任务标记为 failed。

 

而对于产生异常的任务, TaskTracker 并不是直接将标记为代码错误或者不可运行,在上面我们已经说了,对于任务的异常有可能是程序代码问题,也有可能是 JVM 本身的 BUG或者节点硬件的问题。因此,对于这些的任务, TaskTracker 并不是直接标记为不可用。而是重新更换节点进行运行,但是如果一个任务在不同的节点产生次数超过一定次数,那么TaskRacker 将会真正标记为失败而不会再运行。一般情况下,失败的最大可允许次数为 4次,但是程序设计人员可以对其进行更改。

 

对于 map 任务,运行任务的最多尝试次数由 mapred.map.max.attempts属性控制,而对于 reduce 任务,则由 mapred.reduce.max.attempts 属性控制。

 

我们知道, MapReduce 是分布在多个节点上进行并发任务的运行,因此对于一个任务整体而言,其又被分为若干个小任务进行执行,而对于某些情况下来说,某些小任务的失败是可接受的,因此我们可以设定一个接受范围,是针对任务失败的情况下来说的运行失败的百分比。当失败的任务占总任务比例的多少以下时,任务结果是可接受的。

 

对于map 任务和 reduce任务可以独立控制,分别通过mapred.max.map.failures.percent和

mapred.max.reduce.failures.percent 属性来设置。

 

我们还需要知道,对于任务运行异常的分类而言,并不是所有的异常都是会产生任务失败,而有可能对于某些任务因为网络或硬件或其他的原因运行速度过慢, Hadoop 框架会自动在另外一个节点上启动同一个任务,作为任务执行的一个备份。这就是所谓的“任务推测”。

 

对于 Hadoop 框架来说,判断任务执行状态并进行“任务推测”的算法有很多,但是最常用的是对任务的进度进行比较,如果一个任务的进度明显落后与其他任务,则 Hadoop 会自动启动一个相同的任务来进行同样的工作。如果一个类的任务同时已经完成,则其所有的

推测任务都被暂停,所有的同样任务结束。

 

MapReduce任务失败的处理方式

对于 MapReduce 任务失败来说,主要有两种失败方式,分别是 JobTracker 失败与TaskTracker 失败。

 

首先我们来看 JobTracker 失败。我们知道 JobTracker 是 Hadoop 的任务调度器,如果产生了此种失败,那么整个 MapReduce 任务一定失败,切无法自动修复和处理。因为无法对整个任务进行处理。当然此种失败的可能性是很小的。

 

其次我们来看另外一种失败,即 TaskTracker 失败。 TaskTracker 是 Hadoop 框架中用来运行 MapReduce 任务的分节点,一般运行的是当前节点中存储的数据任务。我们在上一节中已经说明, TaskTracker 通过发送心跳通知 JobTracker 对任务过程进行追踪处理。一旦TaskTracker 失败产生,心跳通信也随之停止,那么 JobTracker 会将 TaskTracker 的任务重新选择节点并运行。

 

心跳通知间隔的值由mapred.tasktracker.expiry.interval 属性来设置(以毫秒为单位) ,并将它从等待任务调度的 tasktracker 池中移除。

 

需要我们知道的是,对于一个运行 TaskTracker 的节点来说,如果运行任务的失败次数较多或者运行速度过于缓慢以至于 Hadoop 框架经常性为之展开推测执行的任务,那么此TaskTracker 也有很大可能被 Hadoop 框架进行标记,标记为不合适进行 TaskTracker 任务的节点而在一般的任务执行中避免调用此节点。

 

作业的调度

早期版本的 Hadoop 使用一种非常简单的方越来调度用户的作业:按照作业提交的JI 固序,使用 FIFO( 先进先出)调度算法来运行作业。典型情况下,每个作业都会使用整个集群,因此作业必须等待直到轮到自己运行。虽然共享集群极有可能为多用户提供大量资源,但问题在于如何公平地在用户之间分配资源,这需要一个更好的调度器。生产作业需要及时完成,以便正在进行即兴查询的用户能够在合理的时间内得到返回结果。

 

随后,加入设置作业优先级的功能,可以通过设置 mapred.job.priority 属性或JobClient setJobPrio ty()方陆来设置优先级(在这两种方怯中,可以选择VERY_HIGH , HIGH , NORMAL , LOW ,VERY_LOW 中的一个值作为优先级)。作业调度器选择要运行的下一个作业时,它选择的是优先级最高的那个作业。然而,在FIFO 调度算法中,优先级并不支持抢占 (preemption) ,所以高优先级的作业仍然会被那些在高优先级作业被调度之前已经开始的、长时间运行的低优先级的作业所阻塞。

 

经典 MapReduce 任务的数据处理过程

下面我们来看一些更为细节一些的东西。我们在前面章节中已经知道, MapReduce 任务主要分成两大步骤,分别是 Map 与 Reduce 任务。其中还有一个过程,即 Map 任务传递已经进过整理的数据给 Reduce 端进行处理,这一过程称为 Shuffle。 Shuffle 是 MapReduce 任务处理的核心,也是我们需要着重掌握的东西,即 MapReduce 任务处理的过程。与第一节对 MapReduce 总运行过程介绍相类似,我们也是使用一张图来介绍。MapReduce排序的流程如图所示:


 

这个过程处理相对比较复杂,我们分成两部分, Map 处理过程与 Reduce 处理过程分开进行讲解。

 

Map端的输入数据处理过程

首先我们看到,在 MapTask 中,每个 Map 都有一个内存缓冲区,用于存储数据的输入。默认情况下,缓冲区的大小为 100MB,一旦缓冲区内的数据容量达到阈值,即我们设定的 80%,就开始讲数据写入磁盘中。

 

默认情况下,缓冲区的大小为 100 MB ,此值可以通过改变 io.sort.mb 属性来调整。一旦缓冲内容达到闹值 (io.sort.spill.percent ,默认为 0.80 ,或 80%) ,一个后台线程便开始把内容写到 (spill) 磁盘中。在写磁盘过程中, map 输出继续被写到缓冲区,但如果在此期间缓冲区被填满, map 会阻塞直到写磁盘过程完成。写磁盘将按轮询方式写到 mapred.local.dir 属性指定的作业特定子目录中的目录中。

 

在写入磁盘之前, Map 还会降数据按要求分片,分片的原理与方法我们在后面章节中进行介绍。

 

如果 Map 缓冲区的写入速度较快,那么 Hadoop 框架会为 Map 任务建立一个写入文件队列供任务进入等待,默认的等待序列正的任务数为 10。

 

对于在网络上传输的任务来说,压缩及 Combiner 是一个很好节省传输数据及加快运行时间的方法。如果我们在 Map 任务中设置了 Combiner 方法,那么在数据写入磁盘之前,Combiner 会对数据反复运行从而获得一个已经计算好的结果。使得写入或者传输的数据更加紧凑。

 

而采用压缩方式对传输的数据进行处理也是一个较好的办法,这样会让数据占据的空间更好,处理更快。

 

默认情况下,输出是不压缩的,但只要将 mapred.compressmap.output 设置为 true ,就可以轻松启用此功能。使用的压缩库囱 mapred.map.outputcompression.codec 指定

 

Map 任务完成后,会通知 TaskTacker 状态已更新,然后 TaskTracker 通过心跳通知JobTracker。下面的 Reduce 所在的 TaskTracker 有一个线程定期询问 JobTracker 以便获得 Map输出的位置,直到它获得所有输出的位置。

 

等以上处理过程结束后, Map 任务将预处理后的数据写入磁盘空间供 Reduce 任务进行下一步的分析处理。

 

Reduce端的输入数据处理过程

现在转到处理过程的 reduce 部分。 map 输出文件位于运行 map 任务的 tasktracker的本地磁盘(注意,尽管 map 输出经常写到 map tasktracker 的本地磁盘,但 reduce输出并不这样) ,现在, tasktracker 需要为分区文件运行 reduce 任务。更进一步,

reduce 任务需要集群上若干个 map 任务的 map 输出作为其特殊的分区文件。每个map 任务的完成时间可能不同,因此只要有一个任务完成, reduce 任务就开始复制其输出。这就是 reduce 任务的复制阶段 (copy phase) 0 reduce 任务有少量复制线程,因此能够井行取得 map 输出。默认值是个钱程,但这个默认值可以通过设mapred.reduce.parallel.copies属性来改变。

 

这里复制还有一些细节需要说明,那么就是如果 Map 的输出相当小,则就被复制到Reduce 的内存区域中,否则直接被复制到磁盘上。随着复制内容的增多, Reduce 会启动一个合并任务将其合并以便进一步节省运行空间。例如有 100 个数据文件存储在 Reduce 的数

据空间中,那么合并任务将进行 10 次,每次合并 10 个文件,合并结束后,只剩下 10 个数据文件。

 

缓冲区大小由mapred.job.shuffle.input.buffer.percent属性控制,指定用于此用途的堆空间的百分比

 

需要注意的是,每次合并目标是合并最小数量的文件以便满足最后一次的合并系数,例如如果 Reduce 内存中有 40 个文件,要求每次合并 10 个文件然后得到 4 个文件,进行的具体过程是第一此只合并 4 个文件,最后的三次每次合并 10 个文件,在最后的一次中 4 个已经合并的文件和余下的 6 个文件(未合并)进行 10 个文件的合并,其实这里并没有改变合并次数,它只是一个优化措施,尽量减少写到磁盘的数据量,因为最后一趟总是合并到 Reduce。

 

缓冲区合并阈值的百分比由mapred.iob.shuffle.merge.percent决定

 

 

一旦所有的 Map 输入被复制并按要求合并,那么 Reduce 任务进入其处理过程。为数据使用设定的 Reduce 方法,并将最终结果输出。

 

JVM重用与记录异常对应策略

我们在前面的运行过程可以看到, Hadoop 为每个任务启动一个新 JVM,每个新的 JVM的开启需要耗时 1 秒,而如果 JVM 开启过多,也就是任务过于细粒度的话,那么耗费在开启与关闭 JVM 的资源损耗是非常没有必要的。

 

因此仿照线程池与连接池的原理, Hadoop 设计人员采用了“ JVM 重用”。对于大量超短任务如果重用 JVM 会提升性能。当启用 JVM 重用后, JVM 不会同时运行多个任务,而是顺序执行。 TaskTracker 可以一次启动多个 JVM 然后同时运行,接着重用这些 JVM。程序设计人员可以指定给定作业每个 JVM 运行的任务的最大数,默认为 1,即无重用; -1 表示无限制即该作业的所有的任务都是有一个 JVM。

 

mapred.job.reuse.jvm.num.tasks 在一个 tasktracker 上,对于给定的作业的每个 JVM 上可以运行的任务最大数。

 

而对于部分数据异常的处理,我们知道任何一个数据,特别是容量庞大的数据集库,其含有坏的记录的可能性是非常大的,并且由于产生的原因不同,记录格式也大相径庭。首先我们来看一个实例,对输出异常抛出异常:

 

conf.setInt("Mapred.Map.max.attempts",2);

conf.setInt("Mapred.skip.attempts.to.start.skipping",1);

conf.setInt("Mapred.skip.Map.max.skip.records",1);

 

可以通过对 conf 的设置进行对错误记录的处理。

Mapred.Map.max.attempts:是对于Map任务中错的记录最多尝试次数。

Mapred.Reduce.max.attemps:是对于Reduce任务中错的记录最多尝试次数。

Mapred.skip.attempts.to.start.skipping:是启用skip mode模式

Mapred.skip.Map.max.skip.records:一旦记录读取失败次数超过设定值,则开始对错误的记录进行 skip。

 

Hadoop 检测出来的坏记录以序列文件的形式保存在 _logs/skip 伊子目录下的作业输出目录中。在作业完成后,可查看这些记录(例如,使用 hadoop fs-text) 进行诊断。

 

MapReduce 高级程序设计

学习使用了自定义的 Map 和 Reduce 进行程序设计,并掌握了基本环境设置。但是读者可能会注意到,在进行程序设计时,对于设计模型的选择基本上是以默认的设计模型为主,而我们所自定义的无非就是 map 方法与 reduce 方法。

 

MapReduce 程序设计默认格式类型详解

对于任何一个希望写出自定义 MapReduce 程序的程序设计人员来说,优先使用的应该是 MapReduce 默认的格式类型。从最基本的使用性能上来考虑,默认的格式类型是经过大牛们千锤百炼设计和创作出来的,安全、性能、稳定方面考虑都强于我们自定义的类型,所以作者的建议是在进行 MapReduce 程序设计时还是首选默认类型。

 

任何一个 MapReduce 的基本输如输出格式可以归结为如下的格式类型:

map:( k1, v1) -> list( k2, v2);

reduce:( k2, list( v2)) -> list( k3, v3);

 

map 方法定义的源码如下所示:

publicclassmap<KEYIN, VALUEIN, KEYOUT, VALUEOUT> { // 设置 map

        publicclass Context extends MapContext<KEYIN, VALUEIN, KEYOUT,VALUEOUT> {

        }

 

// 设置Map方法

        protectedvoid map(KEYIN key, VALUEIN value, Context context) throws IOException, InterruptedException {

                                                                                                                                                                                                                                                    

        }

}

 

方法中被定义了一个泛型, KEYIN 和 VALUEIN 分别对应于输入的键值对格式类型。而 KEYOUT和 VALUEOUT 分别对应于输出的键值对格式类型。

而对于 Reduce 来说, reduce 方法定义的源码如下所示:

publicclassreduce<KEYIN, VALUEIN, KEYOUT, VALUEOUT> { // 设置 Reduce

        publicclass Context extends ReduceContext<KEYIN, VALUEIN, KEYOUT,VALUEOUT> {

        }

        protectedvoid reduce(KEYIN key, Iterable<VALUEIN>values, Context context) throwsIOException,

                          InterruptedException{ // 设置Reduce 方法

        }

}

与 map 方法类似, reduce 也被定义了一个泛型, KEYIN 和 VALUEIN 分别对应于输入的键值对类型。而 KEYOUT 和 VALUEOUT 分别对应于输出的键值对类型。

 

从中可以看到,在自定义的 reduce 中对于输出的键值对,要求与 map 中输出键值对相对应。原因很好理解, reduce 方法就是对 map 方法的输出结果进行再次处理。而对于 reduce方法的输出,则可能会根据需要产生一个新的输入输出的类型。无论是 Map 还是 Reduce 类中都包含一个 Context 类型,对于 Context类型来说,其作用是构建一个上下文系统,解决输入输出类型在 Hadoop 框架内传送的问题。通过 write 方法,可以很容易的在 Map 与 Reduce 之间传递数据。其源码如下:

publicvoidwrite(KEYOUT key, VALUEOUT value) throwsIOException, InterruptedException {

                 output.write(key, value); //写入上下文

        }

 

自定义输入输出类型设置

对类型按需求配置是进行自定义 MapReduce 程序设计的重要步骤。例如我们在上面进行的计算数字的例子,在此例中,对输入的数据类型来说,输入的键的类型采用默认的“LongWritable”,当时说明不需要对输入键类型进行设置,从而采用通用的默认设置即可。而对于采用的输入值的类型设置,采用的也是默认的基本数据类型“Text”。

看到这里可能会有疑问,能不能将输入的数据类型换成其他的数据类型。答案是不进行设置是不行的。请看自定义的 Tool 类中的 run 方法代码段:

        @Override

        publicint run(String[] args) throws Exception{

                 Configurationconf = getConf(); //获得当前环境

                 Jobjob = new Job(conf); // 设置任务

                 job.setJarByClass(getClass()); // 注入 Jar

                 FileSystemfs =FileSystem.get(conf); // 获得文件系统

                 fs.delete(new Path("out"), true); // 删除已存在的输出目录

                 FileInputFormat.addInputPath(job, new Path("sample.txt")); // 设置输入路径

                 FileOutputFormat.setOutputPath(job, new Path("out"));// 设置输出路径

                 job.setMapClass(TxtCounter.TxtMap.class); // 设置输入类

                 job.setReduceClass(TxtCounter.TxtReduce.class); // 设置输出类

                 job.setOutputKeyClass(Text.class); // 设置输出键类型

                 job.setOutputValueClass(IntWritable.class); // 设置输出值类型

                 job.waitForCompletion(true); // 运行

                 return 0;

        }

 

回忆下在 main 方法对 Job 的设置,并没有设置相应的输入数据类型,而只是对输出数据进行设置,见如上代码段的黑体部分。

而此时如果我们对 Map 方法进行修改,为了不影响数据的准确性,我们只对不需要的键类型进行修改,修改成“NullWritable”,程序如图所示。

staticclassTxtMap extends Map<NullWritable, Text, Text,IntWritable> {

        protectedvoid Map(NullWritable key, Text value, Context context) throwsjava.io.IOException,

                          InterruptedException{ // 自定义的 Map 方法

                 String[]strs = value.toString().split(" "); // 切割字符串

                 for (String str : strs) { //遍历内容

                          context.write(new Text(str), new IntWritable(1)); // 写入上下文

                 }

        }

}

 

则程序无法运行。因为对于Map来说,其Hadoop设置的默认输入类型为“ TextInputFormat.class”,因而在此代码段中,对于输入的类型已经有了严格限制,不能够使用自定义的输入数据类型格式。

 

对于 Map 方法中输入数据类型的自定义我们在下一节中进行讲解。

同时还可以看到,在 run 方法的代码段中,对 MapReduce 的输出键值对类型进行了设置,如下所示:

                 job.setOutputKeyClass(Text.class); // 设置输出键类型

                 job.setOutputValueClass(IntWritable.class); // 设置输出值类型

 

对于 Job 来说,设置的方法是 setOutputKeyClass 与 setOutputValueClass。细心的读者可能会问,那么这个设置的到底是 Map 中的类型呢还是 Reduce 中的类型呢?

setOutputKeyClass 与 setOutputValueClass 方法所设置的类型即为全局输出类型,可以替代 Map 输出的键值对类型也可以替代 Reduce 输出的键值对类型。

而对于有些情况下,需要对默认的 Map 输出键值对类型进行修改,则需要调用如下方法:

                 job.setMapOutputValueClass(Class<?>theClass);    //设置 Map 输出值类型

                 job.setMapOutputKeyClass(Class<?>theClass);          //设置Reduce 输出值类

Typemismatch 问题

今天在写MapReduce程序时遇到了Type mismatch的问题,后来找到了问题所在。

即setOutputKeyClass() 会同时限定Mapper和Reducer的输出 key 类型,同理,setOutputValueClass()会同时限定Mapper和Reducer的输出value类型。

如果Mapper和Reducer的输出key或value类型不同,可以通过setMapOutputKeyClass和setMapOutputValueClass来设定Mapper的输出key/value对。

举个例子, Mapper类如下:

staticclassFetchMapper extends Mapper<LongWritable, Text, Text,LongWritable>{ }

Reducer类如下:

staticclassFetchReducer extends Reducer<Text, LongWritable, Text,Text> { }

FetchMapper的输出<k2,v2>是<Text,LongWritable>,而FetchReducer的输出为<k3,v3>是<Text,Text>。

可见v2 和 v3 是不同的。此时如果用下面的设置启动程序的话就会出现Type mismatched 错误:

                 job.setInputFormatClass(TextInputFormat.class);

                 job.setOutputFormatClass(TextOutputFormat.class);

                 job.setOutputKeyClass(Text.class);

                 job.setOutputValueClass(Text.class);

 

而加上有下划线部分的代码则可以解决这个问题。

 

                 job.setInputFormatClass(TextInputFormat.class);

                 job.setOutputFormatClass(TextOutputFormat.class);

                 job.setMapOutputValueClass(LongWritable.class);

                 job.setOutputKeyClass(Text.class);

                 job.setOutputValueClass(Text.class);

自定义全局类型变量设置要求

publicclassToolRunnerWithDefault extends Configured implementsTool {

        @Override

        publicint run(String[] args) throws Exception{

                 Jobjob = new Job(getConf()); // 获得当前环境变量

                 job.setJarByClass(getClass()); // 获得运行类

                 FileSystemfs =FileSystem.get(getConf()); // 获取文件系统

                 fs.delete(new Path("out"), true); // 删除已存在的文件

                 FileInputFormat.addInputPath(job, new Path("sample.txt")); // 设置默认输入数据路径

                 FileOutputFormat.setOutputPath(job, new Path("out"));// 设置默认输出数据路径

                 job.waitForCompletion(true); // 运行程序

                 return 0; // 正常返回

        }

 

        publicstaticvoid print(Tool tool) throws Exception { //打印输出结果的方法

                 FileSystemfs =FileSystem.get(tool.getConf()); //获取路径

                 Pathpath = new Path("out/part-r-00000"); // //建立读入路径

                 FSDataInputStreamfsin = fs.open(path); // 创建读入流

                 intlength = 0; // 辅助数据

                 byte[] buff = newbyte[128]; // 辅助数组

                 while ((length = fsin.read(buff, 0, 128))!= -1) { // 开始入取数据

                          System.out.println(new String(buff, 0, length)); //打印结果

                 }

        }

 

        publicstaticvoid main(String[] args) throws Exception { //主方法

                 Tooltool = new ToolRunnerWithDefault(); // 创建运行类

                 ToolRunner.run(tool, args); //运行测试驱动

                 print(tool); // 打印结果

        }

}

 

与下面的代码实现效果一致:

publicclassToolRunnerWithDefault extends Configured implementsTool {

        @Override

        publicint run(String[] args) throws Exception{

                 Jobjob = new Job(getConf()); // 获得当前环境变量

                 job.setJarByClass(getClass()); // 获得运行类

                 FileSystemfs =FileSystem.get(getConf()); // 获取文件系统

                 fs.delete(new Path("out"), true); // 删除已存在的文件

                 FileInputFormat.addInputPath(job, new Path("sample.txt")); // 设置默认输入数据路径

                 FileOutputFormat.setOutputPath(job, new Path("out"));// 设置默认输出数据路径

                 job.setInputFormatClass(TextInputFormat.class); // 设置默认输入格式

                 job.setMapClass(Map.class); // 设置Map

                 job.setMapOutputKeyClass(LongWritable.class); // 设置输出键类型

                 job.setMapOutputValueClass(Text.class); // 设置输出值类型

                 job.setPartitionerClass(HashPartitioner.class); // 设置分片

                 job.setNumReduceTasks(1); // 设置同时的 Reduce 任务数

                 job.setReduceClass(Reduce.class); // 设置Reduce

                 job.setOutputKeyClass(LongWritable.class); // 设置输出键类型

                 job.setOutputValueClass(Text.class); // 设置输出值类型

                 job.setOutputFormatClass(TextOutputFormat.class); // 设置默认输出格式

                 job.waitForCompletion(true); // 运行程序

                 return 0; // 正常返回

        }

 

        publicstaticvoid print(Tool tool) throws Exception { //打印输出结果的方法

                 FileSystemfs =FileSystem.get(tool.getConf()); //获取路径

                 Pathpath = new Path("out/part-r-00000"); // //建立读入路径

                 FSDataInputStreamfsin = fs.open(path); // 创建读入流

                 intlength = 0; // 辅助数据

                 byte[] buff = newbyte[128]; // 辅助数组

                 while ((length = fsin.read(buff, 0, 128))!= -1) { // 开始入取数据

                          System.out.println(new String(buff, 0, length)); //打印结果

                 }

        }

 

        publicstaticvoid main(String[] args) throws Exception { //主方法

                 Tooltool = new ToolRunnerWithDefault(); // 创建运行类

                 ToolRunner.run(tool, args); //运行测试驱动

                 print(tool); // 打印结果

        }

}

 

InputFormat 输入格式详解

在前面的学习中,对于 Map 类型的输入格式,输入数据类型,输入数据的长度都没有进行定义。这一切的设定都是通过一个名为 setInputFormatClass 的方法进行处理,其代码如下所示:

job.setInputFormatClass(TextInputFormat.class); // 设置默认输入格式

此方法接受一个 TextInputFormat.class 类,在介绍 InputFormat 输入格式之前,应该先分析下 TextInputFormat 的类型具体是什么。 TextInputFormat 的源码如下所示:

 

publicclassTextInputFormat extends FileInputFormat<LongWritable, Text> {

        @Override

        public RecordReader<LongWritable, Text>createRecordReader(InputSplit split, TaskAttemptContext context) { // createRecordReader

                 returnnew LineRecordReader(); // 返回一个实例

        }

 

        @Override

        protectedboolean isSplitable(JobContext context, Path file) { //判断是否分片

                 CompressionCodeccodec = new CompressionCodecFactory(context.getConfiguration()).getCodec(file); // 压缩格式

                 returncodec == null;

        }

}

 

从上面的源代码我们看到,默认的 TextInputFormat 是继承自 FileInputFormat,而FileInputFormat 是一个泛型类,其类型被定义为 LongWritable 与 Text 这一基本的数据类型。而两个默认重载的方法 createRecordReader 与 isSplitable 负责对输入的数据进行分片划分输入分区。在这一节中,我们主要讨论自定义的分区输入方法。

 

输入记录与分区

在前面我们知道,一个输入分片就是由单个 Map 处理的输入数据块,每个 Map 只会处理一个分片。而每个分片则又被分成若干个记录来表示。而每条记录又是由键值对构成的一系列数据组成,可以通过 Map 一个接一个的处理输入进来的每条数据。

在上面我们已经看到,任何一个输入的数据格式都是继承自 FileInputFormat,而FileInputFormat 的源码如下所示:

 

public abstract classFileInputFormat<K, V> extends InputFormat<K, V> {

 

具体的代码细节我们这里不做讨论,但从上面我们还是可以看到,一个 FileInputFormat是继承自 InputFormat。 InputFormat 是一个抽象类,其自定义了两个方法, InputFormat 的源码如下所示:

publicabstractclass InputFormat<K, V> {

        // 使用一个list存储内容

        publicabstract List<InputSplit> getSplits(JobContext context) throws IOException, InterruptedException;

        // 要调用者实现读取方法

        publicabstract RecordReader<K, V> createRecordReader(InputSplit split, TaskAttemptContext context)

                          throws IOException, InterruptedException;

}

 

从上面的源代码我们可以看到, getSplits 与 createRecordReader 是两个默认的抽象方法,而接受的参数分别为 JobContext、 InputSplit、 TaskAttemptContext 这三种类型变量。 JobContext我们在前面已经讲过,是对上下文进行设置,通过写入写出键值对信息供 Map 方法与 Reduce方法使用。

 

InputSplit 其包含一个以字节为单位的长度和一组存储位置,分片不包含数据本身,而是指向数据的引用。需要注意的是, InputSplit 是由 InputFormat 创建的,一般无需应用开发人员处理。 InputFormat 负责产生输入分片并将它们分割成记录。

 

对于 TaskAttemptContext 来说,我们知道,用户向 Hadoop 提交 Job(作业), Job 在JobTracker 对象的控制下执行。 Job 不是独立完成的, Job 提交后, Hadoop 根据集群的规模将 Job 分解为若干个 Task(任务),然后分发到集群中,在 TaskTracker 的控制下运行。 Task包括 Map Task 和 Reduce Task,是 MapReduce 的 Map 操作和 Reduce 操作的地方。

 

InputFormat源码及执行过程分析

下面我们继续对源码进行分析,当客户端也就是 MapReduce 应用程序运行任务,并通过 getSplits 方法对输入的数据进行分片,然后将其传送给 JobTracker,等待 TaskTracker 对数据进行再次的处理。

 

在 TaskTracker 中, InputFormat 将通过调用 createRecordReader 方法来获取分片中的数据,并从中获得 RecordReader 作为返回值,这点在源码中可以清晰的看到。在这里我们还需要讲解下 RecordReader,其源码如下所示:

public abstract class RecordReader<KEYIN, VALUEIN>implements Closeable {

        public abstract void initialize(InputSplit split, TaskAttemptContext context) throws IOException,

                          InterruptedException;// 初始化方法

        // 判断是否还有下一个

        publi cabstract boolean nextKeyValue() throwsIOException, InterruptedException;

        // 返回key

        publicabstract KEYIN getCurrentKey() throwsIOException, InterruptedException;

        // 返回value

        publicabstract VALUEIN getCurrentValue() throwsIOException;

        publicabstractfloat getProgress() throwsIOException, InterruptedException; //返回过程值

        publicabstractvoid close() throwsIOException; // 关闭方法

}

 

上面可以看到, RecordReader 基本上就是一个对于输入记录的迭代器, Map 类使用这个迭代器去获取其中的键值对。对于这种说法,可能有读者还有疑问。不过没关系,源码说话。 Map 中 run 方法代码如下所示:

 

publicclassMap<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {

 

        publicvoid run(Context context) throwsIOException, InterruptedException {

                 setup(context); // 调用 setup

                 while (context.nextKeyValue()) { // 判断结束

                          Map(context.getCurrentKey(), context.getCurrentValue(),context); // 获取对应值

                 }

                 cleanup(context); // 清理方法

        }

}

我们可以看到,context中的nextKeyValue 方法被反复调用从而使得其中包含的键值对的信息能够写入Map中,当其中 nextKeyValue 一旦返回值为 false,即读取任务结束, Map 调用 cleanup 方法进行后续处理,写入也随之结束。

 

实现自己的的 RecordReader 类

在前面我们已经看到,任何一个自定义的 InputFormat 都是实现了 FileInputFormat 抽象类的一个具体类,而任何一个继承自 FileInputFormat 的类,都要实现其中抽象方法。其InputFormat 类的源码如下所示:

publicabstractclass InputFormat<K, V> {

        // 实现分片方法

        publicabstract List<InputSplit> getSplits(JobContext context) throws IOException, InterruptedException;

 

        // 实现创建记录方法

        publicabstract RecordReader<K, V> createRecordReader(InputSplit split, TaskAttemptContext context)

                          throws IOException, InterruptedException;

}

 

这里我们看到,抽象的 InputFormat 中含有两个抽象方法 getSplits 与 createRecordReader。我们知道,对于 split 分片来说,无需用户对其进行管理和设置,那么我们只需要定义我们所需要的 createRecordReader 即可。程序代码如下所示:

classMyInputFormat extends FileInputFormat<LongWritable, Text> { // 创建输入格式

        @Override

        public RecordReader<LongWritable, Text>createRecordReader( // 创建输入记录

                          InputSplitsplit, TaskAttemptContext context) throws IOException, InterruptedException {

                 returnnew LineRecordReader(); // 返回输入记录实例

        }

}

 

在代码中我们可以看到,我们自定义的 MyInputFormat 类继承自 FileInputFormat,并且通过对其进行泛型的设定从而制定输入输出类型。返回一个 RecordReader 类型的实例。

读者看到这里会发生迷糊,那么这个 RecordReader 又是什么?如何去定义。

小提示:是不是还有个 RecordWriter?

对于读者的疑问,我们还是使用传统的先看源码后分析的方法进行解答,对于我们在上面自定义的 MyInputFormat 类中,返回的是一个 LineRecordReader ,这里我们看下LineRecordReader 的源码,如下所示:

publicclassLineRecordReader extends RecordReader<LongWritable, Text> {

        // 初始化方法

        publicvoid initialize(InputSplit genericSplit,TaskAttemptContext context) throwsIOException {

        }

        publicboolean nextKeyValue() throwsIOException { // 实现的判断下一值方法

        }

        public LongWritable getCurrentKey() { // 返回 key 值的方法

        }

        public Text getCurrentValue() { // 返回 value 值的方法

        }

        publicfloat getProgress() { // 获取过程的方法

        }

}

对于上述代码,我们并不是要分析其主要的写作细节,而是看到, LineRecordReader 是继承自 RecordReader,并实现其中的抽象方法, RecordReader 代码如下所示:

 

publicabstractclass RecordReader<KEYIN, VALUEIN>implements Closeable {

       

        publicabstractvoid initialize(InputSplit split, TaskAttemptContext context) throws IOException,

                          InterruptedException;// 初始化方法

 

        // 判断是否还有下一个

        publicabstractboolean nextKeyValue() throwsIOException, InterruptedException;

 

        // 返回key

        publicabstract KEYIN getCurrentKey() throwsIOException, InterruptedException;

 

        // 返回value

        publicabstract VALUEIN getCurrentValue() throwsIOException;

 

        publicabstractfloat getProgress() throwsIOException, InterruptedException; //返回过程值

 

        publicabstractvoid close() throwsIOException; // 关闭方法

}

 

上面代码可以看到,对于 RecordReader 所有的子类来说,即需要实现其中对应的抽象方法,而其中最重要的又是由 nextKeyValue、 getCurrentKey、 getCurrentValue 组成。这些组成在一起构成了最基本的结构。

 

小问题:实际上就是实现了父类未实现的方法,那么我们同样去继承,该怎么办呢?

 

现在读者对 RecordReader 已经有了一个大概的了解,所谓的 RecordReader 类在 Hadoop框架中,就是告诉 Map 类如何从数据中读取我们所需要的内容,读取的格式是什么,如何读取,每次读取的长度是多少等。

 

下面我们就实现我们自己的 RecordReader。

classMyRecordReader extends RecordReader<NullWritable, Text> {

        private FileSplit split; //定义输入分片

        private Configuration conf; //定义环境变量

        private Text value; //定义值

        privatebooleanflag; // 创建一个辅助布尔值

 

        @Override

        publicvoid close() throws IOException { // 默认的关闭方法,空实现

        }

 

        // 默认的返回键的方法

        @Override

        public NullWritable getCurrentKey() throwsIOException, InterruptedException {

                 return NullWritable.get(); // 返回空值

        }

 

        // 返回值方法

        @Override

        public Text getCurrentValue() throwsIOException, InterruptedException {

                 returnvalue; // 返回当前值

        }

 

        @Override

        publicfloat getProgress() throwsIOException, InterruptedException {//对过程进行监视

                 return 0;

        }

 

        // 初始化方法,执行一次

        @Override

        publicvoid initialize(InputSplit split,TaskAttemptContext context) throwsIOException,

                          InterruptedException{

                 this.split = (FileSplit) split;

                 this.conf = context.getConfiguration();

        }

 

        @Override

        publicboolean nextKeyValue() throwsIOException, InterruptedException { //键值对方法

                 if (!flag) { //判断是否处理完毕

 

                          Pathfile = split.getPath(); // 获得分片路径

                          byte[] fileBuffer = newbyte[(int) split.getLength()]; // 创建辅助数组

                          FileSystemfs =FileSystem.get(conf); // 创建文件系统

                          FSDataInputStreaminput = fs.open(file); // 创建输入流

                          input.read(fileBuffer); //读入辅助字符数组

                          Stringstr = new String(fileBuffer); // 创建辅助字符串

                          value = new Text(str); // 对值进行赋值

                          flag = true; //更改表计量

                          returntrue; // 返回值

                 }

                 returnfalse; // 返回值

        }

}

这段代码的需求是将输入的数据作为一个整体进行存储并分类。从上面我们看到,实现的 MyRecordReader 是继承自 RecordReader,然后重写了其中的 initialize、 nextKeyValue 这两个主要方法。

 

在 initialize 方法中,我们对所需要的数据进行了进行了初始化操作,对其中的全局变量进行了初始化并赋值。初始化完毕后, nextKeyValue 为我们提供了对下一个键值对的获取,从 Map 的代码如下所示:

publicclassMap<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {

        publicvoid run(Context context) throwsIOException, InterruptedException {

                 setup(context); // 调用 setup 方法

                 while (context.nextKeyValue()) { // 判断是还有内容

                          Map(context.getCurrentKey(), context.getCurrentValue(),context); // 获取对应值

                 }

                 cleanup(context); // 关闭方法

        }

}

 

对于 Map 来说,如果有下一个键值对的存在则继续将键值对写入(通过 while 循环确认),如果没有则执行 cleanup()完成剩余工作。在这里我们是用的是 nextKeyValue 用以判断下一个键值是否存在,并完成对键值的赋值。 flag 是一个布尔变量用以处理记录的处理情况。 nextKeyValue 在反复的调用过程中对 flag进行判断,如果 flag 为 false,则表示未对数据进行处理, true 表示处理完毕。通过 split 可以获得输入分片的地址,从而可以打开数据输入,字节数组用以存放输入的二进制流并在后续将其转换成字符串类型输出到对应的 value 中。

 

小提示:原来就是在 Map 中的一个遍历方法啊。通过遍历其中的值并依此返回 key 值和 Value 值进行处理。

 

自定义的 FileInputFormat 类

FileInputFormat 层次图如图所示。


 

从 Hadoop 自带的 TextInputFormat 中我们可以看到,任何一个定义的 InputFormat 都是继承自 FileInputFormat。

而 InputFormat 主要提供了两个功能,第一是确认输入数据文件的来源路径,第二就是输入文件生成分片的实现。

首先对于输入路径来说, InputFormat 给我们提供了若干静态方法可以对输入的数据进行获取。

 

        publicstaticvoid addInputPath(Job job, Path path) //添加路径方法

        publicstaticvoid addInputPaths(Job job, StringcommaSeparatedPaths) //重载添加路径方法

        publicstaticvoid setInputPaths(Job job, Path... inputPaths) //添加多个路径方法

        publicstaticvoid setInputPaths(Job job, StringcommaSeparatedPaths ) //重载添加多个路径方法

 

对于上面代码中 add 与 set 的区别,其中 addInputPath 与 addInputPaths 这两个方法可以将一个或多个路径添加到路径列表,其可以多次进行调用或者添加多个路径来建立输入文件路径列表。 setInputPaths 可以用来替换前面 add 的内容,更改为新的路径。如果需要排除特定文件,可以使用 FileInputFormat 的 setInputPathFilter()设置一个过滤器:

 

        publicstaticvoid setInputPathFilter(Job job, Class<? extends PathFilter>filter); //设置过滤器方法

 

有些应用程序可能不希望文件被切分,而是用一个 Map 完整处理每一个输入文件,有两种方法可以保证输入文件不被切分。

   第一种方法就是增加最小分片大小,将它设置成大于要处理的最大文件大小。

   第二种方法就是使用 FileInputFormat 具体子类,并且重载 isSplitable()方法,把其返回值设置为 false,代码如下所示:

 

publicclassNonSplittableTextInputFormat extends TextInputFormat {

        @Override

        protectedboolean isSplitable(JobContext context, Path file) { //确认作为输入不分片

                 returnfalse;

        }

}

 

下面的程序为我们演示了将输入数据作为一个整体进行处理的例子:

publicclassToolRunnerWithMyRecordReader extends Configured implementsTool {

        @Override

        publicint run(String[] args) throws Exception{

                 Jobjob = new Job(getConf()); // 获取当前环境

                 job.setJarByClass(getClass()); // 设置任务类

                 FileSystemfs =FileSystem.get(getConf()); // 创建文件系统

                 fs.delete(new Path("out"), true); // 删除已存在的文件目录

                 FileInputFormat.addInputPath(job, new Path("sample.txt")); // 创建输入数据路径

                 FileOutputFormat.setOutputPath(job, new Path("out"));// 创建输出文件路径

                 job.setInputFormatClass(MyInputFormat.class); // 设置输入格式类

                 job.setMapClass(Map.class); // 默认设置 Map

                 job.setPartitionerClass(HashPartitioner.class); // 设置分片

                 job.setNumReduceTasks(1); // 设置 Reduce 任务数

                 job.setReduceClass(Reduce.class); // 设置Reduce

                 job.setOutputKeyClass(NullWritable.class); // 设置输出键格式

                 job.setOutputValueClass(Text.class); // 设置输出值格式

                 job.setOutputFormatClass(TextOutputFormat.class); // 设置输出格式类

                 job.waitForCompletion(true); // 运行MapReduce

                 return 0;

        }

 

        publicstaticvoid print(Tool tool) throws Exception {

                 FileSystemfs =FileSystem.get(tool.getConf()); //获取当前环境

                 Pathpath = new Path("out/part-r-00000"); // 建立路径

                 FSDataInputStreamfsin = fs.open(path); // 创建输入流

                 intlength = 0; // 创建辅助变量

                 byte[] buff = newbyte[128]; // 辅助数组

                 while ((length = fsin.read(buff, 0, 128))!= -1) { // 读取内容

                          System.out.println(new String(buff, 0, length)); //打印内容

                 }

        }

 

        publicstaticvoid main(String[] args) throws Exception { //主方法

                 Tooltool = new ToolRunnerWithMyRecordReader(); // 确立Tool 方法

                 ToolRunner.run(tool, args); //运行

                 print(tool);

        }

}

 

classMyInputFormat extends FileInputFormat<NullWritable, Text> { // 设置输入格式

        // 设置recordReader

        @Override

        public RecordReader<NullWritable, Text>createRecordReader(InputSplit split, TaskAttemptContext context)

                          throws IOException, InterruptedException {

                 returnnew MyRecordReader(); // 返回recordReader

        }

 

        @Override

        protectedboolean isSplitable(JobContext context, Path filename) {

                 returntrue; // 进行分片处理

        }

}

 

classMyRecordReader extends RecordReader<NullWritable, Text> {

        private FileSplit split; //定义输入分片

        private Configuration conf; //定义环境变量

        private Text value; //定义值

        privatebooleanflag; // 创建一个辅助布尔值

 

        @Override

        publicvoid close() throws IOException { // 默认的关闭方法,空实现

        }

 

        // 默认的返回键的方法

        @Override

        public NullWritable getCurrentKey() throwsIOException, InterruptedException {

                 return NullWritable.get(); // 返回空值

        }

 

        // 返回值方法

        @Override

        public Text getCurrentValue() throwsIOException, InterruptedException {

                 returnvalue; // 返回当前值

        }

 

        @Override

        publicfloat getProgress() throwsIOException, InterruptedException {//对过程进行监视

                 return 0;

        }

 

        // 初始化方法,执行一次

        @Override

        publicvoid initialize(InputSplit split,TaskAttemptContext context) throwsIOException,

                          InterruptedException{

                 this.split = (FileSplit) split;

                 this.conf = context.getConfiguration();

        }

 

        @Override

        publicboolean nextKeyValue() throwsIOException, InterruptedException { //键值对方法

                 if (!flag) { //判断是否处理完毕

 

                          Pathfile = split.getPath(); // 获得分片路径

                          byte[] fileBuffer = newbyte[(int) split.getLength()]; // 创建辅助数组

                          FileSystemfs =FileSystem.get(conf); // 创建文件系统

                          FSDataInputStreaminput = fs.open(file); // 创建输入流

                          input.read(fileBuffer); //读入辅助字符数组

                          Stringstr = new String(fileBuffer); // 创建辅助字符串

                          value = new Text(str); // 对值进行赋值

                          flag = true; //更改表计量

                          returntrue; // 返回值

                 }

                 returnfalse; // 返回值

        }

}

 

除了我们自定义的 InputFormat 类, Hadoop 框架还为我们提供了大量的 InputFormat 类用于处理不同的数据输入和输出。

下面我们来分析和详解一些最常用的 InputFormat 类。

首先我们回忆下在我们默认的 setInputFormat 方法中设定的输入格式,及最常见的是TextInputFormat。

TextInputFormat 是 Hadoop 中定义的默认输入的 InputFormat,其重写了 FileInputFormat中的 createRecordReader 和 isSplitable 方法。

该类使用的 reader 是 LineRecordReader,即以回车键(CR = 13)或换行符(LF = 10)为行分隔符。

TextInputFormat 中对于数据的每条记录作为一个输入,键是 LongWritable 类型,存储该行在数据文件中的偏移量,而其对应的值就是输入的整个行的数据。

 

对 TextInputFormat 的具体使用我们不再进行说明,下面我们来看其处理内容,对于我们输入的文件格式如下:

Helloworld goodbye world

Hellohadoop goodbye hadoop

那么其处理结果如下所示:

(0,Helloworld goodbye world)

(24,Hellohadoop goodbye hadoop)


上面的key 明显不是行号,因为每个分片需要单独处理的原因,行号只是一个片内的顺序标记,所以在分片内在的行号是可以的,而在文件中是很难办到的。然而为了使 key 是唯一的,我们可以利用已知的上一个分片的大小,计算出当前位置在整个文件中的偏移量(不是行号),这样加上文件名,就能确定唯一 key,如果行固定长,就可以算出行号。

 

看到这里可能会问,如果一个文件被分成很多行,其中从开头到最后一行的数据容量超过 HDFS 块的边界,那么该如何解决呢?效果如图所示。

 

答案就是因为 FileInputFormat 定义的是逻辑结构,不能匹配 HDFS 块大小,所以TextFileInputFormat 的以行为单位的逻辑记录中,即使某一行是跨文件块存储的,那么只需要 Map 进行一些远程读取而可以正常的获得数据信息,其对资源的需求与开销是可以忽略不计的。

 

OutputFormat 输出格式详解

上一节中我们对输入格式设置进行了学习,下面我们对输出格式进行了解。我们将同大家一起探讨怎样自定义最后一个步骤—即怎样写输出文件。 OutputFormat 将 Map/Reduce作业的输出结果转换为其他应用程序可读的方式,从而轻松实现与其他系统的互操作。

 

OutputFormt 接口决定了在哪里以及怎样持久化作业结果。Hadoop 为不同类型的格式提供了一系列的类和接口,实现自定义操作只要继承其中的某个类或接口即可。你可能已经熟悉了默认的 OutputFormat,也就是 TextOutputFormat,它是一种以行分隔,包含制表符界定的键值对的文本文件格式,下面我们从 TextOutputFormat 开始,讲解 OutputFormat 的使用和定义方法。

 

OutputFormat默认输出格式

我们知道,默认的输出格式是 TextOutputFormat,其作用是将 Reduce 处理的结果作为文本输出,键值对输入时可以是任何类型,输出时由 TextOutputFormat 转为字符串。其源代码如下所示:

 

public class TextOutputFormat<K,V> extends FileOutputFormat<K, V> {

      protectedstatic class LineRecordWriter<K, V>{

 

             if(!nullKey) { //判断 key 是否为空

             writeObject(key);//写出key

             }

             if(!(nullKey || nullValue)) { //判断 key 或 value 是否为空

             out.write(keyValueSeparator);//写出分隔符

             }

             if(!nullValue) { //判断 value 是否为空

             writeObject(value);//写出value

             }

      }

            

      publicRecordWriter<K, V> getRecordWriter(TaskAttemptContext job) throws IOException,

             InterruptedException{

      }

}

 

从源代码我们可以看到,根据传来的内容进行写入操作,直接调用相应的写操作。其中需要注意的是, TextOutputFormat 是继承自 FileOutputFormat,我们看下 FileOutputFormat的源码:

 

publicabstractclass FileOutputFormat<K, V>extendsOutputFormat<K, V> {

        publicabstract RecordWriter<K, V>getRecordWriter(TaskAttemptContext job) throwsIOException,

                          InterruptedException;// 获取getRecordWriter

}

 

从中我们可以看到,任何一个继承自 FileOutputFormat 的自定义 OutputFormat 都需要实现其中的 getRecordWriter 方法。

小提示:请读者回忆下我们在学习 InputFormat 中实现的 FileInputFormat 需要实现的对RecordReader 中的实现。

这里也是一样,需要向 Hadoop 框架描述写入的方法。我们来看 RecordWriter的具体源代码,如下所示:

 

publicabstractclass RecordWriter<K, V> {

        publicabstractvoid write(K key, V value) throws IOException, InterruptedException;

        publicabstractvoid close(TaskAttemptContext context) throws IOException, InterruptedException;

}

 

getRecordWriter 用于返回一个 RecordWriter 的实例, Reduce 任务在执行的时候就是利用这个实例来输出 Key/Value 的。(如果 Job 不需要 Reduce,那么 Map 任务会直接使用这个实例来进行输出。)

 

RecordWriter 有如下两个方法:void write(K key, Vvalue)与 close(TaskAttemptContext context);

前者负责将 Reduce 输出的 Key/Value 写成特定的格式,后者负责对输出做最后的确认并关闭输出。前面提到的 OutputFormat 的字面含义,其实就是由这个 RecordWriter 来实现的。

 

自定义 OutputFormat 输出格式

上面我们已经对 OutputFormat 的源码进行了分析,在分析的基础上,我们既可以自定义我们自己的 OutputFormat。

与自定义 InputFormat 相类似,我们在定义 OutputFormat 的时候必须要首先让其继承自FileOutputFormat,代码如下所示:

 

classMyOutoutFormat extends FileOutputFormat {

        @Override

        public RecordWriter getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {

                 returnnull;

        }

}

 

在上面代码示例中,需要我们自定义的MyOutoutFormat去实现FileOutputFormat 中的抽象方法,

getRecordWriter(TaskAttemptContextjob)。其中的返回值要求是 RecordWriter 类型。在进行下一步之前,还是请读者回头查阅我们在定义 InputFormat 中对 RecordReader 中的描述。

RecordWriter 的源码我们已经基本介绍完毕,那么我们需要做的就是实现我们自定义的RecordWriter。代码如下所示:

 

classMyRecordWriter extends RecordWriter {

        TaskAttemptContextjob; // 定义辅助任务变量

        FSDataOutputStreamfsout; // 定义辅助写出流变量

        Configurationconf; // 定义辅助环境变量

        FileSystem fs; // 定义辅助文件系统变量

 

        public MyRecordWriter(TaskAttemptContext job) { // 构造方法初始化

                 this.job = job; //任务变量初始化

                 conf = job.getConfiguration(); // 获得环境变量

                 try {

                          init();// 初始化

                 } catch (Exception e) {

                          e.printStackTrace();

                 }

        }

 

        publicvoid init() throws Exception { // 初始化方法

                 fs =FileSystem.get(conf); // 获得文件系统

                 fsout = fs.create(new Path("out/result.txt")); //创建输出路径

        }

 

        @Override

        publicvoid close(TaskAttemptContext context) throws IOException, //关闭方法

                          InterruptedException{

                 fsout.close(); // 关闭输出流

        }

 

        @Override

        publicvoid write(Object key, Object value) throws IOException, InterruptedException {

                 Textval = (Text) value; //类型转换

                 val.write(fsout); //写入输出流

                 val.writeString(fsout, "\n"); //写完一行后换行

        }

}

 

首先我们看到最基本的实现方法, write 与 close。在前面我们已经分析过, write 就是为了向指定的文件系统中写入对应的数据,在这里我们使用的是直接对 Text 类型的写入,调用其相应的 write 方法把数据写入输出流中,并且为了更好的表示,每次写入结束后紧接一个换行符。

 

小问题:看到这里读者可能会为,我们使用相应类型的 write 方法,那么如何获取相应的输入流?

对于此的解释,我们可以看FileInputFormat中的抽象方法,其形参即为TaskAttemptContext,对上下文环境的设置,通过此我们很容易的获得当前的环境变量与存储设置。自定义的 OutputFormat 如程序所示:

 

publicclassMyOutputFormatSample extends Configured implementsTool {

        publicstaticvoid main(String[] args) throws Exception { //运行主方法

                 Tooltool = new MyOutputFormatSample(); // 创建运行类

                 ToolRunner.run(tool, args); //运行当前类

                 print(tool); // 打印结果

        }

 

        @Override

        publicint run(String[] args) throws Exception{ // 主方法

                 Configurationconf = getConf(); //获得当前环境

                 Jobjob = new Job(conf); // 获得当前任务

                 job.setJarByClass(getClass()); // 注入主类

                 FileInputFormat.addInputPath(job, new Path("sample.txt")); // 设置输入路径

                 FileOutputFormat.setOutputPath(job, new Path("out"));// 设置输出路径

                 FileSystemfs =FileSystem.get(conf); // 获得输出文件系统

                 fs.delete(new Path("out"), true); // 删除已存在的目录

                 job.setOutputFormatClass(MyOutoutFormat.class); // 设置自定义输出格式

                 job.waitForCompletion(true); // 运行任务

                 return 0; // 获得返回值

        }

 

        publicstaticvoid print(Tool tool) throws Exception { //打印结果方法

                 FileSystemfs =FileSystem.get(tool.getConf()); //获得文件系统

                 Pathpath = new Path("out/result.txt"); // 获得路径

                 FSDataInputStreamfsin = fs.open(path); // 创建输入流

                 intlength = 0; // 辅助变量

                 byte[] buff = newbyte[128]; // 辅助变量

                 while ((length = fsin.read(buff, 0, 128))!= -1) { // 打印结果

                          System.out.println(new String(buff, 0, length)); //打印结果

                 }

        }

}

 

classMyOutoutFormat extends FileOutputFormat { // 自定义的OutFormat

        @Override

        public RecordWriter getRecordWriter(TaskAttemptContext job) /

                          throws IOException, InterruptedException {

                 returnnew MyRecordWriter(job);

                 // 创建自定义的 MyRecordWriter 类,并传入形参 job 用以传递环境设置

        }

}

 

classMyRecordWriter extends RecordWriter { // 自定义的RecordWriter

        TaskAttemptContextjob; // 任务定义

        FSDataOutputStreamfsout; // 定义输出流

        Configurationconf; // 定义环境变量

        FileSystem fs; // 定义文件系统

 

        public MyRecordWriter(TaskAttemptContext job) { // 构造方法用以接受传递环境变量并初始化

                 this.job = job; //初始化任务

                 conf = job.getConfiguration(); // 获得环境变量

                 try {

                          init();// 初始化

                 } catch (Exception e) {

                          e.printStackTrace();

                 }

        }

 

        publicvoid init() throws Exception { // 初始化方法

                 fs =FileSystem.get(conf); // 创建文件系统

                 fsout = fs.create(new Path("out/result.txt")); //创建输出流

        }

 

        @Override

        publicvoid close(TaskAttemptContext context) throws IOException, //关闭输出流

                          InterruptedException{

                 fsout.close(); // 关闭输出流

        }

 

        @Override

        publicvoid write(Object key, Object value) throws IOException, //写数据方法

                          InterruptedException{

                 Textval = (Text) value; //对值进行强制转型

                 val.write(fsout); //写入数据

                 val.writeString(fsout, "\n"); //写入换行符

        }

}

 

从代码可见,对于自定义的 RecordReader 需要接受当前运行程序的环境变量,从而可以在内部加载相应的环境变量并获得相应的存储系统。在这里我们需要知道的一个内容就是对于在 main 方法中设置的输出文件路径与我们在自定义的输出文件路径要一致。

 

最后我们说下对于 FileOutputFormat 中还有一个重要的方法 checkOutputSpecs,源代码如下所示:

 

        publicvoid checkOutputSpecs(JobContext job) throws FileAlreadyExistsException, IOException {

                 PathoutDir = getOutputPath(job); // 获取输出路径

                 if (outDir == null) { // 判断是否存在

                          thrownew InvalidJobConfException("Output directory not set.");

                 }

                 if (outDir.getFileSystem(job.getConfiguration()).exists(outDir)) { // 判断是否已有文件

                          thrownew FileAlreadyExistsException("Output directory " + outDir + "already exists");

                 }

        }

 

其作用是对已经存在的目录进行查看,若需修改,读者可以自行修改查看。

 

对 Reduce 任务数进行设置

在 Hadoop 中默认是运行一个 Reduce,所有的 Reduce 任务都会放到单一的 Reduce 去执行,效率非常低下。为了提高性能,可以适当增大 Reduce 的数量。最优的 Reduce 数量取决于集群中可用的 Reduce 任务槽的数目。 Reduce 任务槽的数目是集群中节点个数与Mapred.tasktracker.Reduce.tasks.maximum(默认为 2)的乘积,也可以通过 MapReduce 的用户界面获得。

 

一个普遍的做法是将 Reduce 数量设置为比 Reduce 任务槽数目稍微小一些,这会给Reduce 任务留有余地,同时将使得 Reduce 能够在同一波中完成任务,并在 Reduce 阶段充分使用集群。

 

小提示: Reduce 的数量由 Mapred.Reduce.tasks 属性设置,通常在 MapReduce 作业的驱动方法中通过 setNumReduceTasks(n)调用方法动态设置 Reduce 的数目为 n。

 

这里需要注意的是,在本地环境中进行任务处理的时候,即构建本地测试环境时,必须使用 0 个或 1 个 Reduce。

 

为了更好的演示结果,我们对数据进行适当的修改,以下是我们定义的数据值,请读者自行上传值服务器。

 

helloHadoop

GoodWordHadoop

helloHadoop

GoodWordHadoop

helloHadoop

GoodWordHadoop

helloHadoop

GoodWordHadoop

helloHadoop

GoodWordHadoop

helloHadoop

GoodWordHadoop

 

程序演示了对 Reduce 任务进行设置的方法,代码如下所示:

 

publicclassMRWith3Reduce extends Configured implementsTool {

        publicstaticvoid main(String[] args) throws Exception { //主方法

                 Tooltool = new MRWith3Reduce(); // 定义主类

                 ToolRunner.run(tool, args); //运行主类

                 print(tool); // 打印结果

        }

 

        @Override

        publicint run(String[] args) throws Exception{ // 设置运行方法

                 Configurationconf = getConf(); //获得环境变量

                 Jobjob = new Job(conf); // 建立任务

                 job.setJarByClass(getClass()); // 设置任务 Jar

                 FileInputFormat.addInputPath(job, new Path("sample.txt")); // 设置输入文件路径

                 FileOutputFormat.setOutputPath(job, new Path("out"));// 设置输出文件路径

                 job.setNumReduceTasks(3); // 设置同时进行任务数

                 FileSystemfs =FileSystem.get(conf); // 设置文件系统

                 fs.delete(new Path("out"), true); // 删除已有目录

                 job.waitForCompletion(true); // 运行任务

                 return 0;

        }

 

        publicstaticvoid print(Tool tool) throws Exception { //打印结果方法

                 FileSystemfs =FileSystem.get(tool.getConf()); //获得文件系统

                 Pathpath = new Path("out/result.txt"); // 获得路径

                 FSDataInputStreamfsin = fs.open(path); // 创建输入流

                 intlength = 0; // 辅助变量

                 byte[] buff = newbyte[128]; // 辅助变量

                 while ((length = fsin.read(buff, 0, 128))!= -1) { // 打印结果

                          System.out.println(new String(buff, 0, length)); //打印结果

                 }

        }

}

 

我们需要注意的是,对于使用多个 Reduce 进行任务处理时,必须使用集群才能够进行运行,我们需要将文件打包为 Jar 文件进行处理。

请读者将文件产生为 Jar 文件,并运行如下命令行代码:

$ hadoop jar MuliTest.jar MuliTest

对于运行过程的观察,我们很明显的可以看到, Reduce 任务被分成了 3 份进行运行,如图所示:

 

 

从上图我们可以很清楚的看到,原本一个的 Reduce 任务被分成了 3 Reduce 任务进行处理,而对于输出结果的显示,我们可以看到,最终结果被分成了三份进行存放,如图所示。

 

 

OutputFormat分区类Partitioner 详解

在讲解这一章之前,我们首先查看下上一节中任务处理结果,首先我们来依次看下构成的文件内容,如图所示。





 

 

从上面三个图中我们可以看到,对于分区后的 Reduce 来说,只是依次使用了三个Reduce进行数据处理,其中按输入的数据进行分部处理。那么,如果我们需要对任务处理的分配更加细粒度一点,也就是按输入的数据要求进行任务分配,那该又如何呢?

 

小提示:在进行前面学习中,我们是对输入输出数据进行的定义进行了设置,并且对文件的输出存放位置,定义及其来历做了较好的理解。但是读者也注意到,在 Hadoop 运行过程中,每个 Reduce 运行一个任务,并且给予一个固定的输出文件名“ part-r-00000”,这是根据 Reduce 进行任务时的分区进行的定义,如果我们希望任务能够不同的内容选择不同的分区,该采取何种办法呢?

首先我们回忆下我们在前面进行使用默认的处理类对分区的划分,代码如下所示:

                 job.setNumReduceTasks(1); //设置默认一个 Reduce

                 job.setPartitionerClass(HashPartitioner.class); //设置分片方法

 

从上面代码来看这里设置了两个参数, job.setNumReduceTasks(1)的定义我们在上一节中已经说明,下面我们来看下job.setPartitionerClass(HashPartitioner.class)

 

在进行分析之前可以直接告诉读者, Partitioner 类就是为Map 处理后的文件确定使用哪个 Reduce 进行处理,默认情况下使用的是 HashPartitioner,其所对应的键进行哈希操作用于决定该记录使用哪个分区进行处理,每个分区对应一条记录,其源码如下所示:

publicclassHashPartitioner<K, V>extends Partitioner<K, V> {

        publicint getPartition(K key, V value, intnumReduceTasks) { //判断分区执行方法

                 return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks; //判断方法

        }

}

 

从中我们可以看到,每条记录的键的信息被转化成一个非负整数,其值与最大的整型值进行一次按位与操作获得。

一般情况下 MapReduce 的使用者通常会指定 Reduce 任务和 Reduce 任务输出文件的数量。我们在中间 key 上使用分区方法来对数据进行分区,之后再输入到后续任务执行进程。一个缺省的分区函数是使用 hash 方法(比如, hash(key) mod R)进行分区。 hash 方法能产生非常平衡的分区。

小提示:比如,输出的 key 值是 URLs,我们希望每个主机的所有条目保持在同一个输出文件中。

为了支持类似的情况, MapReduce 库的用户需要提供专门的分区方法。以上是对 HashPartitioner 类的介绍,下面需要我们自定义自己的 HashPartitioner 类。代码如下所示:

 

classMyPartitioner extends Partitioner {

        @Override

        publicint getPartition(Object key, Object value, intnumPartitions) { // 自定义的分区方法

                 if (value.toString().startsWith("GoodWord")) //判断内容

                          return 1; // 返回值

                 if (value.toString().startsWith("hello")) // 判断内容

                          return 2; // 返回值

                 else

                          return 0; // 默认返回值

        }

}

 

在这里我们对输入的值进行处理,根据输入的值不同,返回不同的分类值,这样情况下,具有相同分类的键的记录就被归于同一个 Reduce 进行处理。

完整代码如程序所示:

publicclassMRWith3ReduceSample extends Configured implementsTool {

        publicstaticvoid main(String[] args) throws Exception { //主方法

                 Tooltool = new MRWith3Reduce(); // 定义主类

                 ToolRunner.run(tool, args); //运行主类

                 print(tool); // 打印结果

        }

 

        @Override

        publicint run(String[] args) throws Exception{ // 设置运行方法

                 Configurationconf = getConf(); //获得环境变量

                 Jobjob = new Job(conf); // 建立任务

                 job.setJarByClass(getClass()); // 设置任务 Jar

                 FileInputFormat.addInputPath(job, new Path("sample.txt")); // 设置输入文件路径

                 FileOutputFormat.setOutputPath(job, new Path("out"));// 设置输出文件路径

                 job.setNumReduceTasks(3); // 设置同时进行任务数

                 job.setPartitionerClass(MyPartitioner.class); // 分区使用自定义分区类

                 FileSystemfs =FileSystem.get(conf); // 设置文件系统

                 fs.delete(new Path("out"), true); // 删除已有目录

                 job.waitForCompletion(true); // 运行任务

                 return 0;

        }

 

        publicstaticvoid print(Tool tool) throws Exception { //打印结果方法

                 FileSystemfs =FileSystem.get(tool.getConf()); //获得文件系统

                 Pathpath = new Path("out/result.txt"); // 获得路径

                 FSDataInputStreamfsin = fs.open(path); // 创建输入流

                 intlength = 0; // 辅助变量

                 byte[] buff = newbyte[128]; // 辅助变量

                 while ((length = fsin.read(buff, 0, 128))!= -1) { // 打印结果

                          System.out.println(new String(buff, 0, length)); //打印结果

                 }

        }

}

 

classMyPartitioner extends Partitioner { // 自定义分区类

        @Override

        publicint getPartition(Object key, Object value, intnumPartitions) { // 默认方法

                 if (value.toString().startsWith("GoodWord")) //对值的起始值进行判断

                          return 1; // 获得返回值

                 if (value.toString().startsWith("hello")) // 对值的起始值进行判断

                          return 2; // 获得返回值

                 else

                          return 0; // 获得返回值

        }

}

 

我们需要注意的是,对于使用多个 Reduce 进行任务处理时,必须使用集群才能够进行运行,我们需要将文件打包为 Jar 文件进行处理。请读者将文件产生为 Jar 文件,并运行如下命令行代码:

$ hadoop jar MRWith3ReduceSample.jar MRWith3ReduceSample

下面我们直接看下结果文件。首先根据需求分成三个输出文件,


 


Part-r-00000

 

 

 


从结果输出可以看到,对于结果输出来说,以上分成了 3 个分区, part-00000 是空目录,因为我们没有对其进行设置。而 part-00001 包含了全部以“ GoodWord”开头的数据,而part-00002 包含了以“ hello”为开头的全部数据内容。

 

MapReduce 相关特性详解

动态计数器

我们在上面可以看到,对于设定的计数器,我们可以通过在初始出进行设置枚举而获得计数器能够引用枚举中的类型从而提供计数的功能。但是有些时候,对于某些问题的产生,并不适合在枚举出提供出,例如一些产生的错误并不能在一开始的定义,那么则需要一个动态定义计数器的方法对数据进行定义。

Context 类中除了前面所述的使用 getCounter 方法获取枚举中值的方式外,还有一个重载的方法能够对当前计数器进行动态定义,其源码如下所示:

        public Counter getCounter(String groupName, String counterName) {

                 return reporter.getCounter(groupName, counterName);

        }

此方法通过重新动态定义计数器从而实现对信息的动态捕获。代码如程序所示:

 

packagecom.shawn.hadoop.mr.counter;

importjava.io.IOException;

importjava.util.Iterator;

importjava.util.StringTokenizer;

importorg.apache.hadoop.io.IntWritable;

importorg.apache.hadoop.io.LongWritable;

importorg.apache.hadoop.io.Text;

importorg.apache.hadoop.mapreduce.Mapper;

importorg.apache.hadoop.mapreduce.Reducer;

classTxtCounter { // 创建计数类

        staticenum ReportTest { //设置枚举作为错误归类

                 Male, Female// 枚举值

        }

        // 自定义的Mapper

        publicstaticclass TxtMapper extendsMapper<Object, Text, Text, IntWritable> {

        @Override

        protectedvoid map(Object key, Text value,Mapper<Object, Text, Text, IntWritable>.Context context)

                          throws IOException, InterruptedException {

                 Textword = new Text();

                 IntWritableone = new IntWritable(1);

                 StringTokenizeritr = new StringTokenizer(value.toString());

                 Stringstr;

                 while (itr.hasMoreTokens()) {

                          str = itr.nextToken();

                          if (str.equals("Male")){ // 对其中的值进行判断

                                   context.setStatus("Male"); // 写入状态

                                   //context.getCounter(ReportTest.Male).increment(1); // 计数增加

                                   context.getCounter("Gender count", "male").increment(1);

                          }elseif (str.equals("Female")) { // 对其中的值进行判断

                                   context.setStatus("Female"); // 写入状态

                                   //context.getCounter(ReportTest.Female).increment(1); // 计数增加

                                   context.getCounter("Gender count", "female").increment(1);

                          }

                          word.set(str);

                          context.write(word, one);

                 }

        }

}

        // 自定义的Reduce

        publicstaticclass TxtReducer extendsReducer<Text, IntWritable, Text, IntWritable> {

                 @Override

                 protectedvoid reduce(Text key, Iterable<IntWritable>values,

                                   Reducer<Text,IntWritable, Text, IntWritable>.Context context) throws IOException,

                                   InterruptedException{

                          intsum = 0; // 辅助计数

                          Iterator<IntWritable>it = values.iterator(); //获取值

                          while (it.hasNext()) { // 遍历

                                   IntWritablevalue = it.next(); // 取出值

                                   sum += value.get(); // 进行增加

                          }

                          context.write(key, newIntWritable(sum));

                 }

        }

}

 

packagecom.shawn.hadoop.mr.counter;

importorg.apache.hadoop.conf.*;

importorg.apache.hadoop.fs.*;

importorg.apache.hadoop.io.IntWritable;

importorg.apache.hadoop.io.Text;

importorg.apache.hadoop.mapreduce.Job;

importorg.apache.hadoop.mapreduce.lib.input.FileInputFormat;

importorg.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

importorg.apache.hadoop.util.Tool;

importorg.apache.hadoop.util.ToolRunner;

 

publicclassToolRunnerTest extends Configured implementsTool {

 

        @Override

        publicint run(String[] args) throws Exception{ // 驱动方法

                 Configurationconf = getConf(); //Configured中获取Configuration 实例

                 Jobjob = new Job(conf); // 创建任务实例

                 job.setJarByClass(ToolRunnerTest.class); // 创建工作类

 

                 Pathin_path = new Path("hdfs://localhost:9000/wufan/data4");

                 Pathout_path = new Path("hdfs://localhost:9000/wufan/out");

 

                 FileSystemfs = out_path.getFileSystem(conf);

                 fs.delete(out_path, true); //删除已存在的目录

 

                 FileInputFormat.addInputPath(job, in_path); // 建立输入文件路径

                 FileOutputFormat.setOutputPath(job, out_path); //建立输出文件路径

 

                 job.setMapperClass(TxtCounter.TxtMapper.class); // 设置自定义的 Mapper工作类

                 job.setReducerClass(TxtCounter.TxtReducer.class); // 设置自定义的 Reducer

 

                 job.setOutputKeyClass(Text.class); // 设置输出中键的类型

                 job.setOutputValueClass(IntWritable.class); // 设置输出中值的类型

                 job.waitForCompletion(true); // 开始程序运行

                 return 0;

        }

 

        publicstaticvoid print(Tool tool) throws Exception { //读取输出结果的方法

                 Path path = new Path("hdfs://localhost:9000/wufan/out/part-r-00000"); //创建读取文件路径

                 FileSystemfs = path.getFileSystem(tool.getConf());

                 FSDataInputStreamfsin = fs.open(path); // 打开文件

                 intlength = 0; // 设置辅助变量

                 byte[] buff = newbyte[128]; // 设置辅助变量

                 while ((length = fsin.read(buff, 0, 128))!= -1) { // 开始读取文件

                          System.out.println(new String(buff, 0, length)); //输出读取的内容

                 }

        }

 

        publicstaticvoid main(String[] args) throws Exception { //主方法

                 Tooltool = new ToolRunnerTest(); // 创建实现了 Tool 接口的实现类

                 ToolRunner.run(tool, args); // 运行驱动

                 print(tool); // 打印结果

        }

}

 

获取计数器值

除了使用 Web 方式查看计数器的取值, Hadoop 框架还支持使用 API 对计数器进行获取。一般情况下我们查看计数的值必须要等待任务完成,对于正在运行的任务无能为力。而使用API 对计数器进行查看可以运行在任务过程中,便于我们更好的了解任务的运行情况。

 

publicclassGetCounterInRunningTime extends Configured implementsTool {

        publicstaticvoid main(String[] args) throws Exception { //主方法

                 ToolRunner.run(new GetCounterInRunningTime(), args); // 运行当前类

        }

 

        @Override

        publicint run(String[] args) throws Exception{

                 JobClientjobClient = new JobClient(newJobConf(getConf())); // 设置一个任务实例

                 RunningJobjob = jobClient.getJob("job_201305052027_0001"); // 获取已运行的任务

                 // 获取任务计数器

                 Counterscounter = job.getCounters();

                 // 获取计数器数值

                 longhelloWordCounter = counter.getCounter(TxtCounter3.ReportTest.GoodWord);

                 // 获取自定义的计数器值

                 Countercounter2 = counter.findCounter("GroupName", "helloWord");

 

                 System.out.println("计数是: " + helloWordCounter); //打印计数器值

                 System.out.println("自定义计数是: " + counter2.getCounter()); // 打印动态计数器值

                 return 0;

        }

}

 

classTxtCounter3 { // 创建计数类

        staticenum ReportTest { //设置枚举作为错误归类

                 ErrorWord, GoodWord, ReduceReport// 枚举值

        }

}

 

读者可以在 Hadoop 集群环境下运行如下命令行代码:

$ hadoopjar GetCounterInRunningTime.jar GetCounterInRunningTime
结果如图所示:


 

在本例子中,首先以任务 ID 为输入参数调用一个 JobClient 实例的 getJob()方法,并返回一个 RunningJob 对象。然后检查是否有任务与其相对于。在获取任务结束后,调用该任务中的 getCounter 方法返回任务的计数器对象,计数器对象中包含这个任务所有计数器。

getCounterfindCounter 是两个常用的方法,用于返回内置的计数器的计数和自定义的动态计数器的计数。

 

排序与查找

对于大部分程序来说,排序工作是一种重要的任务,能够提供更好的查找与运行的算法,作者本人深有体会,写出一个好的排序算法是一件非常困难的事。但是幸运的是,大多数的排序并不需要我们来完成,而是 Hadoop 框架已经替我们完成,我们只需要提供最基本的排序规则即可。

 

使用 MapFile 进行排序与查找

下面我们看稍微复杂一点的例子,我们在前面已经说过,对于数据的存储,Hadoop还为我们提供了基于二进制的存储形式,即 SequenceFile 这种使用二进制对数据进行存储的数据结构存在,因此我们可以使用二进制数据结构来对数据进行存储并排序。

 

在这里我们并不对 SequenceFile 进行研究,而是研究其一种特殊结构 MapFile:

MapFile是一种基于键值对的用于查找的 SequenceFile,通过分析源代码可知, MapFile 在生成运算结果的时候一般会分别生成关联的索引与数据文件,索引文件中包含上文中所说的“偏移位置”,数据文件则包括需要存储的键值对,通过查找索引文件中记录的偏移位置,我们可以很方便的在数据文件找到关联数据,从而提高查找效率

 

在这里我们使用两种格式对 MapFile 进行查找。第一种请看程序:

 

publicclassSimpleSortTest extends Configured implementsTool {

        publicstaticvoid main(String[] args) throws Exception {

                 Tooltool = new SimpleSortTest(); // 设置运行类

                 ToolRunner.run(tool, args); //运行主程序

                 mapPrint(tool); // 打印结果

        }

 

        @Override

        publicint run(String[] args) throws Exception{

                 JobConfconf = new JobConf(getConf()); // 设置环境

                 FileSystemfs =FileSystem.get(getConf()); // 取得文件系统

                 fs.delete(new Path("oldApi"), true); // 删除已有目录

                 FileInputFormat.addInputPath(conf, new Path("sample.txt")); // 设置输入数据

                 FileOutputFormat.setOutputPath(conf, new Path("oldApi")); // 设置输出路径

                 conf.setOutputFormat(MapFileOutputFormat.class); // 设置输出格式

                 JobClient.runJob(conf); // 运行主代码

                 return 0;

        }

 

        publicstaticvoid mapPrint(Tool tool) throws Exception {

                 FileSystemfs =FileSystem.get(tool.getConf()); //获得当前文件系统

                 MapFile.Readerreader = new MapFile.Reader(fs, "oldApi/part-00000/", tool.getConf());

                 // 使用MapFile 格式进行文件读取操作

                 LongWritablekey = new LongWritable(); // 设置一个 key 值做辅助

                 Textvalue = new Text(); // 设置一个 Value 值做辅助

                 while (reader.next(key, value)) { // 打印全部内容

                          System.out.println(key + " " + value); //打印全部内容

                 }

                 System.out.println("--------------------------------------"); //分隔符

                 reader.reset(); // 重置起始点

                 reader.getClosest(new LongWritable(52), value); // 寻找某一个范围最近的值

                 System.out.println(value); // 打印取得值

                 System.out.println("--------------------------------------"); //分隔符

                 reader.reset(); // 重置起始点

                 reader.get(new LongWritable(70), value); // 精确寻找某一个值

                 System.out.println(value); // 打印输出

        }

}

 

从上述代码我们可以看到,我们在 setOutputFormat 方法中设置了 MapFileOutputFormat 类作为文件的输出格式。前面我们已经说明, MapFileOutputFormat 将文件输出为二进制的文件格式,并且包含两个文件 data 与 index。如图所示:


 

其中 index 文件包含一部分键和 data 文件中键到该键偏移量的映射。而 data 则包含键值对的基本信息。从图中我们可以看到运行结果:

 


对于此运行结果我们可以看到,在查找的部分,首先建立了 MapFile.Reader 实例,然后调用next()方法,直到返回值为 false。在这个过程中, reader 中读取的数据被依次注入到自定义的键值对中,因此我们可以看到完整的输入格式。

 

我们在第一次查找时使用了 getClosest 方法,此方法在读取过程中,首先将 index 文件读入内存,然后对读入的数据进行二分法查找。最后找到等于或者小于所需搜索的键,根据其对应的键去 data 文件中继续寻找键对应的值可以将最接近的部分进行读出。而 get 方法则是精确读取相对饮的值,若值不存在则返回 null。

 

除了刚才使用 get 与 getgetClosest 方法对数据进行读取意外,还有另外一种读取方式:

 

publicclassDefaultSortTest extends Configured implementsTool {

        publicstaticvoid main(String[] args) throws Exception {

                 Tooltool = new SimpleSortTest(); // 设置运行类

                 ToolRunner.run(tool, args); //运行主程序

                 mapPrint(tool); // 打印结果

        }

 

        @Override

        publicint run(String[] args) throws Exception{

                 JobConfconf = new JobConf(getConf()); // 设置环境

                 FileSystemfs =FileSystem.get(getConf()); // 取得文件系统

                 fs.delete(new Path("oldApi"), true); // 删除已有目录

                 FileInputFormat.addInputPath(conf, new Path("sample.txt")); // 设置输入数据

                 FileOutputFormat.setOutputPath(conf, new Path("oldApi")); // 设置输出路径

                 conf.setOutputFormat(MapFileOutputFormat.class); // 设置输出格式

                 JobClient.runJob(conf); // 运行主代码

                 return 0;

        }

 

        publicstaticvoid mapPrint(Tool tool) throws Exception {

                 FileSystemfs =FileSystem.get(tool.getConf()); //获得文件系统

                 LongWritablekey = new LongWritable(55); // 确立查找的索引

                 Textvalue = new Text(); // 设置值辅助

                 Reader[]readers = MapFileOutputFormat.getReaders(fs, new Path("oldApi"), tool.getConf()); // 获取输入分片

                 Writableresult = MapFileOutputFormat.getEntry(readers, new HashPartitioner<LongWritable, Text>(),key, value); //读取输入结果

                 System.out.println(result); // 打印结果

        }

}

从中我们可以看到, getReaders 方法为 MapReduce 任务创建的每个输入文件分别打开一个 MapFile.Reader 实例,用一个 Reader 数组表示,之后的 getEntry 方法使用 HashPartitioner找到对应那个键的 Reader 实例,之后通过 Reader 实例的 get 方法将其值返回。我们使用 Writable result 接受那个返回值,如果没有找到任何值,则返回一个 null。

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值