第1章 MapReduce概述
1.1 MapReduce定义
MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。
MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上。
1.2 MapReduce优缺点
1.2.1 优点
1)MapReduce 易于编程
它简单的实现一些接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量廉价的机器上运行。也就是说你写一个分布式程序,跟写一个简单的串行程序是一模一样的。就是因为这个特点使得MapReduce编程变得非常流行。
2)良好的扩展性
当你的计算资源不能得到满足的时候,你可以通过简单的增加机器来扩展它的计算能力。
3)高容错性
MapReduce设计的初衷就是使程序能够部署在廉价的机器上,这就要求它具有很高的容错性。比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败,而且这个过程不需要人工参与,而完全是由Hadoop内部完成的。
4)适合PB级以上海量数据的离线处理
可以实现上千台服务器集群并发工作,提供数据处理能力。
1.2.2 缺点
1)不擅长实时计算
MapReduce无法像MySQL一样,在毫秒或者秒级内返回结果。
2)不擅长流式计算
流式计算的输入数据是动态的,而MapReduce的输入数据集是静态的,不能动态变化。这是因为MapReduce自身的设计特点决定了数据源必须是静态的。
3)不擅长DAG(有向图)计算
多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下,MapReduce并不是不能做,而是使用后,每个MapReduce作业的输出结果都会写入到磁盘,会造成大量的磁盘IO,导致性能非常的低下。
1.3 MapReduce核心思想
(1)分布式的运算程序往往需要分成至少2个阶段。
(2)第一个阶段的MapTask并发实例,完全并行运行,互不相干。
(3)第二个阶段的ReduceTask并发实例互不相干,但是他们的数据依赖于上一个阶段的所有MapTask并发实例的输出。
(4)MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序,串行运行。
总结:分析WordCount数据流走向深入理解MapReduce核心思想。
1.4 MapReduce进程
一个完整的MapReduce程序在分布式运行时有三类实例进程:
(1)MrAppMaster:负责整个程序的过程调度及状态协调。
(2)MapTask:负责Map阶段的整个数据处理流程。
(3)ReduceTask:负责Reduce阶段的整个数据处理流程。
1.5 官方WordCount源码
采用反编译工具反编译源码,发现WordCount案例有Map类、Reduce类和驱动类。且数据的类型是Hadoop自身封装的序列化类型。
1.6 常用数据序列化类型
Java类型 Hadoop Writable类型
Boolean BooleanWritable
Byte ByteWritable
Integer IntWritable
Float FloatWritable
Long LongWritable
Double DoubleWritable
String Text
Map MapWritable
Array ArrayWritable
1.7 WordCount案例实操
1)需求
在给定的文本文件中统计输出每一个单词出现的总次数
(1)输入数据 vim hello.txt ,创建文件后使用如下命令上传至 HDFS
hadoop fs -put hello.txt /input
#在hello.txt文件中添加如下内容
atguigu atguigu
ss ss
cls cls
jiao
banzhang
xue
hadoop
(2)期望输出数据
atguigu 2
banzhang 1
cls 2
hadoop 1
jiao 1
ss 2
xue 1
2)需求分析
按照MapReduce编程规范,分别编写Mapper,Reducer,Driver。
3)环境准备
(1)创建maven工程
(2)在pom.xml文件中添加如下依赖
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.12.0</version> </dependency> <dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-client</artifactId> <version>3.1.3</version> </dependency> </dependencies>
(3)在项目的src/main/resources目录下,新建一个文件,命名为“log4j2.xml”,在文件中填入。
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="error" strict="true" name="XMLConfig"> <Appenders> <!-- 类型名为Console,名称为必须属性 --> <Appender type="Console" name="STDOUT"> <!-- 布局为PatternLayout的方式, 输出样式为[INFO] [2018-01-22 17:34:01][org.test.Console]I'm here --> <Layout type="PatternLayout" pattern="[%p] [%d{yyyy-MM-dd HH:mm:ss}][%c{10}]%m%n" /> </Appender> </Appenders> <Loggers> <!-- 可加性为false --> <Logger name="test" level="info" additivity="false"> <AppenderRef ref="STDOUT" /> </Logger> <!-- root loggerConfig设置 --> <Root level="info"> <AppenderRef ref="STDOUT" /> </Root> </Loggers> </Configuration>
4)编写程序
(1)编写Mapper类
package com.atguigu.mapreduce; 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); } } }
(2)编写Reducer类
package com.atguigu.mapreduce; 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); } }
(3)编写Driver驱动类
package com.atguigu.mapreduce;
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);
}
}
5)在idea中使用Maven进行清理
6)在idea中使用Maven进行打包
7)使用Xftp工具将生成的jar包上传至Linux(可以给jar包重命名),并使用如下命令运行程序操作即可
hadoop jar TestMapReduce.jar com.atguigu.mapreduce.WordcountDriver /input /output
1.8 WordCount案例实操(本地模式--程序员调式专用)
1)直接使用1.7案例中的 WordcountMapper 类与 WordcountReducer 类
2)新建 WordcountDriver2 类
package com.atguigu.mapreduce; 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; import java.io.IOException; public class WordcountDriver2 { public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException { Configuration configuration = new Configuration(); //设置在集群运行的相关参数-设置HDFS,NAMENODE的地址 configuration.set("fs.defaultFS", "hdfs://hadoop102:9820"); //指定MR运行在Yarn上 configuration.set("mapreduce.framework.name","yarn"); //指定MR可以在远程集群运行 configuration.set("mapreduce.app-submission.cross-platform","true"); //指定yarn resourcemanager的位置 configuration.set("yarn.resourcemanager.hostname", "hadoop103"); //创建job Job job = Job.getInstance(configuration); //设置windows本地jar包所在的路径 job.setJar("D:\\Root\\workSpace\\IntelliJ IDEA 2019.2.4\\workSpace\\TestMapReduce\\target\\TestMapReduce-1.0-SNAPSHOT.jar"); //设置 map 和 reduce 类 job.setMapperClass(WordcountMapper.class); job.setReducerClass(WordcountReducer.class); //设置map输出 job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(IntWritable.class); //设置reduce输出 job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); //设置输入输出路径 FileInputFormat.setInputPaths(job,new Path(args[0])); FileOutputFormat.setOutputPath(job,new Path(args[1])); //执行job boolean b = job.waitForCompletion(true); System.out.println(b ? 0 : 1); } }
3)使用Maven进行打包
4)先运行一次WordcountDriver2类 的 main() 方法 ,然后在idea找到该类的配置,点击Edit Configurations...,添加如下参数至相应位置
-DHADOOP_USER_NAME=atguigu
hdfs://hadoop102:9820/input hdfs://hadoop102:9820/output
5)再次运行WordcountDriver2类 的 main() 方法 即可,若集群中已存在/output目录要先将其删除,点击查看结果:http://hadoop102:9870/explorer.html#/
第2章 Hadoop序列化
2.1 序列化概述
2.1.1 什么是序列化
序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁盘中,以及便于进行网络传输。
反序列化就是将收到的字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转化为内存中的对象。
2.1.2 为什么要序列化
一般来说,"活的"对象只生存在内存里,关机断电就没有了。而且"活的"对象只能由本地的进程使用,不能被发送到网络上的另一台计算机。然而序列化可以存储"活的"对象,可以将"活的"对象发送到远程计算机。
2.1.3 为什么不用Java序列化
Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种效验信息,Header,继承体系),不便于在网络中高效传输,所以,Hadoop自己开发了一套序列化机制(Writable)。
Hadoop序列化的特点:
(1)紧凑:高效使用存储空间
(2)快速:读写数据的额外开销小
(3)可扩展:随着通信协议的升级而可升级
(4)互操作:支持多语言的交互
2.2 自定义bean对象实现序列化接口(Writable)
在企业开发中往往常用的基本序列化类型不能满足所有需求,比如在Hadoop框架内部传递一个bean对象,那么该对象就需要实现序列化接口。
具体实现bean对象序列化步骤如下7步。
(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(),可用”\t”分开,方便后续用。
(7)如果需要将自定义的bean放在key中传输,则还需要实现Comparable接口,因为MapReduce框中的Shuffle过程要求对key必须能排序。详见后面排序案例。
@Override public int compareTo(FlowBean o) { // 倒序排列,从大到小 return this.sumFlow > o.getSumFlow() ? -1 : 1; }
2.3 序列化案例实操
1)在D:\io\input目录下创建phone_data.txt文件,并添加如下内容
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
2)新建FlowBean.java
package com.atguigu.mapreduce.writable; import org.apache.hadoop.io.Writable; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; /* * 实现序列化接口 * */ 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; } 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; } public void write(DataOutput out) throws IOException { out.writeLong(upflow); out.writeLong(downflow); out.writeLong(sumflow); } public void readFields(DataInput in) throws IOException { upflow = in.readLong(); downflow = in.readLong(); sumflow = in.readLong(); } }
3)新建FlowMapper.java
package com.atguigu.mapreduce.writable; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Mapper; import java.io.IOException; /* * 继承Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>类 * */ public class FlowMapper extends Mapper<LongWritable,Text,Text,FlowBean> { //封装的key private Text outKey = new Text(); /** * * @param key 读取数据的偏移量 * @param value 读取的一行一行的内容 * @param context 上下文 在这用来将k,v写出去 * @throws IOException * @throws InterruptedException */ @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { //1.将value变成String String line = value.toString(); //2.切割数据 String[] phoneInfo = line.split("\t"); //3.封装K,V //封装的key outKey.set(phoneInfo[1]); //封装的value---可以只创建一个对象每次只是赋值即可 FlowBean outValue = new FlowBean(Long.parseLong(phoneInfo[phoneInfo.length - 3]), Long.parseLong(phoneInfo[phoneInfo.length - 2])); //4.将key,value写出 context.write(outKey,outValue); } }
4)新建FlowReduce.java
package com.atguigu.mapreduce.writable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Reducer; import java.io.IOException; import java.util.Iterator; /* * 继承Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT>类 * */ public class FlowReducer extends Reducer<Text,FlowBean,Text,FlowBean> { /** * * @param key 手机号 * @param values 同一组(相同的手机号)所有的value * @param context 上下文在这用来写出k,v * @throws IOException * @throws InterruptedException */ @Override protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException { long sumUpflow = 0;//总上行流量 long sumDownflow = 0;//总下行流量 //1.遍历所有的value (进来的数据是一组(相同的手机号为一组)) for (FlowBean value : values) { //2.累加上行流量,下行流量 sumUpflow += value.getUpflow(); sumDownflow += value.getDownflow(); } //3.封装k,v FlowBean outValue = new FlowBean(sumUpflow, sumDownflow); //4.将key,value写出 context.write(key,outValue); } }
5)新建FlowDriver.java
package com.atguigu.mapreduce.writable; 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.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import java.io.IOException; /* * 程序入口 * */ public class FlowDriver { public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException { //1.获取Job对象 Job job = Job.getInstance(new Configuration()); //2.配置 job.setMapperClass(FlowMapper.class); job.setReducerClass(FlowReducer.class); //2.1设置Mapper输出的k,v类型 job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(FlowBean.class); //2.2设置最终输出的k,v类型---在这是Reducer输出的k,v类型 job.setOutputKeyClass(Text.class); job.setOutputValueClass(FlowBean.class); //2.3设置输入输出路径 FileInputFormat.setInputPaths(job,new Path("D:\\io\\input\\phone_data.txt")); FileOutputFormat.setOutputPath(job,new Path("D:\\io\\output")); //3.执行job job.waitForCompletion(true); } }
6)运行FlowDriver类中的main()主程序,若D:\io下存在output文件夹,则必须先将其删除(后面的案例也是同理,就不再提醒了)
7)查看output文件夹,打开part-r-00000
第3章 MapReduce框架原理
3.1 InputFormat数据输入
3.1.1 切片与MapTask并行度决定机制
1)问题引出
MapTask的并行度决定Map阶段的任务处理并发度,进而影响到整个Job的处理速度。
思考:1G的数据,启动8个MapTask,可以提高集群的并发处理能力。那么1K的数据,也启动8个MapTask,会提高集群性能吗?MapTask并行任务是否越多越好呢?哪些因素影响了MapTask并行度?
2)MapTask并行度决定机制
数据块:Block是HDFS物理上把数据分成一块一块。
数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。
3.1.2 Job提交流程源码和切片源码详解
1)Job提交流程源码详解
1.job.waitForCompletion(true) //开始提交Job 1.1 state == JobState.DEFINE //当前Job状态的判断 1.2 submit(); //提交Job 1.2.1 setUseNewAPI(); //设置使用新的API 1.2.2建立连接 connect(); // 1.2.2.1创建提交Job的代理 new Cluster(getConfiguration()); // 1.2.2.1.1判断是本地yarn还是远程(如果是本地会创建LocalJobRunner对象, 如果是集群创建YarnRunner) initialize(jobTrackAddr, conf); 1.2.3 提交job submitter.submitJobInternal(Job.this, cluster) //1.2.3.1 checkSpecs(job) //①检查是否设置了输出路径 ②输出路径是否存在 //1.2.3.2 创建给集群提交数据的Stag路径 Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf); //1.2.3.3 获取jobid ,并创建Job路径 JobID jobId = submitClient.getNewJobID(); //1.2.3.4 拷贝jar包到集群 copyAndConfigureFiles(job, submitJobDir); //如果是本地不会上传jar包,如果是集群会上传jar包到HDFS上 rUploader.uploadFiles(job, jobSubmitDir); //1.2.3.5计算切片,生成切片规划文件并写到对应的路径 writeSplits(job, submitJobDir); maps = writeNewSplits(job, jobSubmitDir); input.getSplits(job); //1.2.3.6向Stag路径写XML配置文件 writeConf(conf, submitJobFile); conf.writeXml(out); //1.2.3.7提交Job,返回提交状态(本地:submitClient对象是LocalJobRunner, //集群:submitClient对象是YarnRunner) status = submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());
2)FileInputFormat切片源码解析(input.getSplits(job))
(1)程序先找到数据存储目录
(2)开始遍历处理(规划切片)目录下的每一个文件
(3)遍历第一个文件ss.txt
(a)获取文件大小 fs.sizeOf(ss.txt)
(b)计算切片大小:computeSplitSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M
(c)默认情况下,切片大小=blocksize
(d)开始切,形成第一个切片:ss.txt----0M:128M 第二个切片ss.txt----128M:256M 第三个切片ss.txt----256M:300M
(每次切片时,都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就划分为一块切片)
(e)将切片信息写到一个切片规划文件中
(f)整个切片的核心过程在getSplit()方法中完成
(g)InputSlit只记录了切片的元数据信息,比如起始位置、长度、所在节点列表等
(4)提交切片规划文件到Yarn上,Yarn上的MrAppMaster就可以根据切片规划文件计算开启Map Task个数
3.1.3 FileInputFormat切片机制
1)切片机制
(1)简单地按照文件内容长度进行切片
(2)切片大小,默认等于Block大小
(3)切片时不考虑数据集整体,而是针对每一个文件单独切片
2)案例分析
(1)输入数据有两个文件:file1.txt(320M)、file2.txt(10M)
(2)经过FileInputFormat的切片机制运算后,形成的切片信息如下:
file1.txt.split1-------0M~128M
file1.txt.split2-------128M~256M
file1.txt.split1-------256M~320M
file2.txt.split1-------0M~10M
(a)源码中计算切片大小的公式:
Math.max(minSize,Math.min(maxSize,blocksize));
而 mapreduce.input.fileinputformat.split.minsize=1 (默认值1)
且 mapreduce.input.fileinputformat.split.maxsize=Long.MAXValue (默认值Long.MAXValue)
故,默认情况下切片大小为blocksize
(b)切片大小设置
maxsize(切片最大值):参数若调得比blockSize小,则会让切片变小,而且就等于配置这个参数的值
minsize(切片最小值):参数调得比blockSize大,则可以让切片变得比blockSize还大
(c)获取切片信息API
//获取切片的文件名称
String name = inputSplit.getPath().getName();
//根据文件类型获取切片信息
FileSplit inputSplit = (FileSplit) context.getInputSplit();
3.1.4 CombineTextInputFormat切片机制
框架默认的TextInputFormat切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下。
1)应用场景:CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。
2)虚拟存储切片最大值设置
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m
注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。
3)切片机制
生成切片过程包括:虚拟存储过程和切片过程二部分。
(1)虚拟存储过程:
将输入目录下所有文件大小,依次和设置的setMaxInputSplitSize值比较,如果不大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值2倍,此时将文件均分成2个虚拟存储块(防止出现太小切片)。
例如setMaxInputSplitSize值为4M,输入文件大小为8.02M,则先逻辑上分成一个4M。剩余的大小为4.02M,如果按照4M逻辑划分,就会出现0.02M的小的虚拟存储文件,所以将剩余的4.02M文件切分成(2.01M和2.01M)两个文件。
(2)切片过程:
(a)判断虚拟存储的文件大小是否大于setMaxInputSplitSize值,大于等于则单独形成一个切片。
(b)如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。
(c)测试举例:有4个小文件大小分别为1.7M、5.1M、3.4M以及6.8M这四个小文件,则虚拟存储之后形成6个文件块,大小分别为:1.7M,(2.55M、2.55M),3.4M以及(3.4M、3.4M),最终会形成3个切片,大小分别为:(1.7+2.55)M,(2.55+3.4)M,(3.4+3.4)M
3.1.5 CombineTextInputFormat案例实操
1)需求
将输入的大量小文件合并成一个切片统一处理。
(1)输入数据:准备4个小文件
(2)期望:一个切片处理4个文件
2)实现过程
(1)新建WCMapper.java
package com.atguigu.mapreduce.inputformat; 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; /* 功能 :实现MapTask中需要实现的业务逻辑代码 Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> 泛型: 一组: KEYIN :读取内容的偏移量 VALUEIN :读取的一行一行的内容的类型(字符串) 二组: KEYOUT :输出的key的类型(在这是单词) VALUEOUT :输出的value的类型(在这是单词的数量) */ public class WCMapper extends Mapper<LongWritable, Text,Text, IntWritable> { //封装的key private Text outkey = new Text(); //封装的value private IntWritable outvalue = new IntWritable(); /** * 作用 :该方法会被循环调用,每调用一次就传入一行读取的内容 * @param key 读取内容的偏移量 * @param value 读取的一行一行的内容 * @param context 上下文--在这使用上下文写出key,valuel * @throws IOException * @throws InterruptedException */ @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { //1.将value转成字符串类型 String line = value.toString(); //2.对字符串进行切割 String[] words = line.split(" "); for (String word : words) { //3.封装k,v outkey.set(word); outvalue.set(1); //4.写出去key,value context.write(outkey,outvalue); } } }
(2)新建WCReducer.java
package com.atguigu.mapreduce.inputformat; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Reducer; import java.io.IOException; /* 功能 :实现ReduceTask中需要实现的业务逻辑代码 Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT> 泛型: 第一组: KEYIN :读取的key值的类型(在这表示单词的类型-mapper写出的key的类型) VALUEIN :读取的value值的类型(在这表示单词数量的类型-mapper写出的value的类型) 第二组: KEYOUT : 写出的key值的类型(在这表示单词的类型) VALUEOUT :写出的value值的类型(在这表示单词数量的类型) */ public class WCReducer extends Reducer<Text, IntWritable,Text,IntWritable> { //封装的value private IntWritable outvalue = new IntWritable(); /** * 作用 :该方法会被循环调用,每调用一次就传入一组(在这单词相同为一组)读取的内容 * @param key :读取一组中的数据的key值 * @param values :读取一组中所有value值 * @param context : 上下文 在这用来写出key,value * @throws IOException * @throws InterruptedException */ @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int sum = 0; //1.遍历所有的value for (IntWritable value : values) { //2.对所有value进行累加 int v = value.get();//将IntWritable转成基本数据类型 sum += v; } //3.封装key,value outvalue.set(sum);//可以理解成将基本数据类型转成IntWritable(实际上是赋值) //4.写出key,value context.write(key,outvalue); } }
(3)新建WCDriver.java
package com.atguigu.mapreduce.inputformat; 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.CombineTextInputFormat; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import java.io.IOException; public class WCDriver { public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException { Job job = Job.getInstance(new Configuration()); //设置虚拟切片值的最大值 //CombineTextInputFormat.setMaxInputSplitSize(job,4194304); //设置InputFormat如果不设置默认用的是TextInputFormat //job.setInputFormatClass(CombineTextInputFormat.class); job.setJarByClass(WCDriver.class); job.setMapperClass(WCMapper.class); job.setReducerClass(WCReducer.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(IntWritable.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); FileInputFormat.setInputPaths(job,new Path("D:\\io\\input2\\*")); FileOutputFormat.setOutputPath(job,new Path("D:\\io\\output2")); boolean b = job.waitForCompletion(true); System.out.println(b); } }
(4)不做任何处理,运行WCDriver类的main()方法,观察切片个数为4。
(5)将WCDriver中注释掉的两行代码打开,再次运行程序,并观察运行的切片个数为1。(注意:再次运行需要先将 D:\io 下的output2文件夹删除)
//设置虚拟切片值的最大值:4M CombineTextInputFormat.setMaxInputSplitSize(job,4194304); //设置InputFormat如果不设置默认用的是TextInputFormat job.setInputFormatClass(CombineTextInputFormat.class);
3.1.6 TextInputFormat的KV
一 InputFormat的继承树 |-----InputFormat(抽象类) |------FileInputFormat(抽象类) |-----TextInputFormat(默认使用的InputFormat特点是一行一行的读取数据) |-----CombineFileInputFormat |-----CombineTextInputFormat(可以将多个小文件切成一片) 二 InputFormat(抽象类) //该方法用来获取切片信息 public abstract List<InputSplit> getSplits(JobContext context) throws IOException, InterruptedException; //该方法用来获取RecordReader对象,该对象是用来读取数据的。 public abstract RecordReader<K,V> createRecordReader(InputSplit split,TaskAttemptContext context) throws IOException, InterruptedException; 三 FileInputFormat(抽象类) 1.重写了 getSplits方法,该方法是用来进行切片的方法。 四 TextInputFormat(默认使用的InputFormat) 1.重写了createRecordReader方法 2.createRecordReader方法中返回了LineRecordReader对象,该对象就是真正的用来读取数据的 那个对象。该对象读取数据的特点是一行一行的读取。 3.LineRecordReader是RecordReader的子类。 @Override public RecordReader<LongWritable, Text> createRecordReader(InputSplit split, TaskAttemptContext context) { return new LineRecordReader(recordDelimiterBytes); }
4.TextInputFormat默认是FileInputFormat的实现类,按行读取每条记录。键是存储该行在整个文件中的起始字节偏移量,LongWritabl类型。值是该行的内容,不包括任何行终止符(换行符和回车符),Text类型。
5.以下是一个示例,比如一个分片包含了如下4条文本记录
Rich learing from
Intelligent learning engine
Learning more convenient
From the real demand for more close to the enterprise
6.每条记录表示为以下键/值对
(0,Rich learing from)
(19,Intelligent learning engine)
(47,Learning more convenient)
(72,From the real demand for more close to the enterprise)
3.2 MapReduce工作流程
上面的流程是整个MapReduce最全工作流程,但是Shuffle过程只是从第7步开始到第16步结束,具体Shuffle过程详解,如下:
(1)MapTask收集我们的map()方法输出的kv对,放到内存缓冲区中
(2)从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件
(3)多个溢出文件会被合并成大的溢出文件
(4)在溢出过程及合并的过程中,都要调用Partitioner进行分区和针对key进行排序
(5)ReduceTask根据自己的分区号,去各个MapTask机器上取相应的结果分区数据
(6)ReduceTask会取到同一个分区的来自不同MapTask的结果文件,ReduceTask会将这些文件再进行合并(归并排序)
(7)合并成大文件后,Shuffle的过程也就结束了,后面进入ReduceTask的逻辑运算过程(从文件中取出一个一个的键值对Group,调用用户自定义的reduce()方法)
注意:
(1)Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快。
(2)缓冲区的大小可以通过参数调整,参数:io.sort.mb默认100M。
(3)源码解析流程:
map ---> sort ---> copy ---> sort ---> reduce map阶段:map ---> sort mapPhase = getProgress().addPhase("map", 0.667f); sortPhase = getProgress().addPhase("sort", 0.333f); reduce阶段:copy ---> sort ---> reduce copyPhase = getProgress().addPhase("copy"); sortPhase = getProgress().addPhase("sort"); reducePhase = getProgress().addPhase("reduce");
3.3 Shuffle机制
3.3.1 Shuffle机制
Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle。
3.3.2 Partition分区
1.问题引出
要求将统计结果按照条件输出到不同文件中(分区)
2.默认Partition分区是根据key的hashCode对ReduceTasks个数取模得到的,用户没法控制哪个key存储到哪个分区
/** Partition keys by their {@link Object#hashCode()}. */ @InterfaceAudience.Public @InterfaceStability.Stable public class HashPartitioner<K, V> extends Partitioner<K, V> { /** Use {@link Object#hashCode()} to partition. */ public int getPartition(K key, V value, int numReduceTasks) { return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks; } }
3.自定义Partitioner步骤
(1)自定义类继承Partitioner,重写getPartition()方法
(2)在Job驱动中,设置自定义Partitioner:job.setPartitionerClass(XXX.class);
(3)设置相应的ReduceTask:job.setNumReduceTasks(XXX);
4.分区结论
分区的数量 > ReduceTask的数量 : 会报错。
注意:如果设置了自定义分区类但没有设置ReduceTask的数量那么设置的自定分区类不生效
分区的数量 < ReduceTask的数量
可以但是会造成资源的浪费(没有数据的RT在空跑)
分区的数量 = ReduceTask的数量 (最好的)
3.3.3 Partition分区案例实操
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
(2)期望输出数据
手机号136、137、138、139开头都分别放到一个独立的4个文件中,其他开头的放到一个文件中。
2)在 2.3 案例的基础上,新建MyPartitioner.java
package com.yuange.mapreduce.writable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Partitioner; public class MyPartitioner extends Partitioner<Text,FlowBean> { /** * 返回分区号 * @param text map方法输出的key在这表示手机号 * @param flowBean map方法输出的value在这表示FlowBean类的对象 * @param numPartitions ReduceTask的数量 * @return * * 需求:手机号136、137、138、139开头都分别放到一个独立的4个文件中,其他开头的放到一个文件中。 */ public int getPartition(Text text, FlowBean flowBean, int numPartitions) { String phone = text.toString(); int partition = 4; if (phone.startsWith("136")){ partition = 0; }else if (phone.startsWith("137")){ partition = 1; }else if (phone.startsWith("138")){ partition = 2; }else if (phone.startsWith("139")){ partition = 3; } return partition; } }
3)新建FlowDriver2.java
package com.yuange.mapreduce.writable; 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.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import java.io.IOException; /* * 程序入口 * */ public class FlowDriver2 { public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException { //1.获取Job对象 Job job = Job.getInstance(new Configuration()); //设置自定义分区类 job.setPartitionerClass(MyPartitioner.class); /* 分区的数量 > ReduceTask的数量 : 会报错。 注意:如果设置了自定义分区类但没有设置ReduceTask的数量那么设置的自定分区类不生效 分区的数量 < ReduceTask的数量 可以但是会造成资源的浪费(没有数据的RT在空跑) 分区的数量 = ReduceTask的数量 (最好的) */ //设置ReduceTask的数量(因为有5个分区) job.setNumReduceTasks(5); //2.配置 job.setMapperClass(FlowMapper.class); job.setReducerClass(FlowReducer.class); //2.1设置Mapper输出的k,v类型 job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(FlowBean.class); //2.2设置最终输出的k,v类型---在这是Reducer输出的k,v类型 job.setOutputKeyClass(Text.class); job.setOutputValueClass(FlowBean.class); //2.3设置输入输出路径 FileInputFormat.setInputPaths(job,new Path("D:\\io\\input\\phone_data.txt")); FileOutputFormat.setOutputPath(job,new Path("D:\\io\\output33")); //3.执行job job.waitForCompletion(true); } }
4)运行后查看部分文件是否正确
3.3.4 WritableComparable排序(全排序)
1.排序是MapReduce框架中最重要的操作之一
MapTask和ReduceTask均会对数据按照key排序,该操作属于Hadoop的默认行为,任何应用程序中的数据均会被排序,而不管逻辑上是否需要。
2.默认排序是按照字典顺序排序,且实现该排序的方法是快速排序
对于MapTask,它会将处理的结果暂时放到环形缓冲区,当环形缓冲区使用率到达一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。
对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件,若文件大小超过一定阈值,则溢写在磁盘上,否则存储在内存中,若磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大的文件。若内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。
3.自定义排序
1)需求:对2.3案例产生的结果再次对总流量进行排序,part-r-00000文件数据如下:
13470253144 180 180 360 13509468723 7335 110349 117684 13560439638 918 4938 5856 13568436656 3597 25635 29232 13590439668 1116 954 2070 13630577991 6960 690 7650 13682846555 1938 2910 4848 13729199489 240 0 240 13736230513 2481 24681 27162 13768778790 120 120 240 13846544121 264 0 264 13956435636 132 1512 1644 13966251146 240 0 240 13975057813 11058 48243 59301 13992314666 3008 3720 6728 15043685818 3659 3538 7197 15910133277 3156 2936 6092 15959002129 1938 180 2118 18271575951 1527 2106 3633 18390173782 9531 2412 11943 84188413 4116 1432 5548
2)新建FlowBean.java
package com.atguigu.mapreduce.comparable; import org.apache.hadoop.io.WritableComparable; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; public class FlowBean implements WritableComparable<FlowBean> { private long upflow; private long downflow; private long sumflow; public FlowBean() { } public FlowBean(long upflow, long downflow, long sumflow) { this.upflow = upflow; this.downflow = downflow; this.sumflow = sumflow; } 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 "\t" + upflow + "\t" + downflow + "\t" + sumflow; } public int compareTo(FlowBean o) { return Long.compare(this.sumflow,o.sumflow); } public void write(DataOutput out) throws IOException { out.writeLong(upflow); out.writeLong(downflow); out.writeLong(sumflow); } public void readFields(DataInput in) throws IOException { upflow = in.readLong(); downflow = in.readLong(); sumflow = in.readLong(); } }
3)新建SortDriver.java
package com.atguigu.mapreduce.comparable; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; 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; public class SortDriver { public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException { Job job = Job.getInstance(new Configuration()); // job.setPartitionerClass(SortPartitioner.class); // job.setNumReduceTasks(5); job.setMapperClass(SortMapper.class); job.setReducerClass(SortReducer.class); job.setMapOutputKeyClass(FlowBean.class); job.setMapOutputValueClass(Text.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(FlowBean.class); FileInputFormat.setInputPaths(job,new Path("D:\\io\\input3\\part-r-00000")); FileOutputFormat.setOutputPath(job,new Path("D:\\io\\outputSort4")); job.waitForCompletion(true); } }
4)新建SortMapper.java
package com.atguigu.mapreduce.comparable; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Mapper; import java.io.IOException; public class SortMapper extends Mapper<LongWritable, Text,FlowBean,Text> { @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String phone = value.toString(); String[] split = phone.split("\t"); FlowBean flowBean = new FlowBean(Long.parseLong(split[1]),Long.parseLong(split[2]),Long.parseLong(split[3])); Text text = new Text(split[0]); context.write(flowBean,text); } }
5)新建SortReduce.java
package com.atguigu.mapreduce.comparable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Reducer; import java.io.IOException; public class SortReducer extends Reducer<FlowBean, Text,Text,FlowBean> { @Override protected void reduce(FlowBean key, Iterable<Text> values, Context context) throws IOException, InterruptedException { for (Text value : values) { context.write(value,key); } } }
4.运行SortDriver类中的main()方法,查看结果
3.3.5 WritableComparable排序(区内排序)
1)需求
要求每个省份手机号输出的文件中按照总流量内部排序。
2)需求分析
基于全排序的需求,增加自定义分区类,分区按照省份手机号设置即可
3)新建SortPartitioner.java
package com.atguigu.mapreduce.comparable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Partitioner; public class SortPartitioner extends Partitioner<FlowBean, Text> { @Override public int getPartition(FlowBean flowBean, Text text, int numPartitions) { String phone = text.toString(); int partition = 4; if (phone.startsWith("136")){ partition = 0; }else if (phone.startsWith("137")){ partition = 1; }else if (phone.startsWith("138")){ partition = 2; }else if (phone.startsWith("139")){ partition = 3; } return partition; } }
4)修改SortDriver.java文件中的下面两行代码,将注释去掉即可
// job.setPartitionerClass(SortPartitioner.class); //设置自定义分区类 // job.setNumReduceTasks(5); //设置ReduceTask数目
5)运行SortDriver类中的main(),查看部分结果:----> 即实现了分区,又实现了排序
3.3.6 Combiner合并
1.Combiner是MR程序中Mapper和Reducer之外的一种组件
2.Combiner组件的父类就是Reducer
3.Combiner和Reducer区别在于运行的位置
Combiner是在每一个MapTask所在的节点运行
Reducer是接收全局所有Mapper的输出结果
4.Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减少网络传输量
5.Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combiner的输出k,v应与Reducer的输入k,v类型要对应起来
6.自定义Combiner的实现步骤
1)自定义Combiner继承Reducer,并重写Reduce方法
2)在Job驱动类中设置:job.setCombinerClass(XXX.class)
7.案例实操
1)需求
统计过程中对每一个MapTask的输出进行局部汇总,以减小网络传输量即采用Combiner功能。
(1)新建hello.txt,并输入如下数据
atguigu atguigu
ss ss
cls cls
jiao
banzhang
xue
hadoop
(2)期望输出数据
期望:Combine输入数据多,输出时经过合并,输出数据降低。
2)新建WordCountCombiner.java
package com.atguigu.mapreduce; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Reducer; import java.io.IOException; public class WordCountCombiner extends Reducer<Text, IntWritable,Text,IntWritable> { private int sum; private IntWritable v = new IntWritable(); @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { sum = 0; for (IntWritable value : values) { sum += value.get(); } v.set(sum); context.write(key,v); } }
3)使用1.7案例中的WordcountMapper类、WordcountReduce类
4)新建WordcountDriver3.java,直接运行即可
package com.atguigu.mapreduce; 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; import java.io.IOException; public class WordcountDriver { public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException { Job job = Job.getInstance(new Configuration());
//设置Combiner job.setCombinerClass(WordCountCombiner.class);
job.setJarByClass(WordcountDriver.class); job.setMapperClass(WordcountMapper.class); job.setReducerClass(WordcountReducer.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(IntWritable.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); FileInputFormat.setInputPaths(job,new Path("D:\\io\\input2\\hello.txt")); FileOutputFormat.setOutputPath(job,new Path("D:\\io\\Combiner")); job.waitForCompletion(true); } }
5)结论:使用Combiner合并机制可以减少网络传输资源,提升程序运行效率(不影响最终结果的情况下)
3.4 MapTask工作机制
(1)Read阶段:MapTask通过用户编写的RecordReader,从输入InputSplit中解析出一个个key/value。
(2)Map阶段:该节点主要是将解析出的key/value交给用户编写map()函数处理,并产生一系列新的key/value。
(3)Collect收集阶段:在用户编写map()函数中,当数据处理完成后,一般会调用OutputCollector.collect()输出结果。在该函数内部,它会将生成的key/value分区(调用Partitioner),并写入一个环形内存缓冲区中。
(4)Spill阶段:即“溢写”,当环形缓冲区满后,MapReduce会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。
溢写阶段详情:
步骤1:利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition进行排序,然后按照key进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序。
步骤2:按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N表示当前溢写次数)中。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。
步骤3:将分区数据的元信息写到内存索引数据结构SpillRecord中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件output/spillN.out.index中。
(5)Combine阶段:当所有数据处理完成后,MapTask对所有临时文件进行一次合并,以确保最终只会生成一个数据文件。
当所有数据处理完后,MapTask会将所有临时文件合并成一个大文件,并保存到文件output/file.out中,同时生成相应的索引文件output/file.out.index。
在进行文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并io.sort.factor(默认10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。
让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。
3.5 ReduceTask工作机制
(1)Copy阶段:ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
(2)Merge阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。
(3)Sort阶段:按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可。
(4)Reduce阶段:reduce()函数将计算结果写到HDFS上。
1)设置ReduceTask并行度(个数)
ReduceTask的并行度同样影响整个Job的执行并发度和执行效率,但与MapTask的并发数由切片数决定不同,ReduceTask数量的决定是可以直接手动设置:
// 默认值是1,手动设置为4
job.setNumReduceTasks(4);
2)实验:测试ReduceTask多少合适
(1)实验环境:1个Master节点,16个Slave节点:CPU:8GHZ,内存: 2G
(2)实验结论:
表 改变ReduceTask (数据量为1GB)
MapTask =16 |
||||||||||
ReduceTask |
1 |
5 |
10 |
15 |
16 |
20 |
25 |
30 |
45 |
60 |
总时间 |
892 |
146 |
110 |
92 |
88 |
100 |
128 |
101 |
145 |
104 |
3)注意事项
(1)ReduceTask=0,表示没有Reduce阶段,输出文件个数和Map个数一致
(2)ReduceTask默认值就是1,所以输出文件个数为1
(3)若数据分布不均匀,就有可能在Reduce阶段产生数据倾斜
(4)ReduceTask数量并不是任意设置的,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个ReduceTask
(5)具体多少个ReduceTask,需要根据集群性能而定
(6)若分区数不是1,但是ReduceTask为1,是否执行分区过程?
答:不执行分区过程。因为在MapTask的源码中,执行分区的前提是先判断ReduceNum个数是否大于1,不大于1不会执行
3.6 OutputFormat数据输出
3.6.1 OutputFormat接口实现类
OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了OutputFormat接口,以下是几种常见的OutputFormat实现类
1.文本输出TextOutputFormat
默认的输出格式是TextOutputFormat,它把每条记录写为文本行,它的键和值可以是任意类型,因为TextOutputFormat调用toString()方法把它们转化为字符串
2.SequenceFileOutputFormat
将SequenceFileOutputFormat输出作为后续MapReduce任务的输入,这是一种好的输出格式,它的格式紧凑,很容易被压缩
3.6.2 自定义OutputFormat
1.使用场景
为了实现控制最终文件的输出路径和输出格式,可以自定义OutputFormat
2.自定义OutputFormat步骤
1)自定义一个类继承FileOutputFormat
2)在自定义一个类继承RecordWrier,并具体改写输出数据的方法write()
3)在驱动类中设置不使用ReduceTask:job.setNumReduceTasks(0);
4)在驱动类中设置自OutputFormat定义类:job.setOutputFormatClass(MyOutputFormat.class);
3.实操
1)需求
过滤输入的log日志,包含atguigu的网站输出到 d:/atguigu.log ,不包含atguigu的网站输出到 d:/other.log 。
(1)新建logs.txt文件,并添加如下内容:
http://www.baidu.com http://www.google.com http://cn.bing.com http://www.atguigu.com http://www.sohu.com http://www.sina.com http://www.sin2a.com http://www.sin2desa.com http://www.sindsafa.com
(2)期望输出:数据分别存放在atguigu.log和other.log中,内容分别如下
http://www.atguigu.com
http://cn.bing.com http://www.baidu.com http://www.google.com http://www.sin2a.com http://www.sin2desa.com http://www.sina.com http://www.sindsafa.com http://www.sohu.com
2)新建MyRecordWriter.java
package com.atguigu.mapreduce.outputformat; 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.LongWritable; 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; public class MyRecordWriter extends RecordWriter<LongWritable, Text> { private FSDataOutputStream atguigu; private FSDataOutputStream other; public MyRecordWriter(TaskAttemptContext job) { try { FileSystem fs = FileSystem.get(job.getConfiguration()); atguigu = fs.create(new Path(FileOutputFormat.getOutputPath(job), "atguigu.txt")); other = fs.create(new Path(FileOutputFormat.getOutputPath(job),"other.txt")); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException("创建输出流失败="+e); } } @Override public void write(LongWritable key, Text value) throws IOException, InterruptedException { String line = value.toString() + "\n"; if (line.contains("atguigu")){ atguigu.write(line.getBytes()); }else { other.write(line.getBytes()); } } @Override public void close(TaskAttemptContext context) throws IOException, InterruptedException { IOUtils.closeStreams(atguigu); IOUtils.closeStreams(other); } }
3)新建MyOutputFormat.java
package com.atguigu.mapreduce.outputformat; import org.apache.hadoop.io.LongWritable; 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; public class MyOutputFormat extends FileOutputFormat<LongWritable, Text> { @Override public RecordWriter<LongWritable, Text> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException { return new MyRecordWriter(job); } }
4)新建OutputDriver.java
package com.atguigu.mapreduce.outputformat; 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.mapreduce.Job; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import java.io.IOException; public class OutputDriver { public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException { Job job = Job.getInstance(new Configuration()); //不使用ReduceTask job.setNumReduceTasks(0); //设置OutputForat自定义类 job.setOutputFormatClass(MyOutputFormat.class); job.setOutputKeyClass(LongWritable.class); job.setOutputValueClass(Text.class); FileInputFormat.setInputPaths(job,new Path("D:\\io\\input4\\logs.txt")); FileOutputFormat.setOutputPath(job,new Path("D:\\io\\output55")); job.waitForCompletion(true); } }
5)查看结果
3.7 Join多种应用
3.7.1 Reduce Join
1)工作原理
Map端的主要工作:为来自不同表或文件的 key/value 对打标签以区别不同的来源记录,用连接字段作为key,其余部分和新加的标志作为value,最后进行输出。
Reduce端的主要工作:在每一个分好的组中将来源不同文件的记录进行分开(Map阶段打了标志),然后对文件记录进行合并
2)将如下数据表进行合并,新建 order.txt 和 pd.txt 文件,分别添加如下内容至文件
1001 01 1 1002 02 2 1003 03 3 1004 01 4 1005 02 5 1006 03 6
01 小米 02 华为 03 格力
3)新建 OrderBean.java
package com.atguigu.mapreduce.reducejoin; import org.apache.hadoop.io.WritableComparable; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; public class OrderBean implements WritableComparable<OrderBean> { private String id; private String pname; private String pid; private String amount; public OrderBean() { } public OrderBean(String id, String pname, String pid, String amount) { this.id = id; this.pname = pname; this.pid = pid; this.amount = amount; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getPname() { return pname; } public void setPname(String pname) { this.pname = pname; } public String getPid() { return pid; } public void setPid(String pid) { this.pid = pid; } public String getAmount() { return amount; } public void setAmount(String amount) { this.amount = amount; } @Override public String toString() { return pid + "\t" + pname + "\t" + id + "\t" + amount; } @Override public void write(DataOutput out) throws IOException { out.writeUTF(id); out.writeUTF(pname); out.writeUTF(pid); out.writeUTF(amount); } @Override public void readFields(DataInput in) throws IOException { id = in.readUTF(); pname = in.readUTF(); pid = in.readUTF(); amount = in.readUTF(); } @Override public int compareTo(OrderBean o) { int i = this.getPid().compareTo(o.getPid()); if (i==0){ return o.getPname().compareTo(this.getPname()); } return i; } }
4)新建 ReduceMap.java
package com.atguigu.mapreduce.reducejoin; 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 org.apache.hadoop.mapreduce.lib.input.FileSplit; import java.io.IOException; public class ReduceMapper extends Mapper<LongWritable, Text, OrderBean, NullWritable> { String fileName; OrderBean orderBean; @Override protected void setup(Context context) throws IOException, InterruptedException { FileSplit fs = (FileSplit) context.getInputSplit(); fileName = fs.getPath().getName(); } @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { //1.读取一行行的数据 String line = value.toString(); //2.切分数据 String[] split = line.split("\t"); //3.发送数据 if ("order.txt".equals(fileName)) { orderBean = new OrderBean(split[0], "", split[1], split[2]); }else if ("pd.txt".equals(fileName)){ orderBean = new OrderBean("",split[1],split[0],""); } context.write(orderBean,NullWritable.get()); } }
5)新建 ReduceReduce.java
package com.atguigu.mapreduce.reducejoin; import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.mapreduce.Reducer; import java.io.IOException; import java.util.Iterator; public class ReduceReduce extends Reducer<OrderBean, NullWritable,OrderBean,NullWritable> { @Override protected void reduce(OrderBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException { Iterator<NullWritable> iterator = values.iterator(); iterator.next(); String pname = key.getPname(); while (iterator.hasNext()){ iterator.next(); key.setPname(pname); context.write(key,NullWritable.get()); } } }
6)新建 MyComparator.java
package com.atguigu.mapreduce.reducejoin; import org.apache.hadoop.io.WritableComparable; import org.apache.hadoop.io.WritableComparator; /* 自定义分组方式 1.自定义的类继承WritableComparator 2.重写 compare(WritableComparable a, WritableComparable b) 3.在compare方法中实现需要分组的方式 注意:如果不自定义分组方式那么默认分组方式和排序的方式相同。 */ public class MyComparator extends WritableComparator { public MyComparator(){ //调用父类的构造器 /* protected WritableComparator(Class<? extends WritableComparable> keyClass, boolean createInstances) keyClass : 数据(OrderBean)的运行时类的对象 createInstances : 是否创建实例(对象) */ super(OrderBean.class,true); } /* 指定需要分组的方式(排序的方式--按哪个属性分组就按哪个属性排序) 在框架调用此方法时实际传过来的就是OrderBean的对象 */ @Override public int compare(WritableComparable a, WritableComparable b) { OrderBean o1 = (OrderBean) a; OrderBean o2 = (OrderBean) b; return o1.getPid().compareTo(o2.getPid()); } }
7)新建 ReduceDriver.java
package com.atguigu.mapreduce.reducejoin; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.NullWritable; 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; public class ReduceDriver { public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException { Job job = Job.getInstance(new Configuration()); job.setGroupingComparatorClass(MyComparator.class); job.setMapperClass(ReduceMapper.class); job.setReducerClass(ReduceReduce.class); job.setMapOutputKeyClass(OrderBean.class); job.setMapOutputValueClass(NullWritable.class); job.setOutputKeyClass(OrderBean.class); job.setOutputValueClass(NullWritable.class); FileInputFormat.setInputPaths(job,new Path("D:\\io\\input5\\*")); FileOutputFormat.setOutputPath(job,new Path("D:\\io\\output55")); job.waitForCompletion(true); } }
8)查看结果
9)总结
这种方式合并的操作是在Reduce阶段完成,Reduce端的处理压力太大,Map节点的运算负载很低,资源利用率不高,且在Reduce阶段极易产生数据倾斜。
解决方案:Map端实现数据合并
3.7.2 Map Join
1)使用场景
适用于一张表很小、一张表很大的情况
2)优点
在Map端缓存多张表,提前处理业务逻辑,增加Map端业务,减少Reduce端数据压力,尽可能的减少数据倾斜。
3)思路(数据使用3.7.1案例中的数据即可)
(1)在Map的setup阶段,将文件读取到缓存集合中
(2)在驱动函数中加载缓存:job.addCacheFile(new URI("file://d:/io/input9/pd.txt"))
4)新建OrderBean.java
package com.atguigu.mapreduce.mapjoin; import org.apache.hadoop.io.WritableComparable; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; public class OrderBean implements WritableComparable<OrderBean> { private String pid; private String id; private String pname; private String amount; public OrderBean() { } public OrderBean(String pid, String id, String pname, String amount) { this.pid = pid; this.id = id; this.pname = pname; this.amount = amount; } /* 先按照 pid排序再按照 pname进行排序 1 小米 1001 1 1 1004 1 4 */ @Override public int compareTo(OrderBean o) { int ctpid = this.pid.compareTo(o.pid); if (ctpid == 0){//说明pid相同 //再按照pname排序 return this.pname.compareTo(o.pname); } return ctpid; } /* 序列化 */ @Override public void write(DataOutput out) throws IOException { out.writeUTF(pid); out.writeUTF(id); out.writeUTF(amount); out.writeUTF(pname); } /* 反序列化 */ @Override public void readFields(DataInput in) throws IOException { pid = in.readUTF(); id = in.readUTF(); amount = in.readUTF(); pname = in.readUTF(); } @Override public String toString() { return pid + " " + id + " " + pname + " " + amount; } public String getPid() { return pid; } public void setPid(String pid) { this.pid = pid; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getPname() { return pname; } public void setPname(String pname) { this.pname = pname; } public String getAmount() { return amount; } public void setAmount(String amount) { this.amount = amount; } }
5)新建MapMapper.java
package com.atguigu.mapreduce.mapjoin; 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; import java.util.Map; public class MapMapper extends Mapper<LongWritable, Text,OrderBean, NullWritable> { //缓存数据 private Map<String,String> map = new HashMap<String,String>(); /* 缓存pd.txt */ @Override protected void setup(Context context) { FileSystem fs = null; FSDataInputStream fis = null; BufferedReader br = null; try { //1.创建流 //1.1创建文件系统对象 fs = FileSystem.get(context.getConfiguration()); //1.2创建流 //1.3获取缓存文件的路径 System.out.println("------------------------------------------------------------============================-"); URI[] cacheFiles = context.getCacheFiles(); System.out.println("-------------------------------------------------------------==========================="); fis = fs.open(new Path(cacheFiles[0])); //2.读取数据 //2.1需要一行一行读取内容(字符缓冲流)---1.将字节流转成字符流 2.在字符流外套字符缓冲流 br = new BufferedReader(new InputStreamReader(fis,"utf-8")); String line = ""; while ((line = br.readLine()) != null) { //2.2将数据切割 String[] split = line.split("\t"); //3.将数据存放到map中 map.put(split[0], split[1]); } }catch (Exception e){ e.printStackTrace(); //终止程序 throw new RuntimeException("xxxxxxxxxxxxx"); }finally { //关资源 IOUtils.closeStream(br); IOUtils.closeStream(fis); if( fs != null) { try { fs.close(); } catch (IOException e) { e.printStackTrace(); } } } } /* 只读取order.txt */ @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String line = value.toString(); //切割数据 String[] split = line.split("\t"); //封装K,V OrderBean orderBean = new OrderBean(); orderBean.setId(split[0]); orderBean.setPid(split[1]); orderBean.setAmount(split[2]); orderBean.setPname(map.get(split[1]));//根据key获取对应的value //写出去 context.write(orderBean,NullWritable.get()); } }
6)新建MapDriver.java
package com.atguigu.mapreduce.mapjoin; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.NullWritable; 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 MapDriver { public static void main(String[] args) throws IOException, URISyntaxException, ClassNotFoundException, InterruptedException { Job job = Job.getInstance(new Configuration()); //添加缓存路径 job.addCacheFile(new URI("file:///d:/io/input9/pd.txt")); job.setMapperClass(MapMapper.class); job.setNumReduceTasks(0);//不使用Reducer(不排序) job.setMapOutputKeyClass(OrderBean.class); job.setMapOutputValueClass(NullWritable.class); job.setOutputKeyClass(OrderBean.class); job.setOutputValueClass(NullWritable.class); FileInputFormat.setInputPaths(job, new Path("D:\\io\\input9\\order.txt")); FileOutputFormat.setOutputPath(job,new Path("D:\\io\\input9\\output2323")); job.waitForCompletion(true); } }
3.8 计数器应用
Hadoop为每个作业维护设置若干内置计数器,以描述多项指标。如:某些计数器记录已处理的字节数和记录数,使用户可监控已处理的输入数据量和已产生的输出数据量。
API:
1)采用枚举统计
2)采用计数器组、计数器名称方式统计
3)运行结果在控制台查看
3.9 数据清洗(ETL)
在运行核心业务MapReduce程序之前,往往要先对数据进行清洗,清理掉不符合用户要求的数据。清理的过程往往只需要运行Mapper程序,不需要运行Reduce程序。
1)需求
去除日志中字段个数小于等于11的日志。
(1)数据文件准备:下载web.log文件即可
百度网盘链接地址:https://pan.baidu.com/s/1xWCV0OwuUxkd2wtVqMjlMA
提取码:yuan
(2)期望输出数据
每行字段长度都大于11。
2)需求分析
需要在Map阶段对输入的数据根据规则进行过滤清洗。
3)实现代码
(1)新建LogMapper.java
package com.atguigu.mapreduce.log; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Counter; import org.apache.hadoop.mapreduce.Mapper; import java.io.IOException; public class LogMapper extends Mapper<LongWritable, Text,Text, NullWritable> { Counter success; Counter fail; @Override protected void setup(Context context) throws IOException, InterruptedException { //创建计数器对象 success = context.getCounter("Log", "success"); fail = context.getCounter("Log", "fail"); } @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String line = value.toString(); String[] s = line.split(" "); if (s.length > 11){ context.write(value,NullWritable.get()); success.increment(1); }else { fail.increment(1); } } }
(2)新建LogDriver.java
package com.atguigu.mapreduce.log; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import javax.xml.soap.Text; import java.io.IOException; public class LogDriver { public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException { Job job = Job.getInstance(new Configuration()); job.setMapperClass(LogMapper.class); job.setNumReduceTasks(0); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(NullWritable.class); FileInputFormat.setInputPaths(job,new Path("D:\\io\\input6")); FileOutputFormat.setOutputPath(job,new Path("D:\\io\\output66")); job.waitForCompletion(true); } }
3.10 MapReduce开发总结
1.输入数据接口:InputFormat
(1)默认使用的实现类是:TextInputFormat
(2)TextInputFormat的功能逻辑:一行读一次文本,然后将该行的起始偏移量作为Key,行内容作为value返回
(3)CombineTextInputFormat可以把多个小文件合并成一个切片处理,提高处理效率
2.逻辑处理接口:Mapper
用户根据业务需求实现其中三个方法:map()、setup()、cleanup()
3.Partitioner分区
(1)有默认实现HashPartitioner,逻辑是根据key的哈希值和numReduces来返回一个分区号
(2)若业务上有特别的需求,可以自定义分区
4.Comparable排序
(1)当我们用自定义的对象作为key来输出时,就必须要实现WritableComparable接口,重写compareTo()方法
(2)部分排序(局部排序):对最终输出的每一个文件进行内部排序
(3)全排序:对所有数据进行排序,通常只有一个Reduce
(4)二次排序:排序的条件有两个(案例:Map Join和Reduce Join)
5.Combiner合并
Combiner合并可以提高程序的执行效率,减少IO传输,但是使用时必须不能影响原有的业务处理结果
6.逻辑处理接口:Reducer
用户根据业务需求实现其中三个方法:reduce()、setup()、cleanup()
7.输出数据接口:OutputFormat
(1)默认实现类是TextOutputFormat,逻辑功能是将每一个K,V对向目标文本文件输出一行
(2)将SequenceFileOutputFormat输出作为后续MapReduce任务的输入,这是一种很好的输出格式,因为它格式紧凑,容易被压缩
(3)用户可以自定义OutputFormat
第4章 Yarn资源调度器
Yarn是一个资源调度平台,负责为运算程序提供服务器运算资源,相当于一个分布式的操作系统平台,而MapReduce等运算程序则相当于运行于操作系统之上的应用程序。
4.1 Yarn基本架构
YARN主要由ResourceManager、NodeManager、ApplicationMaster和Container等组件构成。
4.2 Yarn工作机制
(1)MR程序提交到客户端所在的节点。
(2)YarnRunner向ResourceManager申请一个Application。
(3)RM将该应用程序的资源路径返回给YarnRunner。
(4)该程序将运行所需资源提交到HDFS上。
(5)程序资源提交完毕后,申请运行mrAppMaster。
(6)RM将用户的请求初始化成一个Task。
(7)其中一个NodeManager领取到Task任务。
(8)该NodeManager创建容器Container,并产生MRAppmaster。
(9)Container从HDFS上拷贝资源到本地。
(10)MRAppmaster向RM 申请运行MapTask资源。
(11)RM将运行MapTask任务分配给另外两个NodeManager,另两个NodeManager分别领取任务并创建容器。
(12)MR向两个接收到任务的NodeManager发送程序启动脚本,这两个NodeManager分别启动MapTask,MapTask对数据分区排序。
(13)MrAppMaster等待所有MapTask运行完毕后,向RM申请容器,运行ReduceTask。
(14)ReduceTask向MapTask获取相应分区的数据。
(15)程序运行完毕后,MR会向RM申请注销自己。
4.3 作业提交全过程详解
(1)作业提交
第1步:Client调用job.waitForCompletion方法,向整个集群提交MapReduce作业。
第2步:Client向RM申请一个作业id。
第3步:RM给Client返回该job资源的提交路径和作业id。
第4步:Client提交jar包、切片信息和配置文件到指定的资源提交路径。
第5步:Client提交完资源后,向RM申请运行MrAppMaster。
(2)作业初始化
第6步:当RM收到Client的请求后,将该job添加到容量调度器中。
第7步:某一个空闲的NM领取到该Job。
第8步:该NM创建Container,并产生MRAppmaster。
第9步:下载Client提交的资源到本地。
(3)任务分配
第10步:MrAppMaster向RM申请运行多个MapTask任务资源。
第11步:RM将运行MapTask任务分配给另外两个NodeManager,另两个NodeManager分别领取任务并创建容器。
(4)任务运行
第12步:MR向两个接收到任务的NodeManager发送程序启动脚本,这两个NodeManager分别启动MapTask,MapTask对数据分区排序。
第13步:MrAppMaster等待所有MapTask运行完毕后,向RM申请容器,运行ReduceTask。
第14步:ReduceTask向MapTask获取相应分区的数据。
第15步:程序运行完毕后,MR会向RM申请注销自己。
(5)进度和状态更新
YARN中的任务将其进度和状态(包括counter)返回给应用管理器, 客户端每秒(通过mapreduce.client.progressmonitor.pollinterval设置)向应用管理器请求进度更新, 展示给用户。
(6)作业完成
除了向应用管理器请求作业进度外, 客户端每5秒都会通过调用waitForCompletion()来检查作业是否完成。时间间隔可以通过mapreduce.client.completion.pollinterval来设置。作业完成之后, 应用管理器和Container会清理工作状态。作业的信息会被作业历史服务器存储以备之后用户核查。
4.4 资源调度器
目前,Hadoop作业调度器主要有三种:FIFO、Capacity Scheduler和Fair Scheduler。Hadoop3.1.3默认的资源调度器是Capacity Scheduler。
具体设置详见:yarn-default.xml文件
<property> <name>yarn.resourcemanager.scheduler.class</name> <value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler</value> <description>The class to use as the resource scheduler.</description> </property>
1)先进先出调度器(FIFO)
2)容量调度器(Capacity Scheduler)
3)公平调度器(Fair Scheduler)
(1)支持多队列作业,每个队列可以单独配置
(2)同一队列的作业按照其优先级分享整个队列的资源,并发执行
(3)每个作业可以设置最小资源值,调度器会保证作业获得这个最小资源
(1)公平调度器设计目标是:在时间尺度上,所有作业获得公平的资源,某一时刻一个作业应获得资源和实际获得资源的差距叫做“缺额”
(2)调度器会优先为缺额大的作业分配资源
4.5 容量调度器多队列提交案例
4.5.1 需求
Yarn默认的容量调度器是一条单队列的调度器,在实际使用中会出现单个任务阻塞整个队列的情况。同时,随着业务的增长,公司需要分业务限制集群使用率。这就需要我们按照业务种类配置多条任务队列。
4.5.2 配置多队列的容量调度器
默认Yarn的配置下,容量调度器只有一条Default队列。在capacity-scheduler.xml中可以配置多条队列,并降低default队列资源占比:
<!-- 红色是修改的部分 --> <property> <name>yarn.scheduler.capacity.root.queues</name> <value>default,hive</value> <description> The queues at the this level (root is the root queue). </description> </property> <property> <name>yarn.scheduler.capacity.root.default.capacity</name> <value>40</value> </property> <!-- 同时为新加队列添加必要属性:--> <property> <name>yarn.scheduler.capacity.root.hive.capacity</name> <value>60</value> </property> <property> <name>yarn.scheduler.capacity.root.hive.user-limit-factor</name> <value>1</value> </property> <property> <name>yarn.scheduler.capacity.root.hive.maximum-capacity</name> <value>80</value> </property> <property> <name>yarn.scheduler.capacity.root.hive.state</name> <value>RUNNING</value> </property> <property> <name>yarn.scheduler.capacity.root.hive.acl_submit_applications</name> <value>*</value> </property> <property> <name>yarn.scheduler.capacity.root.hive.acl_administer_queue</name> <value>*</value> </property> <property> <name>yarn.scheduler.capacity.root.hive.acl_application_max_priority</name> <value>*</value> </property> <property> <name>yarn.scheduler.capacity.root.hive.maximum-application-lifetime</name> <value>-1</value> </property> <property> <name>yarn.scheduler.capacity.root.hive.default-application-lifetime</name> <value>-1</value> </property>
在配置完成后,重启Yarn,就可以看到两条队列:
4.5.3 向Hive队列提交任务
默认的任务提交都是提交到default队列的。如果希望向其他队列提交任务,需要在Driver中声明(使用1.8案例中的WordcountDriver2.java):
package com.atguigu.mapreduce;
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;
import java.io.IOException;
public class WordcountDriver2 {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Configuration configuration = new Configuration();
//指定提交至哪一个队列
configuration.set("mapred.job.queue.name", "hive");
//设置在集群运行的相关参数-设置HDFS,NAMENODE的地址
configuration.set("fs.defaultFS", "hdfs://hadoop102:9820");
//指定MR运行在Yarn上
configuration.set("mapreduce.framework.name","yarn");
//指定MR可以在远程集群运行
configuration.set("mapreduce.app-submission.cross-platform","true");
//指定yarn resourcemanager的位置
configuration.set("yarn.resourcemanager.hostname", "hadoop103");
//创建job
Job job = Job.getInstance(configuration);
//设置windows本地jar包所在的路径
job.setJar("D:\\Root\\workSpace\\IntelliJ IDEA 2019.2.4\\workSpace\\TestMapReduce\\target\\TestMapReduce-1.0-SNAPSHOT.jar");
//设置 map 和 reduce 类
job.setMapperClass(WordcountMapper.class);
job.setReducerClass(WordcountReducer.class);
//设置map输出
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
//设置reduce输出
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//设置输入输出路径
FileInputFormat.setInputPaths(job,new Path(args[0]));
FileOutputFormat.setOutputPath(job,new Path(args[1]));
//执行job
job.waitForCompletion(true);
}
}
这样,这个任务在集群提交时,就会提交到hive队列:
第5章 常见错误及解决方案
1)导包容易出错。尤其Text和CombineTextInputFormat。
2)Mapper中第一个输入的参数必须是LongWritable或者NullWritable,不可以是IntWritable. 报的错误是类型转换异常。
3)java.lang.Exception: java.io.IOException: Illegal partition for 13926435656 (4),说明Partition和ReduceTask个数没对上,调整ReduceTask个数。
4)如果分区数不是1,但是reducetask为1,是否执行分区过程。答案是:不执行分区过程。因为在MapTask的源码中,执行分区的前提是先判断ReduceNum个数是否大于1。不大于1肯定不执行。
5)在Windows环境编译的jar包导入到Linux环境中运行,hadoop jar wc.jar com.atguigu.mapreduce.wordcount.WordCountDriver /user/atguigu/ /user/atguigu/output
报如下错误:
Exception in thread "main" java.lang.UnsupportedClassVersionError: com/atguigu/mapreduce/wordcount/WordCountDriver : Unsupported major.minor version 52.0
原因是Windows环境用的jdk1.7,Linux环境用的jdk1.8。
解决方案:统一jdk版本。
6)缓存pd.txt小文件案例中,报找不到pd.txt文件
原因:大部分为路径书写错误。还有就是要检查pd.txt.txt的问题。还有个别电脑写相对路径找不到pd.txt,可以修改为绝对路径。
7)报类型转换异常。
通常都是在驱动函数中设置Map输出和最终输出时编写错误。
Map输出的key如果没有排序,也会报类型转换异常。
8)集群中运行wc.jar时出现了无法获得输入文件。
原因:WordCount案例的输入文件不能放用HDFS集群的根目录。
9)出现了如下相关异常
Exception in thread "main" java.lang.UnsatisfiedLinkError: org.apache.hadoop.io.nativeio.NativeIO$Windows.access0(Ljava/lang/String;I)Z at org.apache.hadoop.io.nativeio.NativeIO$Windows.access0(Native Method) at org.apache.hadoop.io.nativeio.NativeIO$Windows.access(NativeIO.java:609) at org.apache.hadoop.fs.FileUtil.canRead(FileUtil.java:977) java.io.IOException: Could not locate executable null\bin\winutils.exe in the Hadoop binaries. at org.apache.hadoop.util.Shell.getQualifiedBinPath(Shell.java:356) at org.apache.hadoop.util.Shell.getWinUtilsPath(Shell.java:371) at org.apache.hadoop.util.Shell.<clinit>(Shell.java:364)
解决方案(一):拷贝hadoop.dll文件到Windows目录C:\Windows\System32。个别同学电脑还需要修改Hadoop源码。
解决方案(二):创建如下包名,并将NativeIO.java拷贝到该包名下
10)自定义Outputformat时,注意在RecordWirter中的close方法必须关闭流资源。否则输出的文件内容中数据为空。