大数据技术 - 通俗理解MapReduce之WordCount(二)

上一章我们搭建了分布式的 Hadoop 集群。本章我们介绍 Hadoop 框架中的一个核心模块 - MapReduce。MapReduce 是并行计算模块,顾名思义,它包含两个主要的阶段,map 阶段和 reduce 阶段。每个阶段输入和输出都是键值对。map 阶段主要是对输入的原始数据做处理,按照 key-value 形式输出数据,输出的数据按照key是有序的。reduce 阶段的输入是 map 任务的输出,会对输入的数据会按照 key 做归并排序,使得输入 reduce 任务输入的 key 也是有序的,reduce 阶段进行完业务处理之后将把数据输出到HDFS中。下面以具体的例子说明 MapReduce 的机制。

本章以 WordCount 为例子讲解 MapReduce 机制,这个例子相当于学习编程语言的 "Hello, World"。假设我们在 HDFS 上有一个 10T 的文件,文件每一行有多个单词,单词之间空格分割,现在我们想统计一下这个文件中每个单词出现的次数,这就是 word count。我们的例子将在上一章搭建的 Hadoop 集群上进行。首先准备数据源,我们实际的例子中的数据量比较少,本地(hadoop0 机器)文件如下:

word1文件:
hello world hadoop
hadoop spark

word2文件:
hadoop hbase
mapreduce hdfs

需要将这两个文件上传至 HDFS,命令如下:

hadoop fs -mkdir -p /hadoop-ex/wordcount/input        #mkdir:创建目录 -p:递归创建多级目录
hadoop fs -put word1 word2 /hadoop-ex/wordcount/input #上本地文件上传至HDFS目录

有了数据源,我们开始写 MapReduce 程序,我用的编辑器是 Intellj IDEA,创建一个 maven 项目,选择 archetype 为 maven-archetype-quickstart。项目创建完成后引入 Hadoop 依赖

<dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>hadoop-client</artifactId>
      <version>2.9.2</version>
</dependency>

先创建在 map 阶段运行的类,也叫做 Mapper:

package com.cnblogs.duma.mapreduce;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

/**
 * WordCountMapper 继承 Mapper 类,需要指定4个泛型类型,分别是
 * 输入 key 类型:本例中输入的 key 为每行文本的行号,例子中用不到所以这里是 Object
 * 输入 value 类型:本例中输入的 value 是每行文本,因此是Text
 * 输出 key 类型:map 输出的是每个单词,类型为 Text
 * 输出 value 类型:单词出现的次数,为 1,因此类型 IntWritable
 */
