MapReduce
是一个分布式运算程序的编程框架。这个框架提供的是一套对HDFS里面文件进行分析的编程思路,即Map
和Reduce
两步。通过MapReduce提供的接口,我们可以方便地编写实现一个分布式计算任务,MapReduce自带的组件会将我们的代码组装成一个分布式计算程序提交给Yarn
进行处理。
(一)序列化定义
序列化是将内存中的对象,转换成字节序列(或其他数据传输协议)以便存储到磁盘(持久化)和网络传输。
(二)Hadoop序列化优点
编写的MR程序在传输数据时必然要将对象进行序列化和反序列化。然而,Java提供的序列化框架Serializable
是一个重量级框架,对象在序列化时会附带很多额外的信息(校验信息、Header、继承体等),序列化的对象需实现Serializable
接口并通过DataInputStream/DataOutputStream传输数据。
为此,Hadoop使用自己的序列化框架Writable
,提供了部分可序列化的基础类并支持自定义可序列化对象,常用的数据类型对应的Hadoop数据序列化类型如下:
Java类型 | Hadoop Writable类型 |
---|---|
Boolean | BooleanWritable |
Byte | ByteWritable |
Int | IntWritable |
Float | FloatWritable |
Long | LongWritable |
Double | DoubleWritable |
String | Text |
Map | MapWritable |
Array | ArrayWritable |
(三)自定义bean对象实现序列化接口(Writable)
具体实现bean对象序列化步骤如下:
(1)必须实现Writable接口
(2)反序列化时,需要反射调用空参构造函数,所以必须有空参构造
public FlowBean() {
super();
}
(3)重写序列化方法
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
(4)重写反序列化方法
@Override
public void readFields(DataInput in) throws IOException {
upFlow = in.readLong();
downFlow = in.readLong();
sumFlow = in.readLong();
}
(5)注意反序列化的顺序和序列化的顺序完全一致
(6)要想把结果显示在文件中,需要重写toString()
(TextOutputFormat特性决定),可用”\t”分开,方便后续用。
(7)如果需要将自定义的bean放在key中传输,则还需要实现Comparable接口,重写compareTo()方法,因为MapReduce框架中的Shuffle
过程要求对key必须能排序。
@Override
public int compareTo(FlowBean o) {
// 倒序排列,从大到小
return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
<dependencies>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>RELEASEversion>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-coreartifactId>
<version>2.8.2version>
dependency>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-commonartifactId>
<version>2.7.2version>
dependency>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-clientartifactId>
<version>2.7.2version>
dependency>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-hdfsartifactId>
<version>2.7.2version>
dependency>
dependencies>
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
public class WordcountMapper extends Mapper<LongWritable, Text, Text, IntWritable>{
Text k = new Text();
IntWritable v = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1 获取一行
String line = value.toString();
// 2 切割
String[] words = line.split(" ");
// 3 输出
for (String word : words) {
k.set(word);
context.write(k, v);
}
}
}
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
public class WordcountReducer extends Reducer<Text, IntWritable, Text, IntWritable>{
int sum;
IntWritable v = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values,Context context) throws IOException, InterruptedException {
// 1 累加求和
sum = 0;
for (IntWritable count : values) {
sum += count.get();
}
// 2 输出
v.set(sum);
context.write(key,v);
}
}
import java.io.IOException;
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;
public class WordcountDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// 1 获取配置信息以及封装任务
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 2 设置jar加载路径
job.setJarByClass(WordcountDriver.class);
// 3 设置map和reduce类
job.setMapperClass(WordcountMapper.class);
job.setReducerClass(WordcountReducer.class);
// 4 设置map输出
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 5 设置最终输出kv类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 6 设置输入和输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 7 提交
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
在Hadoop集群上运行MR程序,需将程序打包成jar包。
添加maven依赖(注意修改其中Driver类的全类名):
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-pluginartifactId>
<version>2.3.2version>
<configuration>
<source>1.8source>
<target>1.8target>
configuration>
plugin>
<plugin>
<artifactId>maven-assembly-plugin artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependenciesdescriptorRef>
descriptorRefs>
<archive>
<manifest>
<mainClass>com.bessen.mapreduce.WordCountDrivermainClass>
manifest>
archive>
configuration>
<executions>
<execution>
<id>make-assemblyid>
<phase>packagephase>
<goals>
<goal>singlegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
在Hadoop集群上运行WordCount程序:
$ hadoop jar MyWordCount.jar com.bessen.mapreduce.WordCountDriver /input /output
一个MapReduce程序就是一个job
,而Driver
类相当于Yarn的客户端,Driver将MR程序的各项配置和数据切片信息提交给Yarn,Yarn会创建一个MR appmaster,负责整个job的资源调度。一个job包括map
和reduce
两步,具体流程如下。
FileInputFormat
负责切片,为避免小文件问题导致切片过多,可以换成CombineTextInputFormat
MapTask
TextInputFormat
,按行的方式读取数据,该行在整个文件的起始偏移量为键,整行数据(不包括终止符)为值,也可以在Driver类中配置使用其他方式读取。OutputCollector
)将输出的键值对写入环形缓冲区Merge
)排序(毕竟之前每次只排序一小部分数据),归并其实就是继续完成之前没做完的分区和区内排序工作。Combiner
合并
12. 根据分区数决定ReduceTask
的个数
ReduceTask个数也可以自己设置,在Driver类中添加:
// 默认值是1,手动设置为3
job.setNumReduceTasks(3);
TextOutputFormat
)写出键值对。MapReduce框架的输入输出采用键值对的格式,这其中就涉及到了框架自带的InputFormat和OutputFormat,根据不同需求对key和value的选择,可以使用MR自带的几个实现类,也可以自定义InputFormat和OutputFormat.
// 设置切割符
configuration.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR,"\t");
// 设置输入格式
job.setInputFormatClass(KeyValueTextInputFormat.class);
// 设置3行为一个切片(InputSplit)
NLineInputFormat.setNumLinesPerSplit(job, 3);
// 设置输入格式
job.setInputFormatClass(NLineInputFormat.class);
// 7设置输入的inputFormat
job.setInputFormatClass(MyFileInputformat.class);
// 设置输出的outputFormat
job.setOutputFormatClass(SequenceFileOutputFormat.class);
MapReduce计算任务最终得到的是键值对类型的数据,在Reducer类里我们直接写一个write()方法进行输出。
context.write(key, value);
但这些数据最终是要输出到磁盘文件里面的,背后负责写出数据的就是OutputFormat。
TextOutputFormat
,会直接调用key和value的toString()方法,把输出的一条条数据写成文本行,格式如下:SequenceFileOutputFormat
,该类输出的格式紧凑,容易压缩,一般使用这个类时,输出作为下一个MapReduce任务的输入。RecordWriter
,重写其中的构造方法和write()、close()方法。FileOutputFormat
,重写getRecordWriter()方法@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
// 返回一个RecordWriter
return new MyRecordWriter(job);
}
// 要将自定义的输出格式组件设置到job中
job.setOutputFormatClass(MyOutputFormat.class);
在前面的MapReduce工作流程中,每个MapTask,在map之后的写入环形缓冲区、分区、区内排序、溢写磁盘、归并、(合并)、压缩的整个过程称为Shuffle(洗牌)。
分区数直接决定了ReduceTask个数,MapReduce默认使用HashPartitioner
进行分区,分区个数默认为1,通常我们需要自定义分区。
自定义分区步骤如下:
自定义一个类继承Partitioner,重写getPartition()方法。
public class MyPartitioner extends Partitioner<Text, TEXT> {
@Override
public int getPartition(Text key, Text value, int numPartitions) {
// 分区逻辑
// ...
return partition;
}
}
在Driver中设置:
job.setPartitionerClass(MyPartitioner.class);
//根据分区逻辑设置ReduceTask数量
job.setNumReduceTasks(3);
Map和Reduce阶段都要进行排序,如MapTask每次溢写磁盘前的区内排序、Map阶段最终的归并排序、ReduceTask从各MapTask节点获取输出后进行的归并排序等。
MapReduce默认对key
进行字典排序。为此,如果想要自定义排序,就需要让key对应的Bean对象具备排序功能,具体过程是:
让自定义的Bean对象实现WritableComparable
接口并重写compareTo
方法。
@Override
public int compareTo(FlowBean bean) {
int result;
// 按照sumFlow大小,倒序排列
if (sumFlow > bean.getSumFlow()) {
result = -1;
}else if (sumFlow < bean.getSumFlow()) {
result = 1;
}else {
result = 0;
}
return result;
}
ReduceTask需要收集所有MapTask节点对应分区的输出,进行汇总操作。如果Map阶段输出的数据量很大,必然会耗费大量的IO和网络传输资源。
如果任务在Map阶段进行提前汇总不会影响最终结果,则可以使用Combiner进行合并,事实上,Combiner组件的父类就是Reducer。
自定义Combiner的步骤:
public class WordcountCombiner extends Reducer<Text, IntWritable, Text,IntWritable>{
@Override
protected void reduce(Text key, Iterable<IntWritable> values,Context context) throws IOException, InterruptedException {
// 1 汇总操作
int count = 0;
for(IntWritable v :values){
count += v.get();
}
// 2 写出
context.write(key, new IntWritable(count));
}
}
job.setCombinerClass(WordcountCombiner.class);
Reduce阶段,相同key的数据会进入同一个reduce方法进行汇总,但在很多情况下,我们需要让相同组的数据进入一个reduce方法,同一组的数据key不一定相同,这时候就需要在reduce前添加分组排序。
分组排序的过程是按照key对输入Reduce阶段的数据进行分组,分组后,同一组的数据进入同一个reduce方法,设置如下:
protected OrderGroupingComparator() {
super(OrderBean.class, true);
}
@Override
public int compare(WritableComparable a, WritableComparable b) {
// 比较的业务逻辑,a、b同一组时返回值为0
// ...
return result;
}
// 设置reduce端的分组
job.setGroupingComparatorClass(MyComparator.class);
其实,Map Join和Reduce Join没有什么新的东西,不像排序、分组、合并这些有MR框架提供的接口或父类,前面的内容是整个MR框架的基础,Map Join和Reduce Join只是两种实现join的逻辑思路。
首先回顾join,如果表的设计过程合乎规范,需要join的两张表中,一张表的外键应该是另一张表的主键,如下图所示:
Reduce Join的思路是在Map阶段的setup()
初始化方法中先区分是哪张表,并在map方法里面打上标签。Map阶段的输出以外键B为key,这样就可以在Reduce阶段将两张表属性B相同的数据汇总到一起进行join操作了。
String name;
@Override
protected void setup(Context context) throws IOException, InterruptedException {
// 1 获取输入文件切片
FileSplit split = (FileSplit) context.getInputSplit();
// 2 获取输入文件名称
name = split.getPath().getName();
}
如果两张表中有一个是小表,那么可以直接让每一个MapTask都缓存一份小表到内存中,这样一来,在Map阶段就能直接完成join,也就不需要后面的Shuffle和Reduce过程了。
// 设置缓存数据路径,HDFS路径为hdfs://
job.addCacheFile(new URI("file:///缓存文件路径"));
// 设置reduceTask数量为0,取消reduce阶段
job.setNumReduceTasks(0);
Map<String, String> hashMap = new HashMap<>();
@Override
protected void setup(Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
// 1 获取第0个缓存的文件
URI[] cacheFiles = context.getCacheFiles();
String path = cacheFiles[0].getPath().toString();
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(path), "UTF-8"));
String line;
while(StringUtils.isNotEmpty(line = reader.readLine())){
// 2 切割
String[] fields = line.split("\t");
// 3 缓存数据到集合
hashMap.put(fields[0], fields[1]);
}
// 4 关流
reader.close();
}