一、MapReduce概述
1.MapReduce核心思想
2.WordCount案例
Mapper类:
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/*
map阶段
KEYIN:输入数据的key:行偏移量
VALUE:输入数据的value:当前行
KEYOUT:输出数据的key:单词
VALUEOUT:输出数据的value:1
*/
public class WordcountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
Text k = new Text();
IntWritable v = new IntWritable(1);
// 1.获取一行
String line = value.toString();
// 2.切割单词
String[] words = line.split(" ");
// 3.循环写出
for (String word : words) {
k.set(word);
context.write(k, v);
}
}
}
Reducer类:
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class WordcountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
IntWritable v = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
// 1.累加求和
int sum = 0;
for (IntWritable value : values) {
sum+=value.get();
}
// 2.封装value
v.set(sum);
// 3.写出
context.write(key,v);
}
}
Driver类:
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.mapreduce.lib.input.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class WordcountDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException{
args = new String [] {"/Users/cc/Downloads/hadoop-study/input","/Users/cc/Downloads/hadoop-study/output"};
// 1 获取Job对象
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 2 设置jar存储的位置
job.setJarByClass(WordcountDriver.class);
// 3 关联Map和Reduce类
job.setMapperClass(WordcountMapper.class);
job.setReducerClass(WordcountReducer.class);
// 4 设置Mapper阶段输出数据的key和value类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 5 设置最终数据输出的key和value类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 6 设置输入路径和输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 7 提交Job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
二、Hadoop概述
1.序列化概述
🤔:为什么不用Java序列化?
Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息,Header,继承体系等),不便于在网络中高效传输。所以,Hadoop自己开发了一套序列化机制(Writable)。
2.自定义Bean对象实现序列化接口(Writable)
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* @Description:自定义Bean对象
* @Author: lnch
* @Date: 5/8/20 12:06 上午
*/
public class FlowBean implements Writable {
private long upFlow; //上行流量
private long downFlow; //下行流量
private long sumFlow; //总流量
// 反序列化时,需要反射调用空参构造函数,所以必须有空参构造
public FlowBean() {
}
public FlowBean(long upFlow, long downFlow) {
this.upFlow = upFlow;
this.downFlow = downFlow;
this.sumFlow = upFlow + downFlow;
}
// 序列化方法
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeLong(upFlow);
dataOutput.writeLong(downFlow);
dataOutput.writeLong(sumFlow);
}
//反序列化方法
@Override
public void readFields(DataInput dataInput) throws IOException {
// 必须和序列化的顺序一致
upFlow = dataInput.readLong();
downFlow = dataInput.readLong();
sumFlow = dataInput.readLong();
}
public long getUpFlow() {
return upFlow;
}
public void setUpFlow(long upFlow) {
this.upFlow = upFlow;
}
public long getDownFlow() {
return downFlow;
}
public void setDownFlow(long downFlow) {
this.downFlow = downFlow;
}
public long getSumFlow() {
return sumFlow;
}
public void setSumFlow(long sumFlow) {
this.sumFlow = sumFlow;
}
public void set(long upFlow2, long downFlow2) {
upFlow = upFlow2;
downFlow = downFlow2;
sumFlow = upFlow2 + downFlow2;
}
// 要想把结果显示在文件中,需要重写toString(),可用”\t”分开,方便后续用
@Override
public String toString() {
return upFlow +
"\t" + downFlow +
"\t" + sumFlow;
}
}
注:如果需要将自定义的bean放在key中传输,则需要实现WritableComparable接口,重写compareTo方法,因为MapReduce框架中的Shuffle过程要求对key必须能排序。下文有案例待更新……
三、MapReduce工作流程
Map阶段工作流程
Reduce阶段工作流程
1. InputFormat数据输入
1.1 切片与MapTask并行度决定机制
数据块:Block是HDFS物理上把数据分成一块一块。
数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。
理解:
- Driver类作为MapperReducer的客户端,在提交Job任务时,可以设置切片数。
- 不考虑数据集整体的意思是,切片只针对单个文件,ss.avi是一个文件,存储在前三个Datanode上,ss2.avi是一个文件,存储在第四个Datanode上,那么切片只会在前三个Datanode中逻辑上切片,或者在第四个Datanode中逻辑上切片。
- 为了减少Datanode间的网络I/O开销,默认切片大小为块大小。
1.2 Job的提交源码和切片源码流程
1.3 FileInputFormat切片机制
(1)源码中计算切片大小的公式
Math.max(minSize, Math.min(maxSize, blockSize));
其中,
mapreduce.input.fileinputformat.split.minsize=1 默认值为1
mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默认值Long.MAXValue
(2)切片大小设置
maxsize(切片最大值):参数如果调得比blockSize小,则会让切片变小,而且就等于配置的这个参数的值。
minsize(切片最小值):参数调的比blockSize大,则可以让切片变得比blockSize还大。
1.4 CombineTextInputFormat切片机制
CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304); // 4m
生成切片过程包括:虚拟存储过程和切片过程二部分。
案例:
输入4个小文件,期望一个切片处理4个文件
// 如果不设置InputFormat,它默认用的是TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);
//虚拟存储切片最大值设置20m
CombineTextInputFormat.setMaxInputSplitSize(job, 20971520);
1.5 KeyValueTextInputFormat切片机制
每一行均为一条记录,被分隔符分割为key和value。
//设置分隔符
conf.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR, " ");
//设置InputFormat
job.setInputFormatClass(KeyValueTextInputFormat.class);
1.6 NLineInputFormat切片机制
切片不再按照Block来划分,而是按照指定的NlineInputFormat指定的行数N来划分,即切片数=输入文件的总行书/N
// 7设置每个切片InputSplit中划分三条记录
NLineInputFormat.setNumLinesPerSplit(job, 3);
// 8使用NLineInputFormat处理记录数
job.setInputFormatClass(NLineInputFormat.class);
1.7自定义InputFormat
案例:一次读取一个文件,封装为KV
(1)继承FileInputFormat,重写createRecordReader方法
import org.apache.hadoop.io.BytesWritable;
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;
/**
* @Description:自定义InputFormat
* @Author: lnch
* @Date: 5/10/20 10:12 下午
*/
public class WholeFileInputformat extends FileInputFormat<Text, BytesWritable> {
@Override
public RecordReader<Text, BytesWritable> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
//创建自定义RecordReader对象
WholeRecordReader recordReader = new WholeRecordReader();
recordReader.initialize(split, context);
return recordReader;
}
}
(2)自定义类,继承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.BytesWritable;
import org.apache.hadoop.io.IOUtils;
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 java.io.IOException;
/**
* @Description:
* @Author: lnch
* @Date: 5/10/20 10:41 下午
*/
public class WholeRecordReader extends RecordReader<Text, BytesWritable> {
FileSplit split;
Configuration configuration;
Text k = new Text();
BytesWritable v = new BytesWritable();
boolean isProgress = true;
// 初始化方法,传入切片和配置信息
@Override
public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
// 初始化
this.split = (FileSplit) split;
configuration = context.getConfiguration();
}
//主要业务逻辑的实现方法
@Override
public boolean nextKeyValue() throws IOException, InterruptedException {
// 核心业务逻辑处理
byte[] buf = new byte[(int) split.getLength()];
if(isProgress) {
// 1.获取fs对象
Path path = split.getPath();
FileSystem fs = path.getFileSystem(configuration);
// 2.获取输入流
FSDataInputStream fis = fs.open(path);
// 3.拷贝
IOUtils.readFully(fis, buf, 0, buf.length);
// 4.封装v
v.set(buf,0, buf.length);
// 5.封装k
k.set(path.toString());
// 6.关闭资源
IOUtils.closeStream(fis);
isProgress = false;
return true;
}
return false;
}
@Override
public Text getCurrentKey() throws IOException, InterruptedException {
return k;
}
@Override
public BytesWritable getCurrentValue() throws IOException, InterruptedException {
return v;
}
@Override
public float getProgress() throws IOException, InterruptedException {
return 0;
}
@Override
public void close() throws IOException {
}
}
(3)设置InputFormat和OutputFormat
// 7. 设置输入的inputFormat
job.setInputFormatClass(WholeFileInputformat.class);
// 8. 设置输出的outputFormat
job.setOutputFormatClass(SequenceFileOutputFormat.class);
2. Shuffle机制
Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle。
2.1 Partition分区
2.1.1 默认Partitione分区
public class HashPartitioner<K, V> extends Partitioner<K, V> {
public int getPartition(K key, V value, int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
默认分区是根据key的hashCode对ReduceTasks个数取模得到的。用户没法控制哪个key存储到哪个分区。其中,默认numReduceTasks为1。
2.1.2 自定义Partitione分区
继承Partitioner类,重写getPartition方法
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
/**
* @Description:自定义partitioner
* @Author: lnch
* @Date: 5/11/20 10:49 下午
*/
public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
@Override
public int getPartition(Text key, FlowBean value, int numPartitions) {
// key是手机号
// value是流量信息
// 获取手机号前3位
String prePhoneNum = key.toString().substring(0, 3);
int partition = 4;
if ("136".equals(prePhoneNum)) {
partition = 0;
}else if ("137".equals(prePhoneNum)) {
partition = 1;
}else if ("138".equals(prePhoneNum)) {
partition = 2;
}else if ("139".equals(prePhoneNum)) {
partition = 3;
}else {
partition = 4;
}
return partition;
}
}
// 设置自定义Partitioner
job.setPartitionerClass(ProvincePartitioner.class);
// 设置ReduceTask任务数
job.setNumReduceTasks(5);
总结:
(1)如果ReduceTask的数量> getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;
(2)如果1<ReduceTask的数量<getPartition的结果数,则有一部分分区数据无处安放,会Exception;
(3)如果ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个ReduceTask,最终也就只会产生一个结果文件 part-r-00000;
(4)分区号必须从零开始,逐一累加。
2.2 WritableComparable排序
2.2.1 排序的分类
(1)部分排序
MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部有序。
(2)全排序
最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask。但该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构。
(3)二次排序
在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。
(4)辅助排序(GroupingComparator分组)
在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序。
2.2.1 全排序
bean对象做为key传输,需要实现WritableComparable接口重写compareTo方法,就可以实现排序。
自定义Bean对象,实现WritableComparable接口,重写序列化方法、反序列化方法,和compareTo排序方法
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* @Description: 自定义Bean对象,实现排序
* @Author: lnch
* @Date: 5/12/20 10:02 下午
*/
public class FlowBean implements WritableComparable<FlowBean> {
private long upFlow; //上行流量
private long downFlow; //下行流量
private long sumFlow; //总流量
// 反序列化时,需要反射调用空参构造函数,所以必须有
public FlowBean() {
}
public FlowBean(long upFlow, long downFlow) {
this.upFlow = upFlow;
this.downFlow = downFlow;
sumFlow = upFlow + downFlow;
}
// 比较,按照总流量大小倒序排序
@Override
public int compareTo(FlowBean bean) {
int result;
// 核心的比较条件判断
if (sumFlow > bean.getSumFlow()) {
result = -1;
}else if (sumFlow < bean.getSumFlow()) {
result = 1;
}else {
result = 0;
}
return result;
}
// 序列化方法
@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();
}
public long getUpFlow() {
return upFlow;
}
public void setUpFlow(long upFlow) {
this.upFlow = upFlow;
}
public long getDownFlow() {
return downFlow;
}
public void setDownFlow(long downFlow) {
this.downFlow = downFlow;
}
public long getSumFlow() {
return sumFlow;
}
public void setSumFlow(long sumFlow) {
this.sumFlow = sumFlow;
}
@Override
public String toString() {
return upFlow +
"\t" + downFlow +
"\t" + sumFlow;
}
}
2.2.2 区内排序
在全排序的基础上自定义分区类,见2.1.2
2.2.3 二次排序
@Override
public int compareTo(OrderBean bean) {
// 先按照订单id进行升序排序,如果相同,按照价格降序排序
int result;
if (order_id > bean.getOrder_id()) {
result = 1;
}else if (order_id < bean.getOrder_id()) {
result = -1;
}else {
if (price > bean.getPrice()) {
result = -1;
}else if (price < bean.getPrice()) {
result = 1;
}else {
result = 0;
}
}
2.2.4 辅助排序(GroupingComparator分组)
辅助排序在Reduce阶段,见下文……
2.3 Combiner合并
(1)Combiner是MR程序中Mapper和Reducer之外的一种组件。
(2)Combiner组件的父类就是Reducer。
(3)Combiner和Reducer的区别在于运行的位置:
- Combiner是在每一个MapTask所在的节点运行;
- Reducer是接收全局所有Mapper的输出结果;
(4)Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量。
(5)Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combiner的输出kv应该跟Reducer的输入kv类型要对应起来。适用于汇总,但不适用于求平均数。
其实方案一中的WordcountCombiner和方案二中的WordcountReducer内容一摸一样,只是将Reducer方法在Map阶段多执行一次,进行局部汇总。
// 指定需要使用Combiner,以及用哪个类作为Combiner的逻辑
job.setCombinerClass(WordcountReducer.class);
2.4 辅助排序(GroupingComparator分组)
partioner是在MapTask阶段将数据写入环形缓冲区中进行的分区操作,其目的是为了划分出几个结果文件(ReduceTask,但是partioner必须小于ReduceTask个数),而是什么决定将一组数据发送给一次Reduce类中的reduce方法中呢?换句话说,Reduce类中的reduce方法中key一样,values有多个,是什么情况下的key是一样的,能不能自定义。其实这就是 GroupingComparator分组(辅助排序)的作用。
————————————————
版权声明:本文为CSDN博主「qq_43193797」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43193797/article/details/86093138
继承WritableComparator类,创建一个构造方法,将比较对象的类传给父类,重写compare方法。注意:区分WritableComparablae排序和辅助排序的区别,前者实现了WritableComparablae类,重写的是compareTo方法,是在Map阶段执行的,后者s继承了WritableComparator类,重写的是compare方法,而且要求一个特殊的构造方法,将比较对象的类传给父类。
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;
/**
* @Description:辅助排序
* @Author: lnch
* @Date: 5/13/20 10:11 下午
*/
public class OrderGroupingComparator extends WritableComparator {
//创建一个构造将比较对象的类传给父类
protected OrderGroupingComparator() {
super(OrderBean.class, true);
}
@Override
public int compare(WritableComparable a, WritableComparable b) {
// 要求只要id相同,就认为是相同的key
OrderBean aBean = (OrderBean) a;
OrderBean bBean = (OrderBean) b;
int result;
if (aBean.getOrder_id() > bBean.getOrder_id()) {
result = 1;
}else if (aBean.getOrder_id() < bBean.getOrder_id()) {
result = -1;
}else {
result = 0;
}
return result;
}
}
// 8 设置reduce端的分组
job.setGroupingComparatorClass(OrderGroupingComparator.class);
3. Outputformat
OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了 OutputFormat接口。
3.1.1 文本输出TextOutputFormat
默认的输出格式是TextOutputFormat,它把每条记录写为文本行。它的键和值可以是任意类型,因为TextOutputFormat调用toString()方法把它们转换为字符串。
3.1.2 SequenceFileOutputFormat
将SequenceFileOutputFormat输出作为后续 MapReduce任务的输入,这便是一种好的输出格式,因为它的格式紧凑,很容易被压缩。
3.1.3 自定义OutputFormat
案例:要在一个MapReduce程序中根据数据的不同输出两类结果到不同目录,这类灵活的输出需求可以通过自定义OutputFormat来实现。
(1)自定义一个类继承FileOutputFormat
(2)改写RecordWriter,具体改写输出数据的方法write()
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
/**
* @Description:
* @Author: lnch
* @Date: 5/14/20 10:49 下午
*/
public class FilterOutputFormat extends FileOutputFormat<Text, NullWritable> {
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
return new FRecordWriter(job);
}
}
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import java.io.IOException;
/**
* @Description:
* @Author: lnch
* @Date: 5/14/20 10:51 下午
*/
public class FRecordWriter extends RecordWriter<Text, NullWritable> {
FSDataOutputStream fosatguigu;
FSDataOutputStream fosother;
public FRecordWriter(TaskAttemptContext job) {
try {
// 1.获取文件系统
FileSystem fs = FileSystem.get(job.getConfiguration());
// 2.创建输出到atguigu.log的输出流
fosatguigu = fs.create(new Path("/Users/lianchao/Downloads/hadoop-study/output/atguigu.log"));
// 3.创建输出到other.log的输出流
fosother = fs.create(new Path("/Users/lianchao/Downloads/hadoop-study/output/other.log"));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void write(Text key, NullWritable value) throws IOException, InterruptedException {
// 判断key当中是否有atguigu,如果有,写出到atguigu.log,如果没有,写出到other.log
if (key.toString().contains("atguigu")) {
// atguigu输出流
fosatguigu.write(key.toString().getBytes());
}else {
fosother.write(key.toString().getBytes());
}
}
@Override
public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
IOUtils.closeStream(fosatguigu);
IOUtils.closeStream(fosother);
}
}
// 要将自定义的输出格式组件设置到job中
job.setOutputFormatClass(FilterOutputFormat.class);
4. Join多种应用
4.1 Reduce Join
Map端的主要工作:为来自不同表或文件的key/value对,打标签以区别不同来源的记录。然后用连接字段作为key,其余部分和新加的标志作为value,最后进行输出。
Reduce端的主要工作:在Reduce端以连接字段作为key的分组已经完成,我们只需要在每一个分组当中将那些来源于不同文件的记录(在Map阶段已经打标志)分开,最后进行合并就ok了。
1)创建商品和订合并后的Bean类
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* @Description:
* @Author: lnch
* @Date: 5/20/20 8:46 下午
*/
public class TableBean implements Writable {
// id pid amount
// pid pname
private String id; // 订单id
private String pid; // 产品id
private int amount; // 数量
private String pname; // 产品名称
private String flag; // 定义一个标记,标记是订单表还是产品表
public TableBean() {
}
public TableBean(String id, String pid, int amount, String pname, String flag) {
this.id = id;
this.pid = pid;
this.amount = amount;
this.pname = pname;
this.flag = flag;
}
@Override
public void write(DataOutput out) throws IOException {
// 序列化方法
out.writeUTF(id);
out.writeUTF(pid);
out.writeInt(amount);
out.writeUTF(pname);
out.writeUTF(flag);
}
@Override
public void readFields(DataInput in) throws IOException {
// 反序列化方法
id = in.readUTF();
pid = in.readUTF();
amount = in.readInt();
pname = in.readUTF();
flag = in.readUTF();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPid() {
return pid;
}
public void setPid(String pid) {
this.pid = pid;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
public String getPname() {
return pname;
}
public void setPname(String pname) {
this.pname = pname;
}
public String getFlag() {
return flag;
}
public void setFlag(String flag) {
this.flag = flag;
}
@Override
public String toString() {
return id + "\t" +
amount + "\t" +
pname;
}
}
2)编写TableMapper类
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.IOException;
/**
* @Description:
* @Author: lnch
* @Date: 5/20/20 9:48 下午
*/
public class TableMapper extends Mapper<LongWritable, Text, Text, TableBean> {
String name;
@Override
protected void setup(Context context) throws IOException, InterruptedException {
// 获取文件的名称
FileSplit inputSplit = (FileSplit) context.getInputSplit();
name = inputSplit.getPath().getName();
}
TableBean tableBean = new TableBean();
Text k = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// id pid amount
// 1001 01 1
// pid pname
// 01 小米
// 获取一行
String line = value.toString();
if (name.startsWith("order")) { // 订单表
String[] fields = line.split("\t");
// 封装key和value
tableBean.setId(fields[0]);
tableBean.setPid(fields[1]);
tableBean.setAmount(Integer.parseInt(fields[2]));
tableBean.setPname("");
tableBean.setFlag("order");
k.set(fields[1]);
}else { //产品表
String[] fields = line.split("\t");
// 封装key和value
tableBean.setId("");
tableBean.setPid(fields[0]);
tableBean.setAmount(0);
tableBean.setPname(fields[1]);
tableBean.setFlag("pd");
k.set(fields[0]);
}
// 写出
context.write(k, tableBean);
}
}
3)编写TableReducer类
import org.apache.commons.beanutils.BeanUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
/**
* @Description:
* @Author: lnch
* @Date: 5/20/20 11:03 下午
*/
public class TableReducer extends Reducer<Text, TableBean, TableBean, NullWritable> {
@Override
protected void reduce(Text key, Iterable<TableBean> values, Context context) throws IOException, InterruptedException {
// 存储所有订单集合
ArrayList<TableBean> orderBeans = new ArrayList<TableBean>();
// 存放产品的信息
TableBean pdBean = new TableBean();
for (TableBean tableBean : values) {
if ("order".equals(tableBean.getFlag())) { // 订单表
TableBean tempBean = new TableBean();
try {
BeanUtils.copyProperties(tempBean, tableBean);
orderBeans.add(tempBean);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}else {
try {
BeanUtils.copyProperties(pdBean, tableBean);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
for (TableBean orderBean : orderBeans) {
orderBean.setPname(pdBean.getPname());
context.write(orderBean, NullWritable.get());
}
}
}
4)编写TableDriver类
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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;
/**
* @Description:
* @Author: lnch
* @Date: 5/21/20 12:06 上午
*/
public class TableDriver {
public static void main(String[] args) throws Exception {
// 0 根据自己电脑路径重新配置
args = new String[]{"/Users/lianchao/Downloads/hadoop-study/input/inputjoin","/Users/lianchao/Downloads/hadoop-study/output/outputjoin"};
// 1 获取配置信息,或者job对象实例
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 2 指定本程序的jar包所在的本地路径
job.setJarByClass(TableDriver.class);
// 3 指定本业务job要使用的Mapper/Reducer业务类
job.setMapperClass(TableMapper.class);
job.setReducerClass(TableReducer.class);
// 4 指定Mapper输出数据的kv类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(TableBean.class);
// 5 指定最终输出的数据的kv类型
job.setOutputKeyClass(TableBean.class);
job.setOutputValueClass(NullWritable.class);
// 6 指定job的输入原始文件所在目录
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 7 将job中配置的相关参数,以及job所用的java类所在的jar包, 提交给yarn去运行
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
4.2 Map Join
思考:在Reduce端处理过多的表,非常容易产生数据倾斜。怎么办?
在Map端缓存多张表,提前处理业务逻辑,这样增加Map端业务,减少Reduce端数据的压力,尽可能的减少数据倾斜。
具体办法:采用DistributedCache
(1)在Mapper的setup阶段,将文件读取到缓存集合中。
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
/**
* @Description:
* @Author: lnch
* @Date: 5/21/20 8:20 下午
*/
public class DistributedCacheMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
HashMap<String, String> pdMap = new HashMap<String, String>();
@Override
protected void setup(Context context) throws IOException, InterruptedException {
// 缓存小表
URI[] cacheFiles = context.getCacheFiles();
String path = cacheFiles[0].getPath();
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(path), "UTF-8"));
String line;
while (StringUtils.isNotEmpty(line = reader.readLine())) {
// pid pname
// 01 小米
// 1.切割
String[] fields = line.split("\t");
pdMap.put(fields[0], fields [1]);
}
IOUtils.closeStream(reader);
}
Text k = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// id pid amount
// 1001 01 1
// pid pname
// 01 小米
// 1.读取一行
String line = value.toString();
// 2.切割
String[] fields = line.split("\t");
// 3.获取pid
String pid = fields[1];
// 4.取出pname
String pname = pdMap.get(pid);
// 5.拼接
line = line + "\t" + pname;
// 6.写出
k.set(line);
context.write(k, NullWritable.get());
}
}
(2)在驱动函数中加载缓存。
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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;
import java.net.URI;
/**
* @Description:
* @Author: lnch
* @Date: 5/21/20 8:15 下午
*/
public class DistributedCacheDriver {
public static void main(String[] args) throws Exception {
args = new String[]{"/Users/lianchao/Downloads/hadoop-study/input/inputcache/inputorder", "/Users/lianchao/Downloads/hadoop-study/output/outputcache"};
// 1 获取job信息
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 2 设置加载jar包路径
job.setJarByClass(DistributedCacheDriver.class);
// 3 关联map
job.setMapperClass(DistributedCacheMapper.class);
// 4 设置最终输出数据类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// 5 设置输入输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 6 加载缓存数据
job.addCacheFile(new URI("/Users/lianchao/Downloads/hadoop-study/input/inputcache/inputpd/pd.txt"));
// 7 Map端Join的逻辑不需要Reduce阶段,设置reduceTask数量为0
job.setNumReduceTasks(0);
// 8 提交
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
四、压缩
1. 数据流的压缩与解压缩
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.compress.CompressionCodec;
import org.apache.hadoop.io.compress.CompressionCodecFactory;
import org.apache.hadoop.io.compress.CompressionInputStream;
import org.apache.hadoop.io.compress.CompressionOutputStream;
import org.apache.hadoop.util.ReflectionUtils;
import java.io.*;
/**
* @Description:压缩
* @Author: lnch
* @Date: 5/26/20 5:37 下午
*/
public class TestCompress {
public static void main(String[] args) throws Exception {
// 压缩(第二个参数为对应的编码器)
// DEFLATE org.apache.hadoop.io.compress.DefaultCodec
// gzip org.apache.hadoop.io.compress.GzipCodec
// bzip2 org.apache.hadoop.io.compress.BZip2Codec
//compress("/Users/lianchao/Downloads/hadoop-study/input/compress/hello.txt","org.apache.hadoop.io.compress.GzipCodec");
decompress("/Users/lianchao/Downloads/hadoop-study/input/compress/hello.txt.gz");
}
/*解压缩*/
private static void decompress(String fileName) throws IOException {
// 1压缩方式检查
CompressionCodecFactory factory = new CompressionCodecFactory(new Configuration());
CompressionCodec codec = factory.getCodec(new Path(fileName));
if (codec == null) {
System.out.println("cannot find codec for file " + fileName);
return;
}
// 2获取输入流
FileInputStream fis = new FileInputStream(new File(fileName));
CompressionInputStream cis = codec.createInputStream(fis);
// 3获取输出流
FileOutputStream fos = new FileOutputStream(new File(fileName + ".decode"));
// 4流的对拷
IOUtils.copyBytes(cis, fos, 1024*1024, false);
// 5关闭资源
IOUtils.closeStream(fos);
IOUtils.closeStream(cis);
IOUtils.closeStream(fis);
}
/*压缩*/
private static void compress(String fileName, String method) throws Exception {
// 1获取输入流
FileInputStream fis = new FileInputStream(new File(fileName));
Class<?> classCodec = Class.forName(method);
CompressionCodec codec = (CompressionCodec) ReflectionUtils.newInstance(classCodec, new Configuration());
// 2获取输出流
FileOutputStream fos = new FileOutputStream(new File(fileName + codec.getDefaultExtension()));
CompressionOutputStream cos = codec.createOutputStream(fos);
// 3流的对拷
IOUtils.copyBytes(fis, cos, 1024*1024, false);
// 4关闭资源
IOUtils.closeStream(cos);
IOUtils.closeStream(fos);
IOUtils.closeStream(fis);
}
}
2. Map输出端采用压缩
// 开启map端输出压缩
conf.setBoolean("mapreduce.map.output.compress", true);
// 设置map端输出压缩方式
conf.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class, CompressionCodec.class);
3. Reduce输出端采用压缩
// 设置reduce端输出压缩开启
FileOutputFormat.setCompressOutput(job, true);
// 设置压缩的方式
FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);
五、Yarn资源调度器
1. Yarn的工作机制
工作机制概述:客户端执行job.waitForCompletion,首先判断是localRunner还是YarnRunner(这里只描述YarnRunner的过程)。YarnRunner向ResourceManager申请一个Application,Resourcemanager返回资源提交路径和Job_id,客户端将切片,.xml文件以及jar包提交到该路径。资源提交完毕后,客户端申请运行mrAppMaster。之后,ResourceManager将请求初始化成Task,Task进入资源调度器(常见的调度策略有三种,FIFO,Capacity调度器和公平调度器,详见下文)。随后空闲的Nodemanager从调度器中领取到Task任务,由Container为其分配cpu,ram等计算资源。
该NodeManager从资源提交路径中下载Job资源,根据切片数量,向ResourceManager申请开启相同数量的MapTask。之后,其他空闲的NodeManager领取MapTask,并由Container分配相应的计算资源。最初领取Task的NodeManager,向领取MapTask的NodeManager发送程序启动脚本,开启Map任务,Map任务结束之后,最初领取Task的NodeManager向ResourceManager申请开启ReduceTask,ReduceTask将每一个Map任务执行完的有序数据,按分区拷贝,执行Reduce任务。
2. 资源调度器
Hadoop2.7.2默认的资源调度器是Capacity Scheduler
2.1 FIFO调度策略
遵循队列先进先出。
2.2 Capacity调度策略
容量调度策略是多个FIFO队列的组合,需要注意的是,该调度器会对同一个用户提交的作业所占的资源进行限定,任务来了之后,会选择最闲的队列进入。
2.3 Fair调度策略
公平调度策略需要注意的是调度器对每个Job按照资源缺额进行资源分配,同一个队列中的Job也是并发执行的。
3. 任务推测执行
作业完成时间取决于最慢的任务完成时间,推测执行机制就是为最慢的任务开启备份任务,谁先运行完,采用谁的结果。
推测执行算法原理: