Hadoop—MapReduce

来源:互联网 发布:搞笑一家人知乎 编辑:程序博客网 时间:2024/06/05 21:58

Hadoop分为两个部分,一个部分是HDFS文件管理系统,另一个就是MapReduce,该部分是负责计算的部分,他可以提取出每一行的数据,并且再此基础上求和/排序等等业务逻辑,并且会归并数据,这一切只需要实现2个方法即可。

MapReduce共有2个进程:
1.ResourceManager:
A.知道管理哪些机器,即管理哪些NodeManager。
B.要有检测机制,能够检测到NodeManager的状态变换,通过RPC心跳来实现。
C.任务的分配和调度,ResourceManager能够做到细粒度的任务分配,比如某一个任务需要占用多大内存,需要多少计算资源。
注:ResourceManager是hadoop2.0版本之后引入了yarn,有yarn来管理hadoop之后,jobtracker就被替换成了ResourceManager

2.NodeManager:
能够收到ResourceManager发过来的任务,并进行任务的处理。这里处理任务指的是Map任务或Reduce任务。

运行过程:
首先从HDFS中获取需要分析的文件,该文件有多少个块,就启动多少个map(默认,可以通过配置修改),将文件内容解析成key,value对,然后写入自己的逻辑转换成新的key,value,每一行都会调用一次map函数。(只需要继承Mapper类,并且重写map方法。)该方法有4个泛型,前两个是读取文件的key/value类型,读取文件的key是偏移值(long类型的序列化类LongWritable),value是读取的文本内容(String类型,但是序列化框架用的是avro是Text类型),后两个是输出的key/value类型,该类型是传给reducer的。

例如:记录各个单词出现多少次

