Hadoop 自定义组件

在一些特殊情况下,我们会自定义一些MapReduce中的组件来满足自己的需求,比如自定义的Partition就是很好的例子。

1.1  自定义InputFormat

在Hadoop系统中自带了一些常用的InputFormat,我们可直接使用,如下:

  1. FileInputFormat<K,V>这个是基本的文件输入父类。
  2. TextInputFormat<LongWritable,Text>这个是默认的数据格式类,我们一般编程,如果没有特别指定的话,一般都使用的是这个;key代表当前行数据距离文件开始的偏移量,value为当前行字符串。
  3. SequenceFileInputFormat<K,V>这个是序列文件输入格式,使用序列文件可以提高效率,但是不利于查看结果,建议在过程中使用序列文件,最后展示可以使用可视化输出。
  4. KeyValueTextInputFormat<Text,Text>这个是读取以Tab(也即是\t)分隔的数据,每行数据如果以\t分隔,那么使用这个读入,就可以自动把\t前面的当做key,后面的当做value。
  5. CombineFileInputFormat<K,V>合并大量小数据是使用,比如mapreduce已经实现了一个org.apache.hadoop.mapreduce.lib.input.CombineTextInputFormat
  6. MultipleInputs 多种输入,可以为每个输入指定逻辑处理的Mapper。

在这儿,作者就不一一介绍了,有兴趣可以自己去尝试每个类型。下面我会介绍如何通过FileInputFormat来实现自定义的InputFormat。

实现FileInputFormat类,必须要实现createRecordReader方法。代码如下:

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import java.io.IOException;

public class CustomInputFormat extends FileInputFormat<Text, Text> {
    @Override
    public RecordReader<Text,Text> createRecordReader(InputSplit inputSplit,  TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
       // 自定义代码
        return new CustomReader();
    }
}

实现createRecordReader的关键就是返回一个自定义的RecordReader,如何实现自定义的RecordReader,请看下面的代码:

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import org.apache.hadoop.util.LineReader;
import java.io.IOException;

public  class CustomReader extends RecordReader<Text ,Text> {

    private LineReader lr ;

    private Text key = new Text();

    private Text value = new Text();

    private long start ;

    private long end;

    private long currentPos;

    private Text line = new Text();

    @Override

    public void initialize(InputSplit inputSplit, TaskAttemptContext taskAttemptContext)
            throws IOException {

        //输入分片的文件对象
        FileSplit split =(FileSplit) inputSplit;
        Configuration conf = taskAttemptContext.getConfiguration();
        Path path = split.getPath();
        FileSystem fs = path.getFileSystem(conf);
        FSDataInputStream is = fs.open(path);
        lr = new LineReader(is,conf);

        //分片,起始位置
        start =split.getStart();

        //分片,终点位置
        end = start + split.getLength();
        is.seek(start);
        if(start != 0){
            //如果不是开始的分片,默认跳过第一行
            start += lr.readLine(new Text(),0,
                    (int)Math.min(Integer.MAX_VALUE, end-start));
        }
        currentPos = start;
    }

    @Override
    public boolean nextKeyValue() throws IOException, InterruptedException {

        //currentPos=end,会继续读取下一行,currentPos > end则返回false

        if(currentPos > end){
            return false;
        }

        // 读取一行数据
        currentPos += lr.readLine(line);
        if(line.getLength()==0){
            return false;
        }

        if(line.toString().startsWith("ignore")){
            currentPos += lr.readLine(line);
        }

        // 转化为word数组
        String [] words = line.toString().split(",");

        // 异常处理
        if(words.length < 2){
            System.err.println("line:"+line.toString()+".");
            return false;
        }

        key.set(words[0]);
        value.set(words[1]);
        return true;
    }

    @Override
    public Text getCurrentKey() throws IOException, InterruptedException {
        return key;
    }

    @Override
    public Text getCurrentValue() throws IOException, InterruptedException {
        return value;
    }

    @Override
    public float getProgress() throws IOException, InterruptedException {
        if (start == end) {
            return 0.0f;
        } else {
            return Math.min(1.0f, (currentPos - start) / (float) (end - start));
        }
    }

    @Override
    public void close() throws IOException {
        lr.close();
    }
}

上面的代码就是实现一个自定义的RecordReader,核心的方法initialize,nextKeyValue。initialize用于实现一些初始化操作,nextKeyValue主要实现读取下一行记录的,转化成<key,value>形式。

这个自定义的CustomInputFormat 实现的功能就是:如果一行记录开始“ignore”则跳过这行。定义完成后就可以像其他系统自带的InputFormat一样正常使用。

1.2  Partitioner和Combiner

除了自定义InputFormat外,常见的自定义的组件还有partitioner和combiner,其中partitioner我们已经在4.1章节中使用过了,它的作用是自定义数据记录的分区的方式,而不用系统自带的分区方式。Hadoop自带的分区实现是HashPartitioner,其实现如下:

/** Partition keys by their {@link Object#hashCode()}. */
public class HashPartitioner<K, V> extends Partitioner<K, V> {

  /** Use {@link Object#hashCode()} to partition. */
  public int getPartition(K key, V value,int numReduceTasks) {
    return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
  }

}

