Hadoop学习笔记:MapReduce 编程进阶

数据类型

Hadoop 使用派生于 Writable 接口的类作为 Mapreduce 计算的数据类型。

value 数据类型,必须实现 org.apache.hadoop.io.Writable 接口,此接口确定了如何进行序列化与反序列化。
key 数据类型必须实现 org.apache.hadoop.io.WritableComparable 接口,定义了键的相互比较。

WritableComparable 继承与 Writable ,并增加了 compareTo 方法。

Hadoop 提供的数据类型

  1. 既可作为 key 又可作为 value
    • IntWritable
    • LongWritable
    • BooleanWritable
    • FloatWritable
    • ByteWritable
    • Text:存储 UTF8 文本
    • BytesWritable:一个字节序列
    • VIntWritable VLongWritable:变长
    • NullWritable
  2. 只可作为 value
    • ArrayWritable:数组
    • TwoDArrayWritable:矩阵
    • MapWritable:键值对
    • SortedMapWritable:有序键值对

自定义 Writable 数据类型

  1. 实现 Writable 接口
  2. 实现 readFields、Write 方法
public class LogWritable implements Writable {
    private Text userIP;
    private IntWritable status;
    
    public LogWritable() {
        this.userIP = new Text();
        this.status = new IntWritable();
    }
    
    public void readFields(DataInput in) throws IOException {
        userIP.readFields(in);
        status.readFields(in);
    }
    
    public void write(DataOutput out) throws IOException {
        userIP.write(out);
        status.write(out);
    }
}
int res = in.readInt();
String userIP = in.readUTF():

工作原理

  1. readFields:反序列化输入数据,并填充 Writable 对象的字段。
  2. write:在底层流中写入 Writable 对象的字段。

注意

  1. 如添加自定义的构造函数用于 Writable 类,需要有空的构造函数。
  2. TextOutputFormat 使用 toString() 方法序列化 key 和 value 类型,如使用它,要保证有有意义的 toString 实现。
  3. 在读取输入数据时,hadoop 可以多次重复使用 Writable 类的一个实例,在 readFields 方法里填充字段时,不应该依赖于该对象的现有状态。

自定义 key 类型

  1. 实现 WritableComparable 接口
  2. 实现 readFields、write、compareTo、hashCode 方法
public class LogWritable implements Writable {
    private Text userIP;
    private IntWritable status;
    
    public LogWritable() {
        this.userIP = new Text();
        this.status = new IntWritable();
    }
    
    public void readFields(DataInput in) throws IOException {
        userIP.readFields(in);
        status.readFields(in);
    }
    
    public void write(DataOutput out) throws IOException {
        userIP.write(out);
        status.write(out);
    }
    
    public int compareTo(LogWritable o) {
        return userIP.compareTo(o.userIP)
    }
    
    public int hashCode() {
        return userIP.hashCode();
    }
}

注意

hadoop 使用 HashPartitioner 作为默认的 Partitioner 实现,来计算中间数据在reducer 中的分布。HashPartitioner 需要键对象的 hashcode 方法来满足以下两个属性。

  1. 在不同的 JVM 实例提供相同的哈希值。
  2. 提供哈希值的均匀分布。

从 mapper 中输出不同值类型的数据

在执行 reducer 端 join 操作时,或者我们在多个 MapReduce 计算中将不同属性类型的数据聚合成一个数据集合时需要避免复杂性时,在 mapper 中输出属于多个值类型的数据集合,是非常有用的。但 reduce 不允许多个输入值类型,此时需要使用 GenericWritable 类来包装属于不同数据类型的多个 value 实例。

public class MultiValueWritable extends GenericWritable {
    private static Class[] CLASSES = new Class[] {
        IntWritable.class,
        Text.class
    };
    
    public MultiValueWritable() {}
    
    public MultiValueWirtable(Writable value) {
        set(value);
    }
    
    protected Class[] getTypes() {
        return CLASSES;
    }
}

// map
context.wirte(new Text(), new MultiValueWritable(new Text()));

// reduce
Writable wirtable = multiValueWritable.get();
if (wirtable instanceof Text) { ... }
else { ... }

为输入数据格式选择合适的 InputFormat

Hadoop 通过 InputFormat 来支持许多不同格式和类型的数据处理。InputFormat 通过解析输入数据来生成用于 mapper 的键值对输入。

InputFormat 还执行将输入数据分割成逻辑分区,基本上决定了 MapReduce 计算的 Map 任务数,并简介决定了 Map 任务的执行位置。

实例

指定 KeyValueTextInputFormat 作为 InputFormat