import java.io.IOException;import org.apache.hadoop.io.LongWritable;import org.apache.hadoop.io.NullWritable;import org.apache.hadoop.io.Text;import org.apache.hadoop.mapreduce.Mapper;public class MyMapper extends Mapper<LongWritable, Text, Text, LongWritable> {    public void map(LongWritable ikey, Text ivalue, Context context) throws IOException, InterruptedException {        //获取行内容        String line= ivalue.toString();        //通过空格切割        String datas[] = line.split(" ");        //每个出现的单词数量记1,并传给reducer        for(String data:datas){            context.write(new Text(data), new LongWritable (1));        }    }}

在输出的时候,每个MapperTask都有一个环形内存缓冲区,用于储存map任务的输出,默认大小100m,一单达到80%就会启动一个后台线程把内容写到磁盘的指定目录下新建一个文件将溢出的内容写入。写入之前会进行将key一致的value值以集合的格式保存进新的value,会根据key的字典顺序进行排序(可自定义排序,后面会说),以及Combiner(如有)即本地化的Reducer,将新value的集合内的值以我们自定义的逻辑方法合并。全部写完后,会将这些溢出的内容文件全部合并成一个新的文件。如果这些文件大于等于三个,那么合并完毕之后会再次进行一次Combiner。

数据从map到reduce中间都经历了什么呢?
首先处理这部分过程的是shuffle,这部分是整个hadoop的核心中的核心,首先,数据经过map方法后, 数据会先经过环形缓冲区,并写入Splill文件,写出时,会进行分区/排序/归并(如有),这里的分区并不是物理分区,而是逻辑分区,是根据你的不同reduce任务,来处理不同的数据而设计的,比如处理数据时,按照不同的月份进行输出整理,那么每个月都有一个reduce来处理,那么他拿数据的时候不会一条一条的找,而是环形缓冲区写出的时候进行逻辑分区,比如前200行是1月份/200-500行是2月份,那么reduce来拿数据的时候就会一目了然的全部拿走。当splill写完之后,这时可能会有多个splill文件,然后会整合成一个文件并进行逻辑分区/排序,如果splill超过3了个那么会再次归并。
然后reduce就会从这个整合过的文件拿数据,从不同的map拿到的数据,再reduce中会整合成一个文件,然后进行排序/及将Key相同的数据的value整合到一个集合中。之后执行reduce方法,输出数据。

这些输出的key /value会传给Reducer。我们要创建一个类,继承Reducer类,并且重写reduce方法,该类也有4个泛型,前2个是map传过来的类型,后两个是输出到文件的类型。
注意此时的value是归并的value的集合,比如说上面的例子Hello单词出现了3次那么传来的key:Hello,Value:{1,1,1}
这时如果我们想输出单词+总次数那么可以如下代码:

import java.io.IOException;import java.util.Iterator;import org.apache.hadoop.io.LongWritable;import org.apache.hadoop.io.Text;import org.apache.hadoop.mapreduce.Reducer;public class MyReducer extends Reducer<Text, LongWritable, Text, LongWritable> {    @Override    protected void reduce(Text key, Iterable<LongWritable> values,            Reducer<Text, LongWritable, Text, LongWritable>.Context context) throws IOException, InterruptedException {        long count = 0;        Iterator<LongWritable> iter = values.iterator();        while(iter.hasNext()){            count += iter.next().get();        }        //将数据写入文件 会输出hello 3        context.write(key, new LongWritable(count));    }}

都写完了,那么怎么启动呢?
这时候需要再写一个类:

import org.apache.hadoop.conf.Configuration;import org.apache.hadoop.fs.Path;import org.apache.hadoop.io.LongWritable;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;public class MyDriver {    public static void main(String[] args) throws Exception {        Configuration conf = new Configuration();        //创建job对象,指定conf和job名称        Job job = Job.getInstance(conf,"WCJob");        //指定job执行的类        job.setJarByClass(WCDriver.class);        //指定mapper类        job.setMapperClass(WCMapper.class);        //指定reducer类        job.setReducerClass(WCReducer.class);        //设置mapper输出的key和value的类型        job.setMapOutputKeyClass(Text.class);        job.setMapOutputValueClass(LongWritable.class);        //设置reducer输出的key和value的类型        job.setOutputKeyClass(Text.class);        job.setOutputValueClass(LongWritable.class);        //指定任务操作的资源的位置        FileInputFormat.setInputPaths(job,                new Path("hdfs://192.168.80.131:9000/park/word"));        //指定任务操作结束后生产的结果文件保存的位置        FileOutputFormat.setOutputPath(job, new Path("hdfs://192.168.80.131:9000/park/word/result"));        //执行任务        job.waitForCompletion(true);    }}

如果出现了空指针异常,下载Hadoop2以上版本时,在Hadoop2的bin目录下没有winutils.exe,网上找到这些资料,然后补充到里面就好了。
然后配置变量环境HADOOP_HOME 和path
在hadoop环境下将次jar包存入并执行 Hadoop jar word.jar就不会有这个问题。

上面key/value的类型是avro框架的一些Writable类,并不能满足我们全部的业务需要,怎么办呢?
可以自定义一个类,只要实现了Writable接口,并且实现write/readFields方法就行,此外要有一个无参的构造函数,否则会有一些奇怪的异常发生。
例如:

import java.io.DataInput;import java.io.DataOutput;import java.io.IOException;import org.apache.hadoop.io.Writable;public class FlowBean implements Writable {    //13877779999 bj zs 2145    private String phone;    private String addr;    private String name;    private long flow;    public FlowBean() {    }    public FlowBean(String phone, String addr, String name, long flow) {        this.phone = phone;        this.addr = addr;        this.name = name;        this.flow = flow;    }    //写入属性    @Override    public void write(DataOutput out) throws IOException {        out.writeUTF(phone);        out.writeUTF(addr);        out.writeUTF(name);        out.writeLong(flow);    }//将属性与avro属性对应,注意顺序也要对应。    @Override    public void readFields(DataInput in) throws IOException {        phone = in.readUTF();        addr = in.readUTF();        name = in.readUTF();        flow = in.readLong();    }    //....Get/set方法略。。}

自定义类

Writable类与Java类对应,就是第一个字母大写加Writable,
例如int 的对应IntWritable。此外 String类型对应Text,序列化对应的时候读取写入都是UTF。

自定义排序

如果想排序怎么办?实现WritableComparable接口,里面也有序列化方法需要实现,还需要实现compareTo方法,该方法是排序的逻辑。

分区

例如,在数据中不想全部都在一个文件,想上海的在一个结果文件中,北京的再一个结果文件中,怎么办?那么就启动多个reduce,把不同结果的分给不同的reduce..怎么做呢?
写一个类,继承Partitioner,写逻辑方法

例如:0 1 2 3分别代表reduceText

public class FlowPatitioner extends Partitioner<Text,FlowBean> {    private static Map<String,Integer> addrMap;    static{        addrMap =new HashMap<String,Integer>();        addrMap.put("bj", 0);        addrMap.put("sh", 1);        addrMap.put("sz", 2);    }    public int getPartition(Text key, FlowBean value, int numPartitions) {        String addr = value.getAddr();        Integer num = addrMap.get(addr);        if(num==null){            return 3;        }        return num;    }}

还需要修改启动类,

    //设置Partitioner类    job.setPartitionerClass(FlowPartitioner.class);    //指定Reducer的数量    job.setNumReduceTasks(4);

**Partitioner将会将数据发往不同reducer,这就要求reducer的数量应该大于等于Partitioner的数量,如果少于则在执行的过程中会报错。

Combiner – 合并

每一个MapperTask可能会产生大量的输出,combiner的作用就是在MapperTask端对输出先做一次合并,以减少传输到reducerTask的数据量。
combiner是实现在Mapper端进行key的归并,combiner具有类似本地的reducer功能。
同样继承Reducer类实现reduce方法。并在启动类中设置combiner类
job.setCombinerClass(自定义Combiner类.class);