内部变量升级为全局变量,ctrl+alt+f
查看子类 ctrl+h
常用数据序列化类型
集群上测试
MapReduce工作流程
(1)用maven打jar包,需要添加的打包插件依赖
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
注意:如果工程上显示红叉。在项目上右键->maven->Reimport刷新即可。
(2)将程序打成jar包
(3)修改不带依赖的jar包名称为xx.jar,并拷贝该jar包到Hadoop集群的/opt/module/hadoop-3.1.3路径
(4)启动Hadoop集群
[atguigu@hadoop102 hadoop-3.1.3]sbin/start-dfs.sh
[atguigu@hadoop103 hadoop-3.1.3]$ sbin/start-yarn.sh
(5)执行xx程序
[atguigu@hadoop102 hadoop-3.1.3]$ hadoop jar wc.jar
com.atguigu.mapreduce.wordcount.WordCountDriver /user/atguigu/input /user/atguigu/output
红色为全连接名
MapReduce详细工作流程(—)
2,获取切片数量 3,提交split,jar和xml(提交到集群中需要jar文件)4,根据切片数确定MapTask的数量 5,读取数据的方式(按行读之类的)6,进行Map中的逻辑运算 7,向环形缓冲区中写入<k,v>数据 (原数据和索引) partiton为区号,缓冲区写满80%后,反向写,那80%的数据需要经过排序后(对key进行排序,按照字典的顺序排)再写入磁盘 10ReduceTask会抓取到同一个分区的来自不同MapTask的结果文件,ReduceTask会将这些文件再进行合并(归并排序)
MapReduce详细工作流程(二)
Map阶段进行快排和归并,Reduce会进行一次归并
Shuffle机制
Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle(没有Reduce就没有Shuffle)
Partition分区
要求将统计结果按照条件输出到不同文件中(分区)。比如:将统计结果按照手机归属地不同省份输出到不同文件中(分区)
1.默认Partitioner分区
默认分区是根据key的hashCode对numReduceTasks个数取模得到的。用户没法控制哪个key存储到哪个分区。
我们可以再Driver类中改变numReduceTasks的数值
2.自定义Partitioner步骤
(1)z自定义类继承Partitioner,重写getPartition()方法
/**
* 分区是在map之后,reduce之前
*/
public class provincePartitioner extends Partitioner<Text, flowBean> {
@Override
public int getPartition(Text text, flowBean flowBean, int numPartitions) {
//text是手机号
String phone = text.toString();
//得到前面三个号
String prePhone = phone.substring(0, 3);
int partition;
if ("136".equals(prePhone)){
partition=0;
}else if("137".equals(prePhone)){
partition=1;
}else if("138".equals(prePhone)){
partition=2;
}else if("139".equals(prePhone)){
partition=3;
}else {
partition=4;
}
return partition;
}
}
方法体内就是控制分区的代码逻辑 (这里的toString()只是把Text类型变为String类型,方便调用字符串方法)
(2)在Job驱动中,设置自定义Partitioner
job.setPartitionerClass(provincePartitioner.class);//与自定义的分区类进行连接
(3)随后根据自定义Partitioner的逻辑设置相应数量的ReduceTask
job.setNumReduceTasks(5);//与自定义的分区数相同
分区总结
(1)如果ReduceTask的数量>getPartition的结果数,则会多产生几个空的输出
文件part-r-000xX;
(2)如果1<ReduceTask的数量<getPautition的结果数,则有一部分分区数据无处安放,Exception;(3)如果ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个ReduceTask,最终也就只会产生一个结果文件part-r-O0000;
(4)分区号必须从零开始,逐一累加。
WritableComparable排序
MapTask和ReduceTask均会对数据按照key进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。
默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。
对于MapTask,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上。而当整个数据处理完毕后,它会对磁盘上所有文件进行归并排序。(先在内存中快排写到磁盘,结束时在磁盘上归并排序)
对于ReduceTask,当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。
自定义排序WritableComparable
bean对象做为key传输,需要实现WritableComparable接口重写compareTo方法,就可以实现排序
public class flowBean implements WritableComparable<flowBean>
@Override
public int compareTo(flowBean o) {//shuffle底层会调用,这里的flowBean o对象就是原有(原排序队列中)的,this就是传进来的
//总流量的倒序排序
if (this.sumFlow > o.sumFlow) {
return -1;
} else if (this.sumFlow < o.sumFlow) {
return 1;
} else {//这里就是二次嵌套
//按照上行流量的正序排列
if ((this.upFlow > o.upFlow)) {
return 1;
} else if (this.upFlow < o.upFlow) {
return -1;
} else {
return 0;
}
}
}
Combiner合并 (没有Reduce就没必要写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类型要对应起来。
1.自定义Combiner实现步骤
public class wordCountCombiner extends Reducer<Text, IntWritable,Text, IntWritable> {
private IntWritable outV = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
int sum=0;
for (IntWritable value : values) {
sum +=value.get();//因为是value是IntWritable类型,而sum是int类型
}
outV.set(sum);
context.write(key,outV);
}
}
在driver中
job.setCombinerClass(wordCountCombiner.class);
2.将WordcountReducer作为Combiner在WordcountDriver驱动类中指定
因为是Reducer中的逻辑和Combiner中的逻辑相似
// 指定需要使用Combiner,以及用哪个类作为Combiner的逻辑
job.setCombinerClass(WordCountReducer.class);
MapTask工作机制
当第四步的MrAppMaster的时候才开始Map阶段,之前皆不是
第五步读的时候默认的是InputFormat去读,默认的是TextInputFormat(一次读一行),调用RecorderReader方法
溢写阶段详情:先按照分区编号Partition进行排序,然后按照key进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序。按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N表示当前溢写次数)中
Merge阶段:当所有数据处理完成后,MapTask对所有临时文件进行一次合并,以确保最终只会生成一个数据文件
当所有数据处理完后,MapTask会将所有临时文件合并成一个大文件,并保存到文件output/file.out中,同时生成相应的索引文件output/file.out.index,在进行文件合并过程中,MapTask以分区为单位进行合并,让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销
Copy阶段:ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中
Sort阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据(指某一个分区的数据)进行一次归并排序即可
ReduceTask并行度决定机制
MapTask并行度由切片个数决定,切片个数由输入文件和切片规则决定。
ReduceTask的并行度同样影响整个Job的执行并发度和执行效率,但与MapTask的并发数由切片数决定不同,ReduceTask数量的决定是可以直接手动设置
// 默认值是1,手动设置为4
job.setNumReduceTasks(4);
Join应用
1.Reduce Join
Map端的主要工作:为来自不同表或文件的key/value对,打标签以区别不同来源的记录。然后用连接字段作为key,其余部分和新加的标志作为value,最后进行输出。
Reduce端的主要工作:在Reduce端以连接字段作为key的分组已经完成,我们只需要在每一个分组当中将那些来源于不同文件的记录(在Map阶段已经打标志)分开,最后进行合并就ok了。
缺点:这种方式中,合并的操作是在Reduce阶段完成,Reduce端的处理压力太大,Map节点的运算负载则很低,资源利用率不高,且在Reduce阶段极易产生数据倾斜
2.Map Join
使用场景:Map Join适用于一张表十分小、一张表很大的场景
优点:在Map端缓存多张表,提前处理业务逻辑,这样增加Map端业务,减少Reduce端数据的压力,尽可能的减少数据倾斜
具体办法:采用DistributedCache
(1)在Mapper的setup阶段,将文件读取到缓存集合中。
(2)在Driver驱动类中加载缓存。
//缓存普通文件到Task运行节点。
job.addCacheFile(new URI("file:///e:/cache/pd.txt"));
//如果是集群运行,需要设置HDFS路径
job.addCacheFile(new URI("hdfs://hadoop102:8020/cache/pd.txt"));
Driver
package com.xwt.mapreduce.mapJoin;
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.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
public class mapJoinDriver {
public static void main(String[] args) throws IOException, URISyntaxException, InterruptedException, ClassNotFoundException {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(mapJoinDriver.class);
job.setMapperClass(mapJoinMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
//加载缓存数据 在setup方法中获取
job.addCacheFile(new URI("file:///D:/尚硅谷/尚硅谷大数据技术之Hadoop3.x/资料/11_input/tablecache/pd.txt"));
//Map端join的逻辑不需要Reduce阶段,设置reduceTask数量为0
job.setNumReduceTasks(0);
FileInputFormat.setInputPaths(job, new Path("D:\\尚硅谷\\尚硅谷大数据技术之Hadoop3.x\\资料\\11_input\\inputtable2"));
FileOutputFormat.setOutputPath(job, new Path("D:\\尚硅谷\\尚硅谷大数据技术之Hadoop3.x\\资料\\output\\outpuMapJoin"));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
Mapper
package com.xwt.mapreduce.mapJoin;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
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.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.HashMap;
public class mapJoinMapper extends Mapper<LongWritable, Text,Text, NullWritable> {
private HashMap<String, String> pdMap = new HashMap<>();
private Text outK =new Text();
@Override
protected void setup(Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
//获取缓存的文件,并把文件内容封装到集合 缓存pd.txt
//01 小米
//02 华为
//03 格力
URI[] cacheFiles = context.getCacheFiles();//获取缓存文件的地址
FileSystem fs = FileSystem.get(context.getConfiguration());
FSDataInputStream fis = fs.open(new Path(cacheFiles[0]));
//从流中读取数据
BufferedReader reader = new BufferedReader(new InputStreamReader(fis, "UTF-8"));
String line; //StringUtils导common下的包
while (StringUtils.isNotEmpty(line=reader.readLine())){ //获取一行数据,如果不为空
//切割
String[] fields = line.split("\t");
//赋值
pdMap.put(fields[0],fields[1]);
}
//关流
IOUtils.closeStream(reader);
}
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
//处理order.txt
//1001 01 1
//1002 02 2
String line = value.toString();
String[] fields = line.split("\t");
//获取pid
String pname = pdMap.get(fields[1]);
//获取订单id和订单数量 封装
outK.set(fields[0]+"\t"+pname+"\t"+fields[2]);
context.write(outK,NullWritable.get());
}
}
MapReduce开发总结
1)输入数据接口:InputFormat
(1)默认使用的实现类是:TextInputFormat
(2)TextInputFormat的功能逻辑是:一次读一行文本,然后将该行的起始偏移量作为key,行内容作为value返回。
(3)CombineTextInputFormat可以把多个小文件合并成一个切片处理,提高处理效率
2)逻辑处理接口:Mapper
用户根据业务需求实现其中三个方法:setup() 初始化 map()用户的业务逻辑 cleanup ()关闭资源
3)Partitioner分区
(1)有默认实现 HashPartitioner,逻辑是根据key的哈希值和numReduces来返回一个分区号;key.hashCode()&Integer.MAXVALUE % numReduces
(2)如果业务上有特别的需求,可以自定义分区。
public class provincePartitioner extends Partitioner<Text, flowBean>
//重写
public int getPartition(Text text, flowBean flowBean, int numPartitions)
4)Comparable排序
(1)当我们用自定义的对象作为key来输出时,就必须要实现WritableComparable接口,重写其中的compareTo()方法。
5)Combiner合并
Combiner合并可以提高程序执行效率,减少IO传输。但是使用时必须不能影响原有的业务处理结果。
6)逻辑处理接口:Reducer
用户根据业务需求实现其中三个方法:setup() 初始化 reduce()用户的业务逻辑 cleanup ()关闭资源
7)输出数据接口:OutputFormat
(1)默认实现类是TextOutputFormat,功能逻辑是:将每一个KV对,向目标文本文件输出一行。
(2)用户还可以自定义OutputFormat。
//reducer输出的数据就到了outputFormat中来了
public class logOutputFormat extends FileOutputFormat<Text, NullWritable> {
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
//需要RecordWriter就给出啥
LogRecordWriter lrw = new LogRecordWriter(job);//没有这个类就创建这个类,alt+enter 参数job就能和驱动产生联系
return lrw;
}
}
public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
private FSDataOutputStream atguiguOut;
private FSDataOutputStream otherOut;
public LogRecordWriter(TaskAttemptContext job) {
//创建两条流
//正常开发中,异常是需要向上抛出的,最后由应用自己去处理,或者这里先try catch一下,然后包装成一个新的异常向外抛出
try {
FileSystem fs = FileSystem.get(job.getConfiguration());//与job中的配置建立联系
atguiguOut = fs.create(new Path("D:\\尚硅谷\\尚硅谷大数据技术之Hadoop3.x\\资料\\atguigu.log"));
otherOut = fs.create(new Path("D:\\尚硅谷\\尚硅谷大数据技术之Hadoop3.x\\资料\\other.log"));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void write(Text key, NullWritable value) throws IOException, InterruptedException {
//这是真正写出的方法
String log = key.toString();
if(log.contains("atguigu")){
atguiguOut.writeBytes(log +"\n");
}else {
otherOut.writeBytes(log +"\n");
}
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
//关流
IOUtils.closeStream(atguiguOut);
IOUtils.closeStream(otherOut);
}
}
MapReduce数据压缩
map端开启压缩 在Driver中写入
Configuration conf = new Configuration();
//开启map端输出压缩
conf.setBoolean("mapreduce.map.output.cpmpress",true);
//设置map端输出压缩方式
conf.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class, CompressionCodec.class);
Job job = Job.getInstance(conf);
reduce端开启压缩 在Driver中写入
//设置reduce端输出压缩开启
FileOutputFormat.setCompressOutput(job,true);
//设置压缩方式
//FileOutputFormat.setOutputCompressorClass(job,BZip2Codec.class);
FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);
map端的压缩方式和reduce端的压缩方式毫无关系