Configuration conf = new Configuration();
Job job = new Job(conf, "log");
...
SetInputFormat(KeyValueTextInputFormat.class)

// 设置作业的输入路径
FileInputFormat.setInputPaths(job, new Path(inputPath));

工作原理

KeyValueTextInputFormat 是一种纯文本文件的输入格式,它为输入文本文件的每一行生成一个键值记录,输入数据每一行使用分隔符分成键(Text)、值(Text)对。

默认的分隔符是制表符。

如某行不包含分隔符,将被视为键、值(空)。

自定义:conf.set("key.value.separator.in.input.line", ",");

KeyValueTextInputFormat 基于 FileInputFormat,FileInputFormat 是一种基于文件的 InputFormat 的基类。需要使用 FileInputFormat 的 setInputPaths 或 addInputPath 方法指定输入路径。

Hadoop 提供的 InputFormat 实现

  1. TextInputFormat:用于纯文本文件,键(LongWritable)是文件中的字节偏移量,值(Text)是行的文本。是默认的 InputFormat。
  2. NLineInputFormat:用于纯文本文件,将输入文件转为固定数目行的逻辑切分,默认一行。LongWritable,Text。NlineInputFormat.setNumLinesPerSplit(job, 50)
  3. SequenceFileInputFormat:用于 Hadoop 顺序文件输入数据。Hadoop 顺序文件将数据存储为二进制键值对,并支持数据压缩。
    • SequenceFileAsBinaryInputFormat,BytesWritable,BytesWritable
    • SequenceFileAsTextInputFormat,Text,Text
  4. DBInputFormat:支持从 Sql 表中读入数据,记录号作为键(LongWritable),查询结果作为值(DBWritable)

一个 Mapreduce 中使用多个输入数据类型和多个 mapper 实现

可以使用 MultipleInputs 功能来运行具有多个输入路径的 MapReduce 作业,同时指定用于每个路径的不同 InputFormat 和 mapper(可选)。

Hadoop 将输入路由到不同 mapper 实例,使用单一的 reducer 实例执行 MapReduce 计算输出。

// 实例
MultipleInputs.addInputPath(job, path, InputFormat.class);

// 源码
public static void addInputPath(
    JobConf conf, 
    Path path, 
    Class<?extends InputFormat> inputFormatClass, 
    Class<?extends Mapper> mapperClass)

实现自定义的 InputFormat

  1. 继承 org.apache.hadoop.mapreduce.InputFormat 抽象类
  2. 重写 createRecordReader() 和 getSplits() 方法
// 实现 LogFileInputFormat
public class LogFileInputFormat extends FileInputFormat<LongWritable, LongWritable> {
    public RecordReader<LongWritable, LogWritable> createRecordReader(InputSplit arg0, TaskAttemptContext arg1) throw ... {
        return new LogFileRecordReader();[
    }
}

// 实现 LogFileRecordReader 类
public class LogFileRecordReader extends RecordReader<LongWritable, LogWritable> {
    LineRecordReader lineReader;
    LogWritable value;
    
    public void initialize(InputSplitinputSplit, TaskAttemptContext attempt)...{
        lineReader = new LineRecordReader();
        lineReader.initialize(inputSplit, attempt);
    }
    
    public boolean nextKeyValue() throws ... {
        if (!lineReader.nextKeyValue()) 
            return false;
        String line = lineReader.getCurrentValue().toString();
        ... //Extract the fields from 'line' using a regex
        
        value = new LogWritable(userIP, status);
        return true;
    }
    
    public LongWritable getCurrentKey() throw .. {
        return lineReader.getCurrentKey();
    }
    
    public LogWritable getCurrentKey() throw .. {
        return value;
    }
    
    public float getProgress throws .. {
        return lineReader.getProgress();
    }
    
