Hadoop 使用派生于 Writable 接口的类作为 Mapreduce 计算的数据类型。
value 数据类型,必须实现 org.apache.hadoop.io.Writable 接口,此接口确定了如何进行序列化与反序列化。
key 数据类型必须实现 org.apache.hadoop.io.WritableComparable 接口,定义了键的相互比较。
WritableComparable 继承与 Writable ,并增加了 compareTo 方法。
Hadoop 提供的数据类型
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():
工作原理
注意
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 方法来满足以下两个属性。
在执行 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 { ... }
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 实现
NlineInputFormat.setNumLinesPerSplit(job, 50)
可以使用 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)
// 实现 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 ... {}
与 InputFormat 类似
Hadoop 默认使用 TextOutputFormat,每个单独的行保存一条记录,使用制表符分隔记录的键值。
TextOutputFormat 扩展 FileOutputFormat。
job.setOutputFormat()
FileOutputFormat.setOutputPath(job, new Path())
Hadoop 在整个 reduce 任务的计算过程中,对 Map 任务生成的中间数据进行分区,一个适当的分区函数能够确保每个 Reduce 任务负载平衡。分区也可以用于将相关的记录集分组,发送到特定的 reduce 任务。
基于中间数据的键空间划分中间数据,分区的总数等于 MapReduce 计算中 reduce 任务数。
Hadoop Partitioner 应扩展 org.apache.hadoop.mapreduce.Partitioner
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
许多时候,需要多个 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 使用一组计数器来聚合 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 计算,可以在控制台查看计数器的值