实现原始是根据key的hashCode来和numReduceTasks取模得到分区的,所以分区依赖于具体的key。具体的自定义见4.1.3节的SalaryPartitioner.java实现这里不多举例子了。另外一个非常有用的特性就是Combiner,关于combiner有两点需要注意的。

第一,与mapper和reducer不同的是,combiner没有默认的实现,需要显式的设置在conf中才有作用。

第二,并不是所有的job都适用combiner,只有操作满足结合律的才可设置combiner。combine操作类似于:opt(opt(1, 2, 3), opt(4, 5, 6))。如果opt为求和、求最大值,求最小值的话,可以使用,但是如果是求中值的话,不适用。

combine其本质是reduce,只是执行的阶段不同。使用Combiner的原因,因为map输出的记录需要shuffle和跨机器网络传输,如果使用Combiner可以在Map端先对单个map输出的数据进行一个局部的聚合,可以减少网络传输的数据量,从而提高性能。

下面我们实现一个word count的combiner,主要就是对map端输出的word进行统计,代码如下:

public class WordcountCombiner extends Reducer<Text, IntWritable, Text, IntWritable>{
       @Override
       protected void reduce(Text key, Iterable<IntWritable> values, Context context)
throws IOException, InterruptedException {
              int count = 0;
              for(IntWritable v :values){ 
                   //对同一个map输出的k,v对进行按k进行一次汇总。不同map的k,v汇总必须要用reduce方法
                    count += v.get();
              }
              context.write(key, new IntWritable(count));
       }
}

在job定义中配置:

job.setCombinerClass(WordcountCombiner.class);

最终可以达到Map端聚合的效果,提高性能。

1.3 自定义输出RecordeWriter

我们有时也会对Reducer输出的自定义,比如需要把数据写入第三方的数据库,这个时候需要自定义MapReducer输出,这个功能也是经常使用的。

首先要定义一个OutputFormat需要实现三个方法:

RecordWriter  getRecordWriter(TaskAttemptContext context);

void  checkOutputSpecs(JobContext context);

OutputCommitter  getOutputCommitter(TaskAttemptContext context);

1.getRecordWriter

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

2. heckOutputSpecs

在JobClient提交Job之前被调用的(在使用InputFomat进行输入数据划分之前),用于检测Job的输出路径。比如,FileOutputFormat通过这个方法来确认在Job开始之前,Job的Output路径并不存在,然后该方法又会重新创建这个Output 路径。这样一来,就能确保Job结束后,Output路径下的东西就是且仅是该Job输出的。

3.getOutputCommitter

用于返回一个OutputCommitter的实例。OutputCommitter用于控制Job的输出环境,它有下面几个方法:

void  setupJob(JobContext jobContext);

void  commitJob(JobContext jobContext);

void  abortJob(JobContext jobContext, JobStatus.State state);

void  setupTask(TaskAttemptContext taskContext);

boolean  needsTaskCommit(TaskAttemptContext taskContext);

void  commitTask(TaskAttemptContext taskContext);

void  abortTask(TaskAttemptContext taskContext);

其中每个方法的功能如下:

  1. setupJob -   mkdir  ${mapred.output.dir}/_temporary
  2. commitJob - touch  ${mapred.output.dir}/_SUCCESS && rm  -r  ${mapred.output.dir}/_temporary
  3. abortJob -   rm  -r  ${mapred.output.dir}/_temporary
  4. setupTask - <nothing>
  5. needsTaskCommit - test -d ${mapred.output.dir}/_temporary/_${TaskAttemptID}
  6. commitTask - mv ${mapred.output.dir}/_temporary/_${TaskAttemptID}/* ${mapred.output.dir}/
  7. abortTask -   rm  -r  ${mapred.output.dir}/_temporary/_${TaskAttemptID}

自定义OutputFormat代码如下:

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.*;
import org.apache.hadoop.mapreduce.lib.output.FileOutputCommitter;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

public class DBOutputFormat extends OutputFormat<Text, Text> {

    @Override
    public RecordWriter<Text, Text> getRecordWriter(TaskAttemptContext taskAttemptContext)
            throws IOException, InterruptedException {
        return new SqlRecordWriter();
    }

    @Override
    public void checkOutputSpecs(JobContext jobContext)
            throws IOException, InterruptedException {
    }

    @Override
    public OutputCommitter getOutputCommitter(TaskAttemptContext context)
            throws IOException, InterruptedException {
        // 文件committer 
        return new FileOutputCommitter (FileOutputFormat.getOutputPath(context), context);
    }
}

SqlRecordWriter自定义实现,最主要的就是write()方法,在这个方法中,我们可以把自己的输出逻辑放到这个方法内,另外就是close()方法主要用于一些善后操作。具体的代码如下:

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;

import java.io.IOException;
import java.sql.SQLException;

public class SqlRecordWriter extends RecordWriter<Text, Text> {

    @Override
    public void write(Text key, Text value) throws IOException, InterruptedException {
        String data=key.toString();
        String  num=value.toString();
        Dao dao=new Dao();
        try {
            // 把结果输出到mysql中
            dao.insertData(data, num);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
        //关闭数据库连接
    }
}

job定义中设置自定义的OutputFormat

job.setOutputFormatClass(DBOutputFormat.class);

 

转载于:https://my.oschina.net/manmao/blog/1935470

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值