public class WordCountMapper
        extends Mapper<Object, Text, Text, IntWritable> {
    /**
     * 把每个单词映射成 <word, 1> 的格式
     */
    private final static IntWritable one = new IntWritable(1);
    private Text outWord = new Text();

    /**
     * 每个 map 函数处理一行数据
     * @param key    输入的行号
     * @param value  每一行文本
     * @param context
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    protected void map(Object key, Text value, Context context) throws IOException, InterruptedException {
        String[] words = value.toString().split(" "); //空格分割一行中的每个单词
        for (String word : words) {
            outWord.set(word);
            System.out.println("<" + outWord + ", " + one + ">"); //打印
            context.write(outWord, one); // map输出
        }
    }
}

这个就是 map 阶段要执行的逻辑,代码本身比较简单。首先说明几个问题。第一,map 阶段会对输入的文件做分割,分割的大小可以通过参数指定,默认按照 HDFS 存储的块大小分割。假设 10TB 的文件,HDFS 块为128MB,那么大概会有 81920 个分块, 每个 map 任务处理一个分块。也就是会为每个分块创建 WordCountMapper 对象,遍历数据块的每一行,并调用 WordCountMapper 类中的 map 函数处理。因此,当前分片的数据有多少行,就会调用多少次 map 函数。我们的例子中有两个文件,每个文件都小于 128MB ,因此会启动2个 map 任务。由于 HDFS 中的数据存储在不同的机器上,因此 map 任务会在尽可能在存储数据块的机器上启动。 这样每个 map 任务可以处理本地的数据,如果有数据块的节点上资源比较紧张无法分配新的 map 任务,只能在其他机器启动 map 任务,将数据下载到该机器,这种情况将产生网络的消耗。第二,刚才提到过,map 函数会执行多次, 一些变量可以定义成类变量,防止创建过多的对象,浪费内存。该例子中,变量 one、outWord 被定义成类变量。第三,Hadoop 为了网络传输更优的序列化与反序列化,重新定义了数据类型,Text 对应java中的 String,IntWritable 对应 java 中的 int。第四,map 任务输出的数据放在本地磁盘上,等待 reduce 任务拉取。

map 函数执行完成后,输出的结果如下

map 任务1:
<hadoop, 1>
<hadoop, 1>
<hello, 1>
<spark, 1>
<world, 1>


map 任务2:
<hadoop, 1>
<hbase, 1>
<hdfs, 1>
<mapreduce, 1>

首先可以看到输出是有序的。下面再看下 reduce 任务

package com.cnblogs.duma.mapreduce;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;
/**
 * WordCountReducer 继承 Reducer 类,需要指定4个泛型类型,分别是
 * 输入 key 类型:map 任务输出的 key 类型, Text
 * 输入 value 类型:map 任务输出的 value 类型,IntWritable
 * 输出 key 类型:reduce 输出的是每个单词,类型为 Text
 * 输出 value 类型:单词出现的次数,因此类型 IntWritable
 */
public class WordCountReducer
        extends Reducer<Text, IntWritable, Text, IntWritable> {
    private Log logger = LogFactory.getLog(WordCountMapper.class);
    private IntWritable result = new IntWritable();

    /**
     * 一次 reduce 函数的调用,会处理一个 key
     * @param key  
     * @param values  相同 key 对应的 values 的集合
     * @param context
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int sum = 0;
        for (IntWritable val : values) { // 同一个单词出现次数相加,即为该单词的个数
            sum += val.get();
        }
        result.set(sum);
        System.out.println("<" + key + ", " + result + ">");
        context.write(key, result); // 输出
    }
}

reduce 任务的输入便是 map 任务的输出,这里也说明几个问题。第一,reduce 的个数需要通过参数或者代码指定, 默认为1。第二,map 任务输出的 key 去到哪个 reduce 任务,默认是 key 的 hash 值取模。第三,reduce 输入的key 有多个且经过排序,每个 key 对应的 value 组成一个 list,如 reduce 函数输入参数所示。输入多少 key 便调用多少次 reduce 函数。在这个例子中,reduce 任务个数为1。第四,reduce 任务的输出是输出到 HDFS 中。本例中输入数据如下

<hadoop, [1, 1, 1]>
<hbase, [1]>
<hdfs, [1]>
<hello, [1]>
<mapreduce, [1]>
<spark, [1]>
<world, [1]>

可以看到,reduce 任务的输入是有序的。reduce 任务处理完成后,输出如下

<hadoop, 3>
<hbase, 1>
<hdfs, 1>
<hello, 1>
<mapreduce, 1>
<spark, 1>
<world, 1>

现在需要一个驱动程序把他们串起来,代码如下

package com.cnblogs.duma.mapreduce;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

public class WordCount {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf, "WordCount"); //第二参数为程序的名字
        job.setJarByClass(WordCount.class); //需要设置类名

        job.setMapperClass(WordCountMapper.class); //设置 map 任务的类
//        job.setCombinerClass(WordCountReducer.class);
        job.setReducerClass(WordCountReducer.class); // 设置 reduce 任务的类

        job.setOutputKeyClass(Text.class);  //设置输出的 key 类型
        job.setOutputValueClass(IntWritable.class); //设置输出的 value 类型

        FileInputFormat.addInputPath(job, new Path(args[0])); //增加输入文件
        FileOutputFormat.setOutputPath(job, new Path(args[1])); //设置输出目录

        System.exit(job.waitForCompletion(true) ? 0 : 1); 
    }
}

现在需要打包放到集群上运行这个例子,可以在项目的根目录执行 mvn package 命令, 也可以利用 IDEA maven 可视化工具直接打包。打包完成后在项目目录中会生成 target 目录,里面有打包好的 jar 文件,我们将它上传到 hadoop0 机器,执行以下命令运行任务

hadoop jar hadoop-ex-1.0-SNAPSHOT.jar com.cnblogs.duma.mapreduce.WordCount /hadoop-ex/wordcount/input /hadoop-ex/wordcount/output

运行日志如下:

19/03/03 04:16:17 INFO client.RMProxy: Connecting to ResourceManager at hadoop0/192.168.29.132:8032
19/03/03 04:16:18 WARN mapreduce.JobResourceUploader: Hadoop command-line option parsing not performed. Implement the Tool interface and execute your application with ToolRunner to remedy this.
19/03/03 04:16:18 INFO input.FileInputFormat: Total input files to process : 2
19/03/03 04:16:19 INFO mapreduce.JobSubmitter: number of splits:2
19/03/03 04:16:19 INFO Configuration.deprecation: yarn.resourcemanager.system-metrics-publisher.enabled is deprecated. Instead, use yarn.system-metrics-publisher.enabled
19/03/03 04:16:19 INFO mapreduce.JobSubmitter: Submitting tokens for job: job_1551593879638_0009
19/03/03 04:16:20 INFO impl.YarnClientImpl: Submitted application application_1551593879638_0009
19/03/03 04:16:20 INFO mapreduce.Job: The url to track the job: http://hadoop0:8088/proxy/application_1551593879638_0009/
19/03/03 04:16:20 INFO mapreduce.Job: Running job: job_1551593879638_0009
19/03/03 04:16:35 INFO mapreduce.Job: Job job_1551593879638_0009 running in uber mode : false
19/03/03 04:16:35 INFO mapreduce.Job:  map 0% reduce 0%
19/03/03 04:16:48 INFO mapreduce.Job:  map 100% reduce 0%
19/03/03 04:17:01 INFO mapreduce.Job:  map 100% reduce 100%
19/03/03 04:17:02 INFO mapreduce.Job: Job job_1551593879638_0009 completed successfully
19/03/03 04:17:02 INFO mapreduce.Job: Counters: 49
    File System Counters
        FILE: Number of bytes read=120
。。。

日志有几个问题需要说明,第一,可以看到第4行 number of splits:2,说明会启动2个 map 任务处理数据。第二,第8行 url :http://hadoop0:8088/proxy/application_1551593879638_0009/ ,可以访问它观察任务的状态、任务任性在那个节点、任务的日志等。

至此,简单的 MapReduce 入门已经介绍完毕,主要就是 map 任务和 reduce 任务两个主要的阶段。当然这两个阶段之间有一个更重要的过程叫做 shuffle,很多任务的优化都需要调这个过程的参数,shuffle 过程的详细介绍我们在之后会讨论。在进行这个例子的时候可能会出一些错,常见的错误我在这里先记录一下。第一,启动任务时候报一下错误

The auxService:mapreduce_shuffle does not exist

解决方法,在 yarn-site.xml 文件中增加参数

<property>
    <name>yarn.nodemanager.aux-services</name>
    <value>mapreduce_shuffle</value>
</property>

第二,在 web 页面查看任务 logs 的时候,可能会报一下错误

Aggregation is not enabled

解决方法,在 yarn-site.xml 文件中增加参数

<property>
    <name>yarn.log-aggregation-enable</name>
    <value>true</value>
</property>

总结

这篇文章主要介绍 MapReduce 的机制,用来入门。MapReduce 主要分两个阶段,map 阶段会对输入的文件分割,分割数决定启动多少 map 任务,map 任务中进行数据处理,并按照<key, value>的格式输出, map 任务的输出数据临时存放在本地磁盘, 经过 shuffle 过程后, 启动 reduce 任务, reduce 任务个数可以手动指定,reduce 任务输入的 key 有序且同一个 key 的 value 会聚合在一起,最终 reduce 任务结果输出到 HDFS。

 

posted @ 2019-03-03 18:04  渡码  阅读(529)  评论(0编辑  收藏  举报