MapReduce 是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。
MapReduce 核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个 Hadoop 集群上。
WordCount 样例中数据流走向深入理解 MapReduce 核心思想:
一个完整的M apReduce 程序在分布式运行时有三类实例进程:
MrAppMaster
:负责整个程序的过程调度及状态协调。MapTask
:负责 Map 阶段的整个数据处理流程。ReduceTask
:负责 Reduce 阶段的整个数据处理流程。Java类型 | Hadoop Writable类型 |
---|---|
Boolean | BooleanWritable |
Byte | ByteWritable |
Int | IntWritable |
Float | FloatWritable |
Long | LongWritable |
Double | DoubleWritable |
String |
Text |
Map | MapWritable |
Array | ArrayWritable |
Null | NullWritable |
用户编写的程序分成三个部分:Mapper
、Reducer
和 Driver
。以官方的 WordCount 为例
WordCount 官方案例源码地址,除了 WordCount 之外,还有其它的样例代码:
/**
* Mapper, 泛型表示两对kv,依次表示:输入kv,输出kv的类型,假设有如下文本文件:
* ab cd
* d ee ab
* 输入的k表示偏移量,第一行k=0,第二行k=6("ab cd\n"共6个字符)
* 输入的v表示一行数据,第一行是ab cd,第二行v=ab cd
* 输出的k表示分词后的字符串,ab/cd/d/ee,按照空格换行等符号进行分隔
* 输出的v表示相应的字符串的个数,这里是固定值1,
* 第一行输出:{ab: 1, cd: 1}
* 第二行输出:{d: 1, ee: 1, ab: 1}
*/
public static class TokenizerMapper
extends Mapper<Object, Text, Text, IntWritable> {
//防止多次创建
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();
@Override
public void map(Object key, Text value, Context context
) throws IOException, InterruptedException {
//将一行字符串分词,按照" \t\n\r\f" :空格字符、制表符、换行符、回车符和换页符分词
StringTokenizer itr = new StringTokenizer(value.toString());
while (itr.hasMoreTokens()) {
//将String转成Text
word.set(itr.nextToken());
context.write(word, one);
}
}
}
/**
* Reducer,泛型表示两对kv,依次表示:输入kv,输出kv的类型
* Mapper输出后的每个 key 只会在 Reducer 中处理一次
* 所以这里从Mapper中传过来的数为:
* {ab: [1,1], cd: [1], d: [1], ee: [1]}
* 合并后输出:{ab: 2 cd: 1, d: 1, ee: 1},实现统计单词逻辑
*/
public static class IntSumReducer
extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable result = new IntWritable();
@Override
public void reduce(Text key, Iterable<IntWritable> values,
Context context
) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
result.set(sum);
context.write(key, result);
}
}
注意:Hadoop 迭代器中使用了对象重用,即 Reducer 提供的 value 迭代器 Iterable
迭代时 value 始终指向一个内存地址(引用值始终不变,即同一个对象),改变的是引用指向的内存地址中的数据。所以如果要获取对象,则要新 new
一个对象,然后使用 BeanUtils.copyProperties(a, b)
将参数复制到新对象中
相当于 YARN 集群的客户端,用于提交我们整个程序到 YARN 集群,提交的是封装了 MapReduce 程序相关运行参数的 job 对象
/**
* Driver
*/
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
if (otherArgs.length < 2) {
System.err.println("Usage: wordcount [...] " );
System.exit(2);
}
//1. 获取Job
Job job = Job.getInstance(conf, "word count");
//2. 设置jar包路径
job.setJarByClass(WordCount.class);
//3. 关联Mapper和Reducer
job.setMapperClass(TokenizerMapper.class);
job.setCombinerClass(IntSumReducer.class);
job.setReducerClass(IntSumReducer.class);
//4. 设置Mapper输出的KV类型,如果和最终输出的KV类型一致,可以不设置
// job.setMapOutputKeyClass(Text.class);
// job.setMapOutputValueClass(IntWritable.class);
//5. 设置最终输出的KV类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//6. 设置输入路径
for (int i = 0; i < otherArgs.length - 1; ++i) {
FileInputFormat.addInputPath(job, new Path(otherArgs[i]));
}
//7. 设置输出路径
FileOutputFormat.setOutputPath(job,
new Path(otherArgs[otherArgs.length - 1]));
//8. 提交Job
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
pom 文件集成打包插件
<dependencies>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-clientartifactId>
<version>3.3.1version>
dependency>
dependencies>
<build>
<finalName>wcfinalName>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<configuration>
<source>1.8source>
<target>1.8target>
<encoding>UTF-8encoding>
configuration>
plugin>
<plugin>
<artifactId>maven-assembly-pluginartifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependenciesdescriptorRef>
descriptorRefs>
configuration>
<executions>
<execution>
<id>make-assemblyid>
<phase>packagephase>
<goals>
<goal>singlegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
将打成的 jar 包重命名为 wc.jar 上传到 hadoop 服务器,使用命令运行 jar 包:
# hadoop jar jar包路径 main方法类路径 输入目录 输出目录
hadoop jar wc.jar com.zyx.mapreduce.WordCount /root/input /output/wc
什么是序列化
序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁盘(持久化)和网络传输。
反序列化就是将收到字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换成内存中的对象。
为什么要序列化
一般来说,“活的”对象只生存在内存里,关机断电就没有了。而且“活的”对象只能由本地的进程使用,不能被发送到网络上的另外一台计算机。 然而序列化可以存储“活的”对象,可以将“活的”对象发送到远程计算机。
为什么不用Java的序列化
Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息,Header,继承体系等),不便于在网络中高效传输。所以,Hadoop自己开发了一套序列化机制(Writable)。
Hadoop序列化特点:
具体实现bean对象序列化步骤如下7步。
必须实现 Writable
接口
反序列化时,需要反射调用空参构造函数,所以必须有空参构造
public FlowBean() {
}
重写序列化方法
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
重写反序列化方法
@Override
public void readFields(DataInput in) throws IOException {
upFlow = in.readLong();
downFlow = in.readLong();
sumFlow = in.readLong();
}
注意:反序列化的顺序和序列化的顺序完全一致
要想把结果显示在文件中,需要重写 toString()
如果需要将自定义的 bean 放在 key 中传输,则还需要实现 Comparable
接口,因为 MapReduce 框中的 Shuffle 过程要求对 key 必须能排序。详见后面排序案例。
@Override
public int compareTo(FlowBean o) {
// 倒序排列,从大到小
return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
统计每一个手机号耗费的总上行流量、总下行流量、总流量
输入数据
1 13736230513 192.196.100.1 www.atguigu.com 2481 24681 200
2 13846544121 192.196.100.2 264 0 200
3 13956435636 192.196.100.3 132 1512 200
4 13966251146 192.168.100.1 240 0 404
5 18271575951 192.168.100.2 www.atguigu.com 1527 2106 200
6 84188413 192.168.100.3 www.atguigu.com 4116 1432 200
7 13590439668 192.168.100.4 1116 954 200
8 15910133277 192.168.100.5 www.hao123.com 3156 2936 200
9 13729199489 192.168.100.6 240 0 200
10 13630577991 192.168.100.7 www.shouhu.com 6960 690 200
11 15043685818 192.168.100.8 www.baidu.com 3659 3538 200
12 15959002129 192.168.100.9 www.atguigu.com 1938 180 500
13 13560439638 192.168.100.10 918 4938 200
14 13470253144 192.168.100.11 180 180 200
15 13682846555 192.168.100.12 www.qq.com 1938 2910 200
16 13992314666 192.168.100.13 www.gaga.com 3008 3720 200
17 13509468723 192.168.100.14 www.qinghua.com 7335 110349 404
18 18390173782 192.168.100.15 www.sogou.com 9531 2412 200
19 13975057813 192.168.100.16 www.baidu.com 11058 48243 200
20 13768778790 192.168.100.17 120 120 200
21 13568436656 192.168.100.18 www.alibaba.com 2481 24681 200
22 13568436656 192.168.100.19 1116 954 200
输入数据格式,数据之间使用制表符("\t
")进行分隔:
7 13560436666 120.196.100.99 1116 954 200
id 手机号码 网络ip 上行流量 下行流量 网络状态码
期望输出数据格式
13560436666 1116 954 2070
手机号码 上行流量 下行流量 总流量
import lombok.Data;
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.io.Writable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.Iterator;
public class Flow {
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration, "flow");
job.setJarByClass(Flow.class);
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
FileInputFormat.addInputPath(job, new Path("/Users/mac/IdeaProjects/bigdata/Hadoop/tmp/phone_data.txt"));
FileOutputFormat.setOutputPath(job, new Path("/Users/mac/IdeaProjects/bigdata/Hadoop/output/flow"));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
@Data
class FlowBean implements Writable {
private long upFlow;
private long downFlow;
private long sumFlow;
public void setSumFlow() {
this.sumFlow = this.upFlow + this.downFlow;
}
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
@Override
public void readFields(DataInput in) throws IOException {
upFlow = in.readLong();
downFlow = in.readLong();
sumFlow = in.readLong();
}
}
class FlowMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
private Text text = new Text();
private FlowBean flowBean = new FlowBean();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String[] split = value.toString().split("\t");
flowBean.setUpFlow(Long.parseLong(split[4]));
flowBean.setDownFlow(Long.parseLong(split[5]));
flowBean.setSumFlow();
text.set(split[1]);
context.write(text, flowBean);
}
}
class FlowReducer extends Reducer<Text, FlowBean, Text, FlowBean> {
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException {
FlowBean resBean = new FlowBean();
Iterator<FlowBean> iterator = values.iterator();
while (iterator.hasNext()) {
FlowBean flowBean = iterator.next();
resBean.setUpFlow(resBean.getUpFlow() + flowBean.getUpFlow());
resBean.setDownFlow(resBean.getDownFlow() + flowBean.getDownFlow());
resBean.setSumFlow();
}
context.write(key, resBean);
}
}
Read -> Map -> Collect -> Spill(溢写) -> Merge -> Copy -> Sory -> Reduce
1-5:Read 阶段:
6:Mapper:
7-13:Shuffle 过程
index
索引:唯一ID,partition
分区:同一个分区的数据最终会进入同一个 ReduceTask,最终同一个分区结果会保存在同一个文件中,几个分区生成几个结果文件, keystart
key开始的索引, valstart
v 开始的索引,0 - keystart 的数据就是 key 值,keystart 与 valstart 之间的数据就是 val 值)
溢写到磁盘文件,多次溢写后可能会生成多个文件14 - 15:Reducer:
16:OutputFormat
MapTask 的并行度决定 Map 阶段的任务处理并发度,进而影响到整个 Job 的处理速度。
并行度决定机制:
Job
的 Map
阶段并行度由客户端在提交 Job
时的切片数决定Split
切片分配一个 MapTask
并行实例处理,最好是切片和 MapTask
在同一个节点上切片大小=BlockSize
,如果 切片大小 != BlockSize
时,可能会出现文件与 MapTask
不在一台机器的情况简单地按照文件的内容长度进行切片
切片大小,默认等于 Block
大小,可以通过指定最大值和最小值来设置
切片最小值:
mapreduce.input.fileinputformat.split.minsize=1
,默认为1
切片最大值:mapreduce.input.fileinputformat.split.maxsize=Long.MAX_VALUE
,默认值为Long.MAX_VALUE
当minSize < blockSize < maxSize
时,切片大小=blockSize
每次切片时,都要判断切完剩下的部分是否大于块的 1.1
倍,不大于 1.1
倍就划分一块切片
切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
切片完成后,将切片信息写到一个切片规划文件中,整个切片的核心过程在 getSplit()
方法中完成,InputSplit
只记录了切片的元数据信息,比如起始位置、长度以及所在的节点列表等。
最后提交切片规划文件到 YARN
上,YARN 上的 MrAppMaster
就可以根据切片规划文件计算开启 MapTask
个数。
源码位置:
org.apache.hadoop.mapreduce.lib.input.FileInputFormat#getSplits(JobContext job)
输入数据有两个文件:
file1.txt 320M
file2.txt 10M
file3.txt 129M
经过 FileInputFormat 的切片机制运算后,形成的切片信息如下:
file1.txt.split1-- 0~128M
file1.txt.split2-- 128~256M
file1.txt.split3-- 256~320M
file2.txt.split1-- 0~10M
file3.txt.split1-- 0~129M
在运行 MapReduce 程序时,输入的文件格式包括:基于行的日志文件、二进制格式文件、数据库表等。因此,针对不同的输入数据类型,需要通过继承 FileInputFormat
父类来支持不同格式的输入文件
FileInputFormat
常见的实现类包括:TextInputFormat
(默认)、KeyValueTextInputFormat(处理KV)、NLineInputFormat(处理多行)、CombineTextInputFormat
(多个文件合并处理)和自定义InputFormat 等。
TextInputFormat
是默认的 FileInputFormat
实现类,按行读取每条记录。
LongWritable
类型。Text
类型。以下是一个示例,比如,一个分片包含了如下2条文本记录。
Rich learning form
Intelligent learning engine
每条记录表示为以下键/值对(Windows):
(0,Rich learning form\r\n)
(20,Intelligent learning engine)
每条记录表示为以下键/值对(Linux):
(0,Rich learning form\n)
(19,Intelligent learning engine)
框架默认的 TextInputFormat
切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个 MapTask
,这样如果有大量小文件,就会产生大量的 MapTask
,处理效率极其低下。
CombineTextInputFormat
用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个 MapTask
处理。
job.setInputFormatClass(CombineFileInputFormat.class);
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m
注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。
生成切片过程包括:虚拟存储过程和切片过程二部分。
将输入目录下所有文件大小,依次和设置的 setMaxInputSplitSize
值比较,会出现3种情况:
根据虚拟存储的结果进行切片:
setMaxInputSplitSize
值,大于等于则单独形成一个切片。setMaxInputSplitSize
值,则继续向后合并,否则生成新的切片Map 方法之后,Reduce 方法之前的数据处理过程称之为 Shuffle。
默认 Partitioner 分区实现逻辑:根据 key 的 hashCode 对 ReduceTasks 个数取模得到的。用户没法控制哪个 key 存储到哪个分区:
public class HashPartitioner<K2, V2> implements Partitioner<K2, V2> {
public void configure(JobConf job) {}
/** Use {@link Object#hashCode()} to partition. */
public int getPartition(K2 key, V2 value,
int numReduceTasks) {
//key.hashCode() & Integer.MAX_VALUE:避免 hashCode 值溢出
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
自定义类继承 Partitioner
,重写 getPartition()
方法
public class FlowPartition extends Partitioner<Text, FlowBean> {
@Override
public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
//控制分区代码逻辑
return partition;
}
}
在 Job 驱动中,设置自定义 Partitioner
job.setPartitionerClass(FlowPartition.class);
自定义 Partition
后,要根据自定义 Partitioner
的逻辑设置相应数量的 ReduceTask
job.setNumReduceTasks(5);
numReduceTask > getPartition()
,则会多产生几个空的输出文件 part-r-000xx
;1 < numReduceTask < getPartition()
,则有一部分分区数据无处安放,会抛 IOException ;numReduceTask=1
(默认),不会走自定义的 Partitioner
,getPartition()
始终返回 0,所以不管 MapTask
输出多少个分区文件,最终结果都交给这一个 ReduceTask ,最终也就只会产生一个结果文件 part-00000
;numReduceTask=0
,则程序会在 Mapper 之后直接输出,不会有 Shuffle 和 Reducer 阶段,输出文件个数和 Map 个数一致案例分析:
例如:假设自定义分区数为5 ,则
样例数据见 2.3.1,手机号136、137、138、139开头的分别放到一个独立的4个文件中,其他开头的放到一个文件中。
在 2.3.1 案例的基础上,做如下修改:
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Partitioner;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class FlowPartition extends Partitioner<Text, FlowBean> {
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration, "flow");
job.setJarByClass(Flow.class);
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//设置自定义Partitioner
job.setPartitionerClass(FlowPartition.class);
//设置相应数量的ReduceTask
job.setNumReduceTasks(5);
FileInputFormat.addInputPath(job, new Path("/Users/mac/IdeaProjects/bigdata/Hadoop/input/phone_data.txt"));
FileOutputFormat.setOutputPath(job, new Path("/Users/mac/IdeaProjects/bigdata/Hadoop/output/flowPartition"));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
@Override
public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
String s = text.toString();
String pre = phone.substring(0, 3);
switch (pre) {
case "136":
return 0;
case "137":
return 1;
case "138":
return 2;
case "139":
return 3;
default:
return 4;
}
}
}
排序是 MapReduce
框架中最重要的操作之一。MapTask
和 ReduceTask
均会对数据按照 key 进行排序。该操作属于 Hadoop 的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。
bean 对象做为 key 传输,需要实现 WritableComparable
接口重写 compareTo()
方法,就可以实现排序。
@Data
class FlowBean implements WritableComparable<FlowBean> {
private int id;
@Override
public void write(DataOutput out) throws IOException {
}
@Override
public void readFields(DataInput in) throws IOException {
}
@Override
public int compareTo(FlowBean o) {
//大于0升序排,小于0降序排
return this.getId() - o.getId();
}
}
Combiner
是MR程序中 Mapper 和 Reducer 之外的一种组件,Combiner 父类就是 Reducer,两者的区别在于运行的位置:
Combiner
是在每一个 MapTask 所在的节点运行,只接收当前节点上 Mapper 的输出结果Reducer
是接收全局所有 Mapper 的输出结果;Combiner 的意义就是对每一个 MapTask 的输出进行局部汇总,以减小网络传输量。
注意:Combiner 能够应用的前提是不能影响最终的业务逻辑,而且 Combiner 的输出 kv 应该跟 Reducer 的输入 kv 类型要对应起来。
错误使用案例:
假如有如下两个 Mapper,然后由 Combiner 进行合并
3 5 7 ->(3+5+7)/3=5
2 6 -> (2+6)/2=4
可以看到,经过 Combiner 合并后, Reducer 数据处理出现错误
(3+5+7+2+6)/5=23/5 不等于 (5+4)/2=9/2
自定义一个 Combiner
继承 Reducer
,重写 reduce()
方法,就可以实现自定义 Combiner
,一般 Combiner 逻辑和 Reducer 逻辑相同,可以共用一个类。
public static class IntSumReducer
extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable result = new IntWritable();
@Override
public void reduce(Text key, Iterable<IntWritable> values,
Context context
) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
result.set(sum);
context.write(key, result);
}
}
//指定 Combiner 类
job.setCombinerClass(IntSumReducer.class);
job.setReducerClass(IntSumReducer.class);
OutputFormat 是 MapReduce 输出的基类,所有 MapReduce 输出都实现了 OutputFormat 接口。下面我们介绍几种常见的 OutputFormat 实现类。
FileOutputFormat
类,获取到输出流RecordWriter
类,重写 write() 方法,在该方法中实现具体的写出逻辑job.setOutputFormatClass()
将 www
开头的网址和其它网址分别输出到两个文件中
输入文件:
www.baidu.com
www.taobao.com
www.bilibili.com
blog.csdn.net
leetcode-cn.com
github.com
gitee.com
自定义的 OutputFormat 类:
//指定输出类
job.setOutputFormatClass(WebsiteOutputFormat.class);
//自定义OutputFormat类
public static class WebsiteOutputFormat extends FileOutputFormat<Text, NullWritable> {
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
//获取输出文件路径
Path file = getOutputPath(job);
//根据配置信息,获取设施系统
FileSystem fs = file.getFileSystem(job.getConfiguration());
//创建两个输出流
FSDataOutputStream otherOs = fs.create(new Path(file.toString() + "/other.log"));
FSDataOutputStream wwwOs = fs.create(new Path(file.toString() + "/www.log"));
//创建一个 RecordWriter 对象
RecordWriter<Text, NullWritable> recordWriter = new RecordWriter<Text, NullWritable>() {
@Override
public void write(Text key, NullWritable value) throws IOException, InterruptedException {
if (key.toString().startsWith("www")) {
wwwOs.write(key.getBytes());
wwwOs.write("\n".getBytes());
} else {
otherOs.write(key.getBytes());
otherOs.write("\n".getBytes());
}
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
IOUtils.closeStream(wwwOs);
IOUtils.closeStream(otherOs);
}
};
return recordWriter;
}
}
案例:
代码:https://gitee.com/xinboss/bigdata/blob/master/Hadoop/src/main/java/com/atguigu/mapreduce/reduceJoin/ReduceJoinDemo.java
代码中需要注意的地方:
- Mapper 读取两个文件,在初始化方法
setup()
中(每读取一个文件执行一次)获取文件名- Mapper 处理代码中,根据文件名,给数据打上标记,记录是来自哪个文件。
- 给自定义对象赋值时,值不能赋为 null,否则会在序列化时报 NullPointerException
- Reducer 中,传入的 value 的迭代器在迭代时,引用地址不变,即始终是一个对象,所以需要新创建一个对象,然后将 value 中的参数值拷贝过去。
Map Join 适用于一张表十分小、一张表很大的场景。在 Reduce 端处理过多的表,非常容易产生数据倾斜。因此就使用到了 Map Join:在 Map 端缓存多张表,提前处理业务逻辑,这样增加 Map 端业务,减少 Reduce 端数据的压力,尽可能的减少数据倾斜。
案例,同Reduce Join
代码:https://gitee.com/xinboss/bigdata/blob/master/Hadoop/src/main/java/com/atguigu/mapreduce/mapJoin/MapJoinDemo.java
代码中需要注意的地方:
- 设置要缓存的文件:
//缓存普通文件到 MapTask 运行节点 job.addCacheFile(new URI("file:///input/reduceJoin/pd.txt")); job.addCacheFile(new URI("file:///D:/input/tablecache/pd.txt")); //如果是 Hadoop 集群运行,则设置HDFS路径 job.addCacheFile(new URI("hdfs://hadoop102:8020/cache/pd.txt"));
- 在 Mapper 的 setup() 方法中,将缓存文件解析到 HashMap 中,方便后面 map() 方法中直接调用
压缩的好处和坏处:
压缩原则:
压缩格式 | Hadoop是否自带 | 算法 | 文件扩展名 | 是否可切片 | 换成压缩格式后,原来的程序是否需要修改 |
---|---|---|---|---|---|
DEFLATE | 是,直接使用 | DEFLATE | .deflate | 否 | 和文本处理一样,不需要修改 |
Gzip | 是,直接使用 | DEFLATE | .gz | 否 | 和文本处理一样,不需要修改 |
bzip2 | 是,直接使用 | bzip2 | .bz2 | 是 | 和文本处理一样,不需要修改 |
LZO | 否,需要安装 | LZO | .lzo | 是 | 需要建索引,还需要指定输入格式 |
Snappy | 是,直接使用 | Snappy | .snappy | 否 | 和文本处理一样,不需要修改 |
压缩算法 | 原始文件大小 | 压缩文件大小 | 压缩速度 | 解压速度 |
---|---|---|---|---|
gzip | 8.3GB | 1.8GB | 17.5MB/s | 58MB/s |
bzip2 | 8.3GB | 1.1GB | 2.4MB/s | 9.5MB/s |
LZO | 8.3GB | 2.9GB | 49.3MB/s | 74.6MB/s |
Snappy 官方介绍:http://google.github.io/snappy/
Snappy is a compression/decompression library. It does not aim for maximum compression, or compatibility with any other compression library; instead, it aims for very high speeds and reasonable compression. For instance, compared to the fastest mode of zlib, Snappy is an order of magnitude faster for most inputs, but the resulting compressed files are anywhere from 20% to 100% bigger.On a single core of a Core i7 processor in 64-bit mode, Snappy compresses at about 250 MB/sec or more and decompresses at about 500 MB/sec or more.
Snappy是一个压缩/解压缩库。它不以最大压缩或与任何其他压缩库兼容为目标;相反,它的目标是非常高的速度和合理的压缩。例如,与zlib的最快模式相比,Snappy对于大多数输入来说要快一个数量级,但产生的压缩文件要大20%到100%。在64位模式的core i7处理器的单核上,Snappy以大约250 MB/秒或更高的速度压缩,以大约500 MB/秒或更高的速度解压缩。
压缩方式选择时重点考虑:压缩/解压缩速度、压缩率(压缩后存储大小)、压缩后是否可以支持切片。
1)为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器
压缩格式 | 对应的编码/解码器 |
---|---|
DEFLATE | org.apache.hadoop.io.compress.DefaultCodec |
gzip | org.apache.hadoop.io.compress.GzipCodec |
bzip2 | org.apache.hadoop.io.compress.BZip2Codec |
LZO | com.hadoop.compression.lzo.LzopCodec |
Snappy | org.apache.hadoop.io.compress.SnappyCodec |
2)要在Hadoop中启用压缩,可以配置如下参数
参数 | 默认值 | 阶段 | 建议 |
---|---|---|---|
io.compression.codecs (在core-site.xml中配置) |
无,这个需要在 命令行输入 hadoop checknative 查看 |
输入压缩 | Hadoop使用文件扩展名判断是否支持某种编解码器 |
mapreduce.map.output.compress (在mapred-site.xml中配置) |
false | mapper输出 | 这个参数设为true启用压缩 |
mapreduce.map.output.compress.codec (在mapred-site.xml中配置) |
org.apache.hadoop.io.compress.DefaultCodec | mapper输出 | 企业多使用LZO或Snappy编解码器在此阶段压缩数据 |
mapreduce.output.fileoutputformat.compress (在mapred-site.xml中配置) |
false | reducer输出 | 这个参数设为true启用压缩 |
mapreduce.output.fileoutputformat.compress.codec (在mapred-site.xml中配置) |
org.apache.hadoop.io.compress.DefaultCodec | reducer输出 | 使用标准工具或者编解码器,如gzip和bzip2 |
mapreduce.output.fileoutputformat.compress.type (在mapred-site.xml中配置) |
RECORD(按行压缩) | reducer 输出 | SequenceFile 输出使用的压缩类型:NONE 和 BLOCK(按块压缩) |
即使你的 MapReduce 的输入输出文件都是未压缩的文件,你仍然可以对 Map 任务的中间结果输出做压缩,因为它要写在硬盘并且通过网络传输到 Reduce 节点,对其压缩可以提高很多性能,这些工作只要设置两个属性即可。
Configuration conf = new Configuration();
// 开启map端输出压缩
conf.setBoolean("mapreduce.map.output.compress", true);
// 设置map端输出压缩方式
conf.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class,CompressionCodec.class);
Job job = Job.getInstance(conf);
job.set.....//省略
// 设置reduce端输出压缩开启
FileOutputFormat.setCompressOutput(job, true);
// 设置压缩的方式
FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);
//FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);
//FileOutputFormat.setOutputCompressorClass(job, DefaultCodec.class);
使用 Sort 程序评测 MapReduce
注:一个虚拟机不超过150G磁盘尽量不要执行这段代码
使用 RandomWriter 来产生随机数,每个节点运行10个Map任务,每个Map产生大约1G大小的二进制随机数
$ hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.3.1.jar randomwriter random-data
执行Sort程序
$ hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.3.1.jar sort random-data sorted-data
验证数据是否真正排好序了
$ hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-client-jobclient-3.3.1-tests.jar testmapredsort -sortInput random-data -sortOutput sorted-data
MapReduce 跑的慢, 程序效率的瓶颈在于两点:
- 计算机性能:CPU、内存、磁盘、网络
- I/O操作优化:
- 数据倾斜
- Map运行时间太长,导致Reduce等待过久
- 小文件过多
自定义分区,减少数据倾斜;
定义类,继承 Partitioner 接口,重写 getPartition 方法
减少溢写的次数
mapreduce.task.io.sort.mb
:Shuffle的环形缓冲区大小,默认100m,可以提高到200m
mapreduce.map.sort.spill.percent
:环形缓冲区溢出的阈值,默认80% ,可以提高的90%
增加每次Merge合并次数
mapreduce.task.io.sort.factor
:默认10,可以提高到20
在不影响业务结果的前提条件下可以提前采用 Combiner
job.setCombinerClass(xxxReducer.class);
为了减少磁盘IO,可以采用 Snappy 或者 LZO 压缩
conf.setBoolean("mapreduce.map.output.compress", true);
conf.setClass("mapreduce.map.output.compress.codec", SnappyCodec.class,CompressionCodec.class);
增加堆内存
mapreduce.map.memory.mb
:默认MapTask内存上限1024MB。可以根据128m数据对应1G内存原则提高该内存。
mapreduce.map.java.opts
:控制MapTask堆内存大小。(如果内存不够,报:java.lang.OutOfMemoryError)
mapreduce.map.cpu.vcores
:默认MapTask的CPU核数1。计算密集型任务可以增加CPU核数
异常重试
mapreduce.map.maxattempts
:每个Map Task最大重试次数,一旦重试次数超过该值,则认为Map Task运行失败,默认值:4。根据机器性能适当提高。
mapreduce.reduce.shuffle.parallelcopies
:每个 Reduce 去 Map 中拉取数据的并行数,默认值是 5。可以提高到 10。mapreduce.reduce.shuffle.input.buffer.percent
:Buffer 大小占 Reduce 可用内存的比例,默认值 0.7。可以提高到 0.8mapreduce.reduce.shuffle.merge.percent
:Buffer 中的数据达到多少比例开始写入磁盘,默认值0.66。可以提高到0.75mapreduce.reduce.memory.mb
:默认 ReduceTask 内存上限 1024MB,根据128m数据对应1G内存原则,适当提高内存到4-6Gmapreduce.reduce.java.opts
:控制 ReduceTask 堆内存大小。(如果内存不够,报:java.lang.OutOfMemoryError)mapreduce.reduce.cpu.vcores
:默认 ReduceTask 的CPU核数1个。可以提高到2-4个mapreduce.reduce.maxattempts
:每个 ReduceTask 最大重试次数,一旦重试次数超过该值,则认为Map Task运行失败,默认值:4。mapreduce.job.reduce.slowstart.completedmaps
:当 MapTask 完成的比例达到该值后才会为 ReduceTask 申请资源。默认是0.05。mapreduce.task.timeout
:如果一个Task在一定时间内没有任何进入,即不会读取新的数据,也没有输出数据,则认为该Task处于Block状态,可能是卡住了,也许永远会卡住,为了防止因为用户程序永远Block住不退出,则强制设置了一个该超时时间(单位毫秒),默认是600000(10分钟)。如果你的程序对每条输入数据的处理时间过长,建议将该参数调大。数据倾斜现象:
减少数据倾斜的方法:
HDFS上每个文件都要在NameNode上创建对应的元数据,这个元数据的大小约为150byte,这样当小文件比较多的时候,就会产生很多的元数据文件,一方面会大量占用NameNode的内存空间,另一方面就是元数据文件过多,使得寻址索引速度变慢。
小文件过多,在进行MR计算时,会生成过多切片,需要启动过多的MapTask。每个MapTask处理的数据量小,导致MapTask的处理时间比启动时间还小,白白消耗资源。
是一个高效的将小文件放入HDFS块中的文件存档工具,能够将多个小文件打包成一个HAR文件,从而达到减少NameNode的内存使用
CombineTextInputFormat用于将多个小文件在切片过程中生成一个单独的切片或者少量的切片。
默认情况下,每个 Task 任务都需要启动一个 JVM 来运行,如果 Task 任务计算的数据量很小,我们可以让同一个 Job 的多个 Task 运行在一个 JVM 中,不必为每个 Task 都开启一个 JVM。
未开启 uber 模式,在 /input 路径上上传多个小文件并执行 wordcount 程序
$ hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.3.1.jar wordcount /input /output2
观察 http://hadoop103:8088/cluster
开启 uber 模式,在 mapred-site.xml
中添加如下配置
<property>
<name>mapreduce.job.ubertask.enablename>
<value>truevalue>
property>
<property>
<name>mapreduce.job.ubertask.maxmapsname>
<value>2value>
property>
<property>
<name>mapreduce.job.ubertask.maxreducesname>
<value>1value>
property>
<property>
<name>mapreduce.job.ubertask.maxbytesname>
<value>value>
property>
分发配置
再次执行 wordcount 程序
$ hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.3.1.jar wordcount /input /output2