    public void close() throw .. {
        lineReader.close();
    }
}

// 指定 LogFileInputFormat 作为 InputFormat
Configuration conf = new Configuration();
Job job = new Job(Conf, "log");
...
job.setInputFormatClass(LogFileInputFormat.class);
FileInputFormat.setInputPaths(job, new Path(inputPath))

// mapper
public void map(LongWritable key, LogWritable value, Context context) throws ... {}

使用 OutputFormat

与 InputFormat 类似

Hadoop 默认使用 TextOutputFormat,每个单独的行保存一条记录,使用制表符分隔记录的键值。

TextOutputFormat 扩展 FileOutputFormat。

job.setOutputFormat()
FileOutputFormat.setOutputPath(job, new Path())

Hadoop 的中间(map 到 reduce)数据分区

Hadoop 在整个 reduce 任务的计算过程中,对 Map 任务生成的中间数据进行分区,一个适当的分区函数能够确保每个 Reduce 任务负载平衡。分区也可以用于将相关的记录集分组,发送到特定的 reduce 任务。

基于中间数据的键空间划分中间数据,分区的总数等于 MapReduce 计算中 reduce 任务数。

Hadoop Partitioner 应扩展 org.apache.hadoop.mapreduce.Partitioner 抽象类。使用 org.apache.hadoop.mapreduce.lib.partition.HashPartitioner 作为默认的。
HashPartitioner 使用 hashCode 划分键,使用公式key.hashcode() mod r,r 是 reduce 任务数量。

public class IPBasePartitioner extends Partitioner<Text, IntWritable> {
    public int getpartition(Text ipAddress, IntWritable value, int numPartitions) {
        String region = getGeoLocation(ipAddress);
        if(region != null) {
            return ((region.hashCode() & Integer.MAX_VALUE) % numPartitions);
        }
        return 0;
    }
}

Job job = new Job(conf, "log");
job.setPartitionerClass(IPBasePartitioner.class);

hadoop 提供的 Partitioner

  1. TotalOrderPartitioner:reducer 的输入记录集是有序的,以确保输入分区中有正确排序。
  2. KeyFieldBasedPartitioner:可以用来换分基于部分键的中间数据。

添加 MapReduce 作业之间的依赖关系

许多时候,需要多个 MapReduce 应用程序以工作流般的方式执行,以达到我们的目的,Hadoop 的 ControlledJob 和 JobControl 类提供了一种机制,即通过指定两个 MapReduce 作业之间的依赖关系来执行 MapReduce 作业的简单工作流图。

// 为第一个 MapReduce 作业创建 Configuration 和 Job 对象
Job job1 = new Job(getConf(), "log1");
job1.setJarByClass(Mapper1.class);
job1.setMapperClass(Mapper1.class);
FileInputFormat.setInputPaths(job1, new Path(inputpath));
FileOutputFormat.setOutputPath(job1, new Path(intermedPath));
...

// 为第二个 MapReduce 作业创建 Configuration 和 Job 对象
Job job2 = new Job(getConf(), "log2");
job2.setJarByClass(Mapper2.class);
job2.setMapperClass(Mapper2.class);
job2.setReduceClass(Reduce2.class);
FileOutputFormat.setOutputpath(job2, new path(outputPath))

// 设置第一个作业的输出目录,并将该目录作为第二个作业的输入目录
FileInputFormat.setInputPath(job2, new Path(intermedPath + "/part*"));

// 使用创建的 Job 对象来创建 ControlledJob 对象
ControlledJob controlledJob1 = new ControlledJob(job1.getConfiguration());
ControlledJob controlledJob2 = new ControlledJob(job2.getConfiguration());

// 将第一个作业添加为第二个作业的依赖
controlledJob2.addDependingJob(controlledJob1);

// 为作业组创建 JobControl 对象,并将 ControlledJob 对象添加到新创建的 JobControl 对象中
JobControl jobControl = new JobControl("JobControlDemoGroup");
jobControl.addJob(controlledJob1);
jobControl.addJob(controlledJob2);

// 创建新的线程来运行添加到 JobControl 对象的作业组。
Thread jobControlThread = new Thread(jobControl);
jobControlThread.start();
while (!jobControl.allFinished()) {
    Thread.sleep(500);
}
jobControl.stop();

用于报告自定义指标的 Hadoop 计数器

Hadoop 使用一组计数器来聚合 MapReduce 计算的指标,Hadoop 计数器有助于理解我们 MapReduce 程序的行为,并跟踪 MapReduce 计算的进度。

可以定义自定义计数器来跟踪 MapReduce 计算中的应用程序特定指标。

定义计数器,在日志处理的应用程序中统计不良或损坏的记录数

// 定义一个枚举自定义计数器列表
public static num LOG_PROCESSOR_COUNTER {
    BAD_RECORDS
};

// 在 mapper 或者 reducer 中增加计数器值
context.getCounter(LOG_PROCESSOR_COUNTER.BAD_RECORDS).increment(1);

// 将以下内容添加到主程序来访问计数器
Job job = new Job(conf, "log");
...
Counters counters = job.getCounters();
Counter badRecordsCounter = counters.findCounter(LOG_PROCESSOR_COUNTER.BAD_RECORDS);
System.out.pringln("# of Bad Records:" + badRecordsCounter.getValue());

// 执行Hadoop MapReduce 计算,可以在控制台查看计数器的值

你可能感兴趣的:(Hadoop)