今日内容:1) 综合案例: 需求一 和 需求二: 需求一: 统计求和需求 排序需求 需求二: 求共同好友2) MapReduce性能优化策略 --- 简单了解3) yarn基本介绍4) yarn的基本架构介绍及其相关的组件说明 --理解即可5) yarn的运行流程 -- 清楚 掌握 最好记忆住6) yarn的三种调度器 -- 理解
1) 综合案例: 需求一 实现完成 参考代码即可
2) 综合案例: 需求二 需求: 求俩俩之间的共同好友有那些
A:B,C,D,F,E,O B:A,C,E,K C:F,A,D,I D:A,E,F,L E:B,C,D,M,L F:A,B,C,D,E,O,M G:A,C,D,E,F H:A,C,D,E,O I:A,O J:B,O K:A,C,D L:D,E,F M:E,F,G O:A,H,I,J
请使用MR 计算求 共同好友
核心一句话: 相同key会被发往同一个reduce. 相同key的value会被合并为一个集合
A: B C D B: A D E C: B A D: E C E: D B
先求一下: 那些人存在其他用户好友列表中 map阶段: k2 放置好友 v2放置用户 B A C A D A A B D B E B B C A C E D C D D E B E
分组后的结果: A : [B,C] B : [A,C,E] C : [A,D] D : [A,B,E] E : [B,D] reduce阶段: k3 v3 A B-C B E-C-A C A-D D A-B-E E B-D
接下来 求俩俩共同好友: 在切割后, 对集合数据进行排序操作 map阶段: k2(俩俩用户) v2(好友) B-C A C-E B A-E B A-C B A-D C A-B D A-E D B-E D B-D E
分组: B-C [A] C-E [B] A-E [B,D] A-C [B] A-D [C] A-B [D] B-E [D] B-D [E]
reduce阶段: k3 v3 A-B D A-C B A-D C A-E B-D B-C A B-D E B-E D C-E B
3. yarn基本介绍: 统一资源调度平台 资源: 内存 CPU 磁盘
yarn管理资源: 内存 和 CPU hdfs管理资源: 磁盘
在实际生产中, 每一个datanode上都会运行着一个nodemanager
datanode: 出磁盘 nodeManager: 出 内存 和 CPU
hadoop1.x中: MapReduce平台 jobTracker : 主节点 仅支持单节点部署 功能: 任务的接收, 资源分配 以及任务分配 TaskTracker: 从节点 可以部署多台 功能: 负责任务最终执行
存在什么弊端: 1) jobTracker 存在单点故障问题 2) jobTracker工作任务有点繁多, 容易导致jobTracker宕机问题 3) 整个MR平台 只能允许 mr任务, 无法支持其他组件的计算任务 hadoop2.x: yarn平台
resourceManager: 主节点 支持有多台的 功能: 任务的接收 ,资源分配 nodeManager: 从节点 支持多台 功能: 负责任务最终执行 application master(app Master): 每一个job任务. 都会启动一个 appMaster 主要负责: 当前job任务的 任务分配工作, 以及负责向resourcemanager申请资源 并对当前这个任务, 进行全生命周期管理
带来的好处: 1) 解决了 原有的单点故障问题 2) 降低了 主节点的任务量, 从而提升其可用性 3) yarn平台支持可以运行多种计算程序: MR spark storm ....4) yarn运行流程: 理解是前提, 记忆不强制 1) 由客户端向RM提交任务(MR,Spark...) 2) RM接收任务, 并根据任务随机找一台NM,启动AppMaster ,通知以container方式 container: 资源信息容器 节点信息 内存信息 CPU信息 运行: AppMaster 阿善看到 3) 指定NM启动AppMaster, 启动后和RM保持心跳机制, 用于报告当前已经启动了,并且通过心跳来传递相关信息 4) 根据RM给定任务信息. 根据任务信息, 对任务进行分配, 主要会分配出要启动多少个Map和多少个reduce, 以及 每个map 和每个reduce需要使用多大资源空间, 然后将资源申请相关信息发送给RM(心跳发送) 5) RM接收到资源申请信息后, 将申请信息交给内部资源调度器, 由资源调度器, 根据相关的资源调度方案, 进行资源 分配即可, 如果当下没有资源, 在此处等待 注意: 资源并不是一次性全部给到AppMaster, 一般会采用极可能满足方案, 如果满足不了, 会先给与一定资源 进行运行, 如果空闲资源连一个container都不足, 就会将这些资源挂起, 等待资源充足 6) AppMaster基于心跳机制, 不断询问RM是否已经准备好了资源了, 如果发现已经准备好了, 然后直接将资源信息获取 7) 根据资源信息说明,到指定的NM上启动container资源容器, 开始运行相关任务 8) NM 接收启动的信息后, 开始启动执行, 此时会和AppMaster以及RM保持心跳连接 将任务的相关信息根据心跳通知AppMaster 将资源的使用信息根据心跳通知RM 9) 当NM运行完成后, 会通知AppMaster并将资源使用完成情况通知给RM 10) AppMaster告知给RM 任务已经运行完成了, RM 回收资源, 通知AppMaster进行自毁即可 注意: 当NM在运行过程中, 如果发生错误了, 此时RM会立即将资源回收,此时AppMaster就需要重新和RM申请资源
5) yarn的资源调度器: scheduler FIFO scheduler : 先进先出调度方案 当一个调度任务进入到调度器之后, 那么调度器会优先满足第一个MR任务全部资源,此时就有可能将资源全部都获取到了 导致后续的任务, 本身的运行时间很短, 但是由于第一个MR将资源全部抢走了, 导致后续任务全部等待
此种调度器在生产中 一般不会使用, 因为 生产中yarn平台不是你自己的 capacity scheduler : 容量调度器 此种调度器是有 Yahoo 提供一种调度方案, 同时也是当下Apache版本的hadoop默认调度方案
可以预先分配出多个队列, 相当于对资源进行预先的划分
每个队列, 可以指定占用多少的百分比的资源, 从而保证, 大的任务可以有单独的队列来运行, 并且小的任务, 也可以正常的运行
fair scheduler : 公平调度器 不需要预先的对资源进行划分操作
可以保证每个MR任务都可以获取到相对均衡的资源来运行MR任务,从而让各个MR都有资源可运行
目前 是 CDH版本的hadoop默认采用调度方案
在生产中, 一般采用的公平调度器, 采用版本一般都是CDH商业化版本hadoop
今日内容:1) MapReduce的combinner(规约)局部聚合 -- 操作2) MapReduce的并行度机制 -- 理解3) map的工作机制 -- 重点理解 记忆4) reduce的工作机制 -- 重点理解 记忆5) MapReduce的自定义分组操作 -- 操作 6) MapReduce的高阶练习 -- 操作
1) mapreduce的combiner: combiner是MapReduce中优化步骤, 主要利用局部聚合的方式,来提升性能: combiner主要是在map端实施局部聚合的工作, 将map的输出结果先进行一次局部合并, 然后发到reduce进行最终合并的操作 通过此操作, 可以减少 从map端 将数据发往reduce端的中间数据传输量, 从而提升MR的执行效率
请问 所有的业务 都可以利用这个局部聚合来优化嘛? 比如说: 求平均数
1,2,3,4,5,6,7 --> 正确: 4
比如现在有三个map, 每个map读取两个数据 正常逻辑, 每个map将数据都发送给reduce, 由reduce进行求平均数
如果想使用combiner, 在每一个map先求出一个平均数, 然后发送到reduce, 在统一求这个平均数 map1: 1,2 --> 1.5
map2: 3,4 --> 3.5
map3: 5,6,7 --> 6
reduce: 1.5,3.5,6 -->3...
并不是所有的业务都可以进行局部聚合的操作, 在实施局部聚合时候, 要校验是否会对最终结果有影响, 如果有, 那么不允许使用局部聚合
通过观察发现 大多数的combiner局部聚合过程, 与reduce执行一模一样的, 只不过, combiner是对每一个map进行聚合的操作 而reduce, 是对多个map的结果, 统一进行聚合的操作
如何实现一个combiner组件呢? 实现方式与 reduce是一模一样的
区别: 在驱动类中, 将combinner组件, 设置combinner位置: job.setCombinerClass( combiner的组件)
需求: 有 三个书架 ,每个书架上都有5本书, 要求 统计出 每种分类的下有几本书??
1号书架 2号书架 3号书架 <
分类: 计算机, 历史, 武林秘籍
map :k2(Text) v2(IntWritable) 类别 1
输出结果: 计算机 1 计算机 1 武林秘籍 1 历史 1 武林秘籍 1
计算机 1 武林秘籍 1 武林秘籍 1 计算机 1 计算机 1
计算机 1 计算机 1 武林秘籍 1 武林秘籍 1 历史 1 中间经过 排序 分组之后: 计算机 [1,1,1,1,1,1,1] 武林秘籍 [1,1,1,1,1,1] 历史 [1,1] reduce: k2 v2 类别 1
结果: k3(Text) v3(IntWritable) 计算机 : 7 历史 : 2 武林秘籍: 6
2) MapReduce的并行度机制: 何为并行度: 在执行MR的时候, map运行的数量 以及 reduce运行数量 有谁来决定的问题 map运行的数量 取决于 读取文件的大小, 默认情况下, 是按照 128M 一个Map来处理, 同时每一个文件至少会有一个map来读取 思考: 如果一个文件300M 存储在HDFS中, 会被分成三个Block块(物理划分), 如果通过MR 读取这个文件, 请问, 是否是一个block对应一个map呢? MR 对整个文件进行读取工作, 然后对整个文件进行逻辑划分(FileSplit)---文件切片 计算切片的规则: Math.max(minSize, Math.min(maxSize, blockSize)); --默认大小 等于 blockSize的大小 而blockSize大小 等于 128M minSize : 默认值为 0 maxSize : 默认值为 Long最大值
如果想要调整切片的大小: 可以通过 inputFormat 来进行调整: TextInputFormat.setMinInputSplitSize(job,值); 调大切片的大小 TextInputFormat.setMaxInputSplitSize(job,值); 调小切片的大小
reduce运行数量: 一般有人为来设定的, 当业务需求要求结果是多个文件的时候, 这个时候一般就要设置reduce的数量 一个reduce就会输出一个聚合结果, 多个reduce就会输出多个聚合结果 如果只有一个reduce, 一般聚合结果, 最终聚合结果, 而如何使用多个reduce, 将聚合结果, 拆分多半
同时reduce数量 还取决于分区的数量, 如果进行自定义分区, 分区数量要和reduce是相等的
3) map阶段的工作机制: 1) 通过FileInputFormat 读取目标地址的数据, 通过FileInputFormat对目标数据进行FileSplit的逻辑分片数据, 最终会启动多少个mapTask取决于 FileSplit的逻辑分片的数量, 读取后, 将数据转换为K1和 v1 2) 自定义map逻辑, 接收k1和v1, 将k1和v1 转换为 k2和v2: 前序读取一行, 调用一次map逻辑, 输出k2和v2 3) 进行分区操作: map端输出一次k2和v2, 那么分区逻辑就会对k2进行分区操作, 默认采用HASH取模计算法, 计算后, 对k2进行分区标记 然后将标记好的k2和v2写入到 环形缓冲区 4) 随着不断写入到环形缓冲区, 环形缓冲区默认的大小为100M, 当达到缓冲区的80%的时候, MR会启动一个溢写线程, 将80%数据写出到磁盘中, 形成一个 临时文件, 在执行溢写过程中, 还会对k2数据进行排序操作, 如果设置了规约, 那么也就会在这个时候执行了 5) 不断的写入, 不断的溢写 , 产生多个临时文件, 当mapTask执行完成后, 会将最后的缓冲区的数据一并写出磁盘上, 然后开始进行合并操作. 保证一个 MapTask 只会产生一个最终的结果文件, 在合并的过程中, 同样也会进行排序, 并且有规约依然还会执行操作 6) 当mapTask的结果文件形成后, 静静等待reduceTask的拉取即可
shuffle中: 分区 排序 规约 属于 map端的shuffle过程
4) reduce阶段的工作流程: 1) 当map执行完成后, reduce开始执行拉取工作, 每一个reduce到mapTask执行的结果文件中, 拉取属于自己分区的数据 2) 将数据首先拉取到内存中, 当内存写满后, 将数据溢写到磁盘上, 形成多个临时文件, 在溢写的时候, 依然会对数据进行排序 3) 当整个拉取工作结束后, 对多个临时文件数据执行merge(合并)操作 , 将多个临时文件合并为一个结果文件, 同时合并过程依然排序 4) 对合并后的结果文件, 开始执行分组操作, 分好一组数据后, 就会调用一次reduce的逻辑, reduce接收k2和v2的数据, 将其转换为k3和v3 5) reduce每输出一次 k3和v3, 就会被FileOutPutFormat 接收到, 然后将数据追加到结果文件上 6) 当整个reducer将所有的分组数据都处理完成, 结果文件, 也就产生了...
shuffle中: 分组 属于 reduce的shuffle过程
发现不管是 map阶段 还是reduce阶段, 大量进行 磁盘 到 内存 内存到磁盘 ... 相关的IO操作, 主要目的能够解决处理海量数据计算问题
带来好处: 能够处理海量的数据 带来的弊端: 造成大量的磁盘IO工作 导致效率比较低
相关的配置: 配置 默认值 解释 mapreduce.task.io.sort.mb 100 设置环型缓冲区的内存值大小 mapreduce.map.sort.spill.percent 0.8 设置溢写的比例 mapreduce.cluster.local.dir ${hadoop.tmp.dir}/mapred/local 溢写数据目录 mapreduce.task.io.sort.factor 10 设置一次合并多少个溢写文件
5) 自定义分组:
如何自定义分组: 1) 创建一个类, 让其继承 WritableComparator 2) 重写空参构造方法 在方法调用父类中构建: super(k2.class ,true) 3) 重写 compare方法: 方法中有两个参数 分别代表分区内相邻的两个数据 4) 在compare方法中, 实现自定义的比较是否相等的逻辑即可 5) 告知给驱动类
wordcount进阶版
今日内容:1) MapReduce的基本介绍 --理解2) MapReduce的编程规范步骤: 八大步骤 --记忆3) MapReduce的入门代码: wordCount --- 要求 独立完成操作4) MapReduce计算程序如何运行: 二种 -- 要求掌握5) MapReduce的分区的实现操作 -- 要求 掌握 操作6) MapReduce的排序和序列化 -- 要求 掌握 操作
1) MapReduce的基本介绍: 分布式计算框架 思想: 分而治之
比如说: 搬砖, 图书馆数书, 数学中计算从1~100和
MapReduce整个计算程序一般可以分为二个部分: map: 负责 分 的 过程
reduce: 负责 合 的过程
MapReduce是一个分布式运算程序的编程框架, 核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在Hadoop集群上。
MR既然是一个分布式计算的程序, 那么必然要有输入 和 输出
实现MapReduce的时候, 需要编写 map 和 reduce map和reduce在进行数据传输的时候, 都是采用 kv键值对的方式来传输的
2) MapReduce的编程规范步骤 : 八步 天龙八步 map阶段: 2步 1) 读取数据, 将读取过来的数据转换为 k1 和 v1 2) 自定义map逻辑, 将 k1 和 v1 转换为 k2 和 v2 shuffle阶段: 4步 接收 map阶段的k2和 v2 转换为 new k2和 v2 3) 分区 : 将相同的k2的数据发往同一个reduce 4) 排序 : 针对 k2 的数据 进行排序工作 5) 规约 : mr中优化步骤, 用于进行局部聚合功能, 一般是可以省略的 6) 分组 : 将相同k2的数据分成一组, 将v2的数据合并形成一个集合 reduce阶段: 2步 7) 自定义reduce逻辑, 接收k2 和 v2的数据, 将其转换为 k3 和 v3 8) 输出结果, 将k3 和 v3 输出到目的地
而 k2 就是 整个MR的灵魂
//key : 这一行的起始点在文件中的偏移量
3) MapReduce的入门代码: wordCount案例 map程序代码: package com.itheima.wordCount;
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;
public class WordCountMapperTask extends Mapper
private IntWritable v2 = new IntWritable(1); private Text k2 = new Text(); @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1. 从 v1 中将一行数据获取出来 String line = value.toString();
if(line != null && !"".equals(line)){ // 千千万万 不要丢掉 ! 号 //2. 对数据进行切割
String[] words = line.split(" ");
//3. 遍历这个数组 for (String word : words) {
//4. 往外输出 k2 和 v2 k2.set(word); context.write(k2,v2);
}
} } } reduce的程序代码: package com.itheima.wordCount;
import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class WordCountReducerTask extends Reducer
@Override protected void reduce(Text key, Iterable
//1. 遍历 v2的数据 int i = 0; for (IntWritable value : values) { i += value.get(); } //2. 写出去 v3.set(i); context.write(key,v3); } } 驱动类: 完整版本: package com.itheima.wordCount;
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configured; 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.TextInputFormat; import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat; import org.apache.hadoop.util.Tool; import org.apache.hadoop.util.ToolRunner;
public class WordCountDriverMain extends Configured implements Tool{ @Override public int run(String[] args) throws Exception { //1. 获取一个 job 任务对象 Job job = Job.getInstance(super.getConf(), "wordCountMR");
//2. 执行封装任务: 天龙八步
//2.1: 指定输入类, 并指定从那里读取 // TextInputFormat 专门用来读取文本文件的, 默认是一行一行的读取, KEY:LongWritable v: Text job.setInputFormatClass(TextInputFormat.class); TextInputFormat.addInputPath(job,new Path("file:///E:\\传智工作\\上课\\北京大数据47期\\day04_hadoop_MR\\资料\\wordcount\\input\\wordcount.txt"));
//2.2: 封装mapTask , 并设置map输出的k2和 v2 job.setMapperClass(WordCountMapperTask.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(IntWritable.class);
//2.3: 分区, 排序 , 规则 , 分组 --如果都是默认, 此处不需要编写
//2.7: 封装reduceTask , 并设置reduce输出 k3 和 v3 job.setReducerClass(WordCountReducerTask.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class);
//2.8: 指定输出类,并指定输出的目的地 job.setOutputFormatClass(TextOutputFormat.class); // 输出的最后目录, 必须不能不存在, 如果存在, 直接报错 TextOutputFormat.setOutputPath(job,new Path("file:///E:\\传智工作\\上课\\北京大数据47期\\day04_hadoop_MR\\资料\\wordcount\\output1"));
//3. 提交任务, 执行操作 boolean flag = job.waitForCompletion(true);
//4. 返回执行的状态 return flag ? 0 : 1 ; }
public static void main(String[] args) throws Exception { int i = ToolRunner.run(new Configuration(), new WordCountDriverMain(), args); System.exit(i); //退出程序, 并给出 状态码: 0 正常 1失败
} } 驱动类: 精简写法 package com.itheima.wordCount;
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configured; 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.TextInputFormat; import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat; import org.apache.hadoop.util.Tool; import org.apache.hadoop.util.ToolRunner;
public class WordCountDriverMain2{
public static void main(String[] args) throws Exception {
//1. 获取一个 job 任务对象 Job job = Job.getInstance(new Configuration(), "wordCountMR");
//2. 执行封装任务: 天龙八步
//2.1: 指定输入类, 并指定从那里读取 // TextInputFormat 专门用来读取文本文件的, 默认是一行一行的读取, KEY:LongWritable v: Text job.setInputFormatClass(TextInputFormat.class); TextInputFormat.addInputPath(job,new Path("file:///E:\\传智工作\\上课\\北京大数据47期\\day04_hadoop_MR\\资料\\wordcount\\input\\wordcount.txt"));
//2.2: 封装mapTask , 并设置map输出的k2和 v2 job.setMapperClass(WordCountMapperTask.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(IntWritable.class);
//2.3: 分区, 排序 , 规则 , 分组 --如果都是默认, 此处不需要编写
//2.7: 封装reduceTask , 并设置reduce输出 k3 和 v3 job.setReducerClass(WordCountReducerTask.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class);
//2.8: 指定输出类,并指定输出的目的地 job.setOutputFormatClass(TextOutputFormat.class); // 输出的最后目录, 必须不能不存在, 如果存在, 直接报错 TextOutputFormat.setOutputPath(job,new Path("file:///E:\\传智工作\\上课\\北京大数据47期\\day04_hadoop_MR\\资料\\wordcount\\output2"));
//3. 提交任务, 执行操作 boolean flag = job.waitForCompletion(true);
System.exit(flag ? 0 :1); //退出程序, 并给出 状态码: 0 正常 1失败
} }
4) mapreduce的运行方式: 4.1: 本地运行 : 右键执行即可
4.2: 集群化运行: 第一步: 保证MR的驱动类中, 必须添加如下信息: job.setJarByClass(WordCountDriverMain.class); 如果没有添加 ,在执行MR的时候, 会报 找不到主类的错误 第二步: 修改输入 输出的路径为 hdfs的路径 简单写法: 使用args即可, 在后期执行的时候, 动态传入 第三步: 保证在pom依赖中 添加了打包插件:
插件作用: 在执行package打包的时候, 可以将项目中所依赖的其他的jar包, 打入到当前的jar包
何时使用: 如果运行环境中, 如果有非hadoop相关的jar的包时候, 建议使用 fatjar包(胖jar包)
如果运行环境中, 如果都是hadoop相关的jar包的时候, 可以不使用farjar, 使用小的jar包即可 如何在打包的时候, 排除掉不需要的jar包: 在dependency标签中, 添加如下的配置即可
5) MapReduce的分区的实现操作: MR分区指的是什么呢? 在shuffle中第一个步骤 就是分区, 目的将相同的k2 发送到同一个reduce中
分区的数量 取决于有多少个reduce
假设如果有二个reduce , 如何将数据分发给这二个reduce呢, 并且还要保证相同的k2肯定在一个分区中? 解决思路: 基于 hashCode取模计算法
原因如下: 因为 相同的数据.hashcode这个方法, 得出的值必然是相同的的 然后使用这个hashcode的值 对 分区的数量取模计算. 得出余数是多少, 分发给那个分区即可
MR默认是如何解决这个分发方式呢? 如出一辙
需求: 将提供彩票数据中, 大于等于14的数据, 放置到一个文件中, 并且将小于14的数据, 放置到另一个文件找那个, 使用MR实现
通过分析发现, 原有默认分区方案, 无法解决上述问题, 需要进行自定义分区
如何进行自定义分区: 抄 抄 HashPartitioner默认分区类 1) 创建一个类, 继承 Partitioner (指定k2和v2的类型 ) 2) 重写一个 getPartition 方法: 方法中有三个参数: 参数1: k2 的数据 参数2: v2 的数据 参数3: reduce的数量 3) 在 getPartition方法中, 编写自定义分区方案 使用v2实现 自定义实现: 如果数据大于等于14 返回 0 如果数据小于14 返回 1 4) 将自定义分区类, 告知给MR (驱动类)
实现代码: 自定义分区的代码: package com.itheima.partition;
import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Partitioner;
public class MyPartition extends Partitioner
int code = v2.get();
if(code >= 14 ){
return 0; }else { return 1; }
} }
map的代码: package com.itheima.partition;
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;
public class PartitionMapperTask extends Mapper
//1. 读取一行数据 String line = value.toString();
if(line!=null && !"".equals(line)){ //2. 切割操作: String[] fileds = line.split("\t");
//3. 从中获取开奖号 int code = Integer.parseInt(fileds[5]);
//4. 写出去 k2.set(line); v2.set(code); context.write(k2,v2); } } } reducer代码: package com.itheima.partition;
import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class PartitionReducerTask extends Reducer
@Override protected void reduce(Text key, Iterable
for (IntWritable value : values) { context.write(key,NullWritable.get()); }
} } 驱动类: package com.itheima.partition;
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IntWritable; 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.TextInputFormat; import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
public class PartitionDriverMain {
public static void main(String[] args) throws Exception {
//1. 获取job对象 Job job = Job.getInstance(new Configuration(), "partitionMR");
// 如果要集群化运行, 必须设置相关信息 job.setJarByClass(PartitionDriverMain.class);
//2. 封装 八大步骤: //2.1: 设置输入类, 指定输入的路径 job.setInputFormatClass(TextInputFormat.class); TextInputFormat.addInputPath(job,new Path("file:///E:\\传智工作\\上课\\北京大数据47期\\day04_hadoop_MR\\资料\\自定义分区\\input"));
//2.2: 设置mapper类, 设置其输出 k2 和 v2的类型 job.setMapperClass(PartitionMapperTask.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(IntWritable.class);
//2.3: 设置 分区 job.setPartitionerClass(MyPartition.class);
//2.4: 设置 排序, 规约, 分组
//2.7: 设置reduce类, 设置其输出 k3 和 v3的类型 job.setReducerClass(PartitionReducerTask.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(NullWritable.class);
//2.8: 设置输出类, 设置输出路径 job.setOutputFormatClass(TextOutputFormat.class); TextOutputFormat.setOutputPath(job,new Path("file:///E:\\传智工作\\上课\\北京大数据47期\\day04_hadoop_MR\\资料\\自定义分区\\output"));
//2.9: 设置 reduce的数量 job.setNumReduceTasks(2);
//3. 提交任务 boolean flag = job.waitForCompletion(true);
//4. 退出程序 给出退出程序的状态码 System.exit(flag ? 0 : 1);
}
}
7) MapReduce的排序和序列化:
MR默认是如何排序的呢? 按照 k2 进行 升序排序 , 如果是字符串, 统一按照字典序 进行排序 如果 是数值类型, 按照数值类型升序排序
100 110 120 210 8 7 4 300 230
请按照字典序升序排列: 100,110,120,210,230,300,4,7,8
在哪里实现这种排序呢? 排序时在每种数据类型中, 有一个比较器, 基于这个比较器 由MR计算框架, 来进行比较排序操作
序列化: 主要目的 帮助进行网络传输功能, 以及可以进行持久化到磁盘的过程 序列化做了什么事情: 将一个对象, 转换为 字节(二进制)数据
在java中, 专门提供了一个用于实现序列化的接口: serializable
在hadoop中, 压根就没有使用这个接口来实现序列化的操作: 原因如下: java中提供的serializable 太重了, 除了会将本生数据转换二进制, 同时还是将其继承体系等等转换为二进制 hadoop在设计之初, 也考虑到这个问题, 所以专门写了一个用于 HADOOP中的序列化的接口: Writable Writable 此序列化接口, 只会将本生数据进行传输, 不会附带继承关系, 校验文件...
如果想要自己实现一个数据类型, 需要实现 Writable接口
如果在此基础上, 还想实现排序的功能: 请使用: WritableComparable 或者 实现两个接口: Writable 和 Comparable 需求: a 1 a 9 b 3 a 7 b 8 b 10 a 5 a 9
要求 先按照第一列进行排序(倒序), 如果第一列数据相同, 按照第二列进行排序(升序)
结果: b 3 b 8 b 10 a 1 a 5 a 7 a 9 a 9
具体实现代码: 自定义数据类型: package com.itheima.sort;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput; import java.io.DataOutput; import java.io.IOException;
public class MyWritable implements WritableComparable
private String first ; private Integer second ;
public String getFirst() { return first; }
public void setFirst(String first) { this.first = first; }
public Integer getSecond() { return second; }
public void setSecond(Integer second) { this.second = second; }
@Override public String toString() { return first +"\t"+second; }
// 序列化: 从对象 --> 二进制过程 // 注意: 序列化的顺序 和 反序列化顺序 必须 一致 否则 无法完成正常工作 @Override public void write(DataOutput out) throws IOException {
out.writeUTF(first); out.writeInt(second);
} // 反序列化: 二进制 --> 对象 @Override public void readFields(DataInput in) throws IOException { this.first = in.readUTF(); this.second = in.readInt();
}
// Comparable 接口中需要实现的比较器 // 定义如何比较问题: // 技巧: 使用 this.compareTo(参数)的数据 此时就是升序 , 如果使用参数.compareTo(this) 倒序 @Override public int compareTo(MyWritable o) { int i = o.first.compareTo(this.first);
if(i == 0 ){ //
如果指定的数与参数相等返回0。
如果指定的数小于参数返回 -1。
如果指定的数大于参数返回 1。
int i1 = this.second.compareTo(o.second); return i1; }
return i; }
} 自定义map类: package com.itheima.sort;
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.IOException;
public class SortMapperTask extends Mapper
@Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1. 获取一行数据 String line = value.toString(); if(line!=null && !"".equals(line)){ //2. 进行切割操作: String[] fileds = line.split("\t");
//3. 封装 k2的数据 MyWritable k2 = new MyWritable(); k2.setFirst(fileds[0]); k2.setSecond(Integer.parseInt(fileds[1]));
//4. 写出去 context.write(k2,NullWritable.get());
}
} } 自定义reduce类: package com.itheima.sort;
import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class SortReducerTask extends Reducer
@Override protected void reduce(MyWritable key, Iterable
for (NullWritable value : values) { context.write(key,value); }
} } 自定义驱动类: package com.itheima.sort;
import com.itheima.partition.MyPartition; import com.itheima.partition.PartitionMapperTask; import com.itheima.partition.PartitionReducerTask; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IntWritable; 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.TextInputFormat; import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
public class SortDriverMain {
public static void main(String[] args) throws Exception {
//1. 获取job对象 Job job = Job.getInstance(new Configuration(), "SortDriverMain");
// 如果要集群化运行, 必须设置相关信息 job.setJarByClass(SortDriverMain.class);
//2. 封装 八大步骤: //2.1: 设置输入类, 指定输入的路径 job.setInputFormatClass(TextInputFormat.class); TextInputFormat.addInputPath(job,new Path("file:///E:\\传智工作\\上课\\北京大数据47期\\day04_hadoop_MR\\资料\\排序\\input"));
//2.2: 设置mapper类, 设置其输出 k2 和 v2的类型 job.setMapperClass(SortMapperTask.class); job.setMapOutputKeyClass(MyWritable.class); job.setMapOutputValueClass(NullWritable.class);
//2.3: 设置 分区 // job.setPartitionerClass(MyPartition.class);
//2.4: 设置 排序, 规约, 分组
//2.7: 设置reduce类, 设置其输出 k3 和 v3的类型 job.setReducerClass(SortReducerTask.class); job.setOutputKeyClass(MyWritable.class); job.setOutputValueClass(NullWritable.class);
//2.8: 设置输出类, 设置输出路径 job.setOutputFormatClass(TextOutputFormat.class); TextOutputFormat.setOutputPath(job,new Path("file:///E:\\传智工作\\上课\\北京大数据47期\\day04_hadoop_MR\\资料\\排序\\output"));
//3. 提交任务 boolean flag = job.waitForCompletion(true);
//4. 退出程序 给出退出程序的状态码 System.exit(flag ? 0 : 1);
}
}
一、MapReduce分布式计算
1. MapReduce计算模型介绍
1.1. 理解MapReduce思想
MapReduce思想在生活中处处可见。或多或少都曾接触过这种思想。MapReduce的思想核心是“分而治之”,适用于大量复杂的任务处理场景(大规模数据处理场景)。即使是发布过论文实现分布式计算的谷歌也只是实现了这种思想,而不是自己原创。
Map负责“分”,即把复杂的任务分解为若干个“简单的任务”来并行处理。可以进行拆分的前提是这些小任务可以并行计算,彼此间几乎没有依赖关系。
Reduce负责“合”,即对map阶段的结果进行全局汇总。
这两个阶段合起来正是MapReduce思想的体现。
图:MapReduce思想模型
还有一个比较形象的语言解释MapReduce:
我们要数图书馆中的所有书。你数1号书架,我数2号书架。这就是“Map”。我们人越多,数书就更快。
现在我们到一起,把所有人的统计数加在一起。这就是“Reduce”。
1.2. Hadoop MapReduce设计构思
MapReduce是一个分布式运算程序的编程框架,核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在Hadoop集群上。
既然是做计算的框架,那么表现形式就是有个输入(input),MapReduce操作这个输入(input),通过本身定义好的计算模型,得到一个输出(output)。
对许多开发者来说,自己完完全全实现一个并行计算程序难度太大,而MapReduce就是一种简化并行计算的编程模型,降低了开发并行应用的入门门槛。
Hadoop MapReduce构思体现在如下的三个方面:
l 如何对付大数据处理:分而治之
对相互间不具有计算依赖关系的大数据,实现并行最自然的办法就是采取分而治之的策略。并行计算的第一个重要问题是如何划分计算任务或者计算数据以便对划分的子任务或数据块同时进行计算。不可分拆的计算任务或相互间有依赖关系的数据无法进行并行计算!
l 构建抽象模型:Map和Reduce
MapReduce借鉴了函数式语言中的思想,用Map和Reduce两个函数提供了高层的并行编程抽象模型。
Map: 对一组数据元素进行某种重复式的处理;
Reduce: 对Map的中间结果进行某种进一步的结果整理。
MapReduce中定义了如下的Map和Reduce两个抽象的编程接口,由用户去编程实现:
map: (k1; v1) → [(k2; v2)]
reduce: (k2; [v2]) → [(k3; v3)]
Map和Reduce为程序员提供了一个清晰的操作接口抽象描述。通过以上两个编程接口,大家可以看出MapReduce处理的数据类型是
l 统一构架,隐藏系统层细节
如何提供统一的计算框架,如果没有统一封装底层细节,那么程序员则需要考虑诸如数据存储、划分、分发、结果收集、错误恢复等诸多细节;为此,MapReduce设计并提供了统一的计算框架,为程序员隐藏了绝大多数系统层面的处理细节。
MapReduce最大的亮点在于通过抽象模型和计算框架把需要做什么(what need to do)与具体怎么做(how to do)分开了,为程序员提供一个抽象和高层的编程接口和框架。程序员仅需要关心其应用层的具体计算问题,仅需编写少量的处理应用本身计算问题的程序代码。如何具体完成这个并行计算任务所相关的诸多系统层细节被隐藏起来,交给计算框架去处理:从分布代码的执行,到大到数千小到单个节点集群的自动调度使用。
2. MapReduce编程规范及示例编写
2.1. 编程规范
MapReduce 的开发一共有八个步骤, 其中 Map 阶段分为2个步骤,Shuffle 阶段 4 个步骤,Reduce 阶段分为2个步骤
Map阶段2个步骤
- 设置 InputFormat 类, 读取输入文件内容,对输入文件的每一行,解析成key、value对(K1和V1)。
- 自定义map方法,每一个键值对调用一次map方法,将第一步的K1和V1结果转换成另外的 Key-Value(K2和V2)对, 输出结果。
Shuffle 阶段 4 个步骤
3. 对map阶段输出的k2和v2对进行分区 将相同的k2发往同一个reduce
4. 对不同分区的数据按照相同的Key排序
5. (可以省略)对数据进行局部聚合, 降低数据的网络拷贝
6. 对数据进行分组, 相同Key的Value放入一个集合中,得到K2和[V2] 相同的k2分成一组 将v2合并成一个集合
Reduce 阶段 2 个步骤
7、对map任务的输出,按照不同的分区,通过网络copy到不同的reduce节点。自定义reduce逻辑,接收k2和v2的数据 将其转换为k3和v38、对多个map任务的输出进行合并、排序。编写reduce方法,在此方法中将K2和[V2]进行处理,转换成新的key、value(K3和V3)输出,并把reduce的输出保存到文件中。
2.2. 编程步骤:
用户编写的程序分成三个部分:Mapper,Reducer,Driver(提交运行mr程序的客户端)
Mapper
(1) 自定义类继承Mapper类
(2) 重写自定义类中的map方法,在该方法中将K1和V1转为K2和V2
(3) 将生成的K2和V2写入上下文中
Reducer
(1) 自定义类继承Reducer类
(2) 重写Reducer中的reduce方法,在该方法中将K2和[V2]转为K3和V3
(3) 将K3和V3写入上下文中
Driver
整个程序需要一个Drvier来进行提交,提交的是一个描述了各种必要信息的job对象
(1)定义类,编写main方法
(2)在main方法中指定以下内容:
1、创建建一个job任务对象
2、指定job所在的jar包
3、指定源文件的读取方式类和源文件的读取路径
4、指定自定义的Mapper类和K2、V2类型
5、指定自定义分区类(如果有的话)
6、指定自定义分组类(如果有的话)
7、指定自定义的Reducer类和K3、V3的数据类型
8、指定输出方式类和结果输出路径
9、将job提交到yarn集群
2.3. WordCount示例编写
需求:在一堆给定的文本文件中统计输出每一个单词出现的总次数
第一步:数据准备
- 创建一个新的文件
cd /export/server vim wordcount.txt |
- 向其中放入以下内容并保存
hello,world,hadoop hive,sqoop,flume,hello kitty,tom,jerry,world hadoop |
- 上传到 HDFS
hdfs dfs -p -mkdir /input/wordcount hdfs dfs -put wordcount.txt /input/wordcount |
第四步: 导入相关依赖
|
第三步:代码编写
(1)定义一个mapper类
//首先要定义四个泛型的类型 //keyin: LongWritable valuein: Text //keyout: Text valueout:IntWritable public class WordCountMapper extends Mapper<LongWritable, Text, Text, Writable>{ //map方法的生命周期: 框架每传一行数据就被调用一次 //key : 这一行的起始点在文件中的偏移量 //value: 这一行的内容 @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { //拿到一行数据转换为string String line = value.toString(); //将这一行切分出各个单词 String[] words = line.split(" "); //遍历数组,输出<单词,1> for(String word:words){ context.write(new Text(word), new LongWritable (1)); } } } |
(2)定义一个reducer类
public class WordCountReducer extends Reducer<Text,LongWritable,Text,LongWritable> { //生命周期:框架每传递进来一个kv 组,reduce方法被调用一次 @Override protected void reduce(Text key, Iterable<LongWritable > values, Context context) throws IOException, InterruptedException { //定义一个计数器 int count = 0; //遍历这一组kv的所有v,累加到count中 for(LongWritable value:values){ count += value.get(); } context.write(key, new LongWritable (count)); } } |
(3)定义一个Driver主类,用来描述job并提交job
public class WordCountRunner{ //该方法用于指定一个job任务 public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); //1:创建一个job任务对象,并指定job的名字 Job job = Job.getInstance(conf, "wordcount"); //2:指定job所在的jar包 job.setJarByClass(WordCountRunner.class); //3:指定源文件的读取方式类和源文件的读取路径 job.setInputFormatClass(TextInputFormat.class); TextInputFormat.addInputPath(job, new Path("file:///D:\\input\\wordcount")); //4:指定自定义的Mapper类和K2、V2类型 job.setMapperClass(WordCountMapper.class); //设置K2类型 job.setMapOutputKeyClass(Text.class); //设置V2类型 job.setMapOutputValueClass(LongWritable.class); //分区和分组采用默认方式 //5:指定自定义的Reducer类和K3、V3的数据类型 job.setReducerClass(WordCountReducer.class); //设置K3的类型 job.setOutputKeyClass(Text.class); //设置V3的类型 job.setOutputValueClass(LongWritable.class); //6:指定输出方式类和结果输出路径 job.setOutputFormatClass(TextOutputFormat.class); //设置输出的路径 TextOutputFormat.setOutputPath(job, new Path("file:///D:\\output\\wordcount")); //7:将job提交给yarn集群 boolean bl = job.waitForCompletion(true); System.exit(bl?0:1); } } |
3. MapReduce程序运行模式
3.1. 本地运行模式
(1)mapreduce程序是被提交给LocalJobRunner在本地以单进程的形式运行
(2)而处理的数据及输出结果可以在本地文件系统,也可以在hdfs上
(3)本地模式非常便于进行业务逻辑的调试
3.2. 集群运行模式
(1)将mapreduce程序提交给yarn集群,分发到很多的节点上并发执行
(2)处理的数据和输出结果应该位于hdfs文件系统
(3)提交集群的实现步骤:
1、将Driver主类代码中的输入路径和输出路径修改为HDFS路径
TextInputFormat.addInputPath(job, new Path("hdfs://node1:8020/input/wordcount")); TextOutputFormat.setOutputPath(job, new Path("hdfs://node1:8020/output/wordcount")); |
2、将程序打成JAR包,然后在集群的任意一个节点上用hadoop命令启动
hadoop jar wordcount.jar cn.itcast.WordCountDriver |
4. 深入MapReduce
4.1. MapReduce的输入和输出
MapReduce框架运转在
一个MapReduce作业的输入和输出类型如下图所示:可以看出在整个标准的流程中,会有三组
4.2. MapReduce的处理流程解析
4.3. Mapper任务执行过程详解
l 第一阶段是把输入目录下文件按照一定的标准逐个进行逻辑切片,形成切片规划。默认情况下,Split size = Block size。每一个切片由一个MapTask处理。
l 第二阶段是对切片中的数据按照一定的规则解析成
l 第三阶段是调用Mapper类中的map方法。上阶段中每解析出来的一个
l 第四阶段是按照一定的规则对第三阶段输出的键值对进行分区。默认是只有一个区。分区的数量就是Reducer任务运行的数量。默认只有一个Reducer任务。
l 第五阶段是对每个分区中的键值对进行排序。首先,按照键进行排序,对于键相同的键值对,按照值进行排序。比如三个键值对<2,2>、<1,3>、<2,1>,键和值分别是整数。那么排序后的结果是<1,3>、<2,1>、<2,2>。如果有第六阶段,那么进入第六阶段;如果没有,直接输出到文件中。
l 第六阶段是对数据进行局部聚合处理,也就是combiner处理。键相等的键值对会调用一次reduce方法。经过这一阶段,数据量会减少。本阶段默认是没有的。
4.4. Reducer任务执行过程详解
l 第一阶段是Reducer任务会主动从Mapper任务复制其输出的键值对。Mapper任务可能会有很多,因此Reducer会复制多个Mapper的输出。
l 第二阶段是把复制到Reducer本地数据,全部进行合并,即把分散的数据合并成一个大的数据。再对合并后的数据排序。
l 第三阶段是对排序后的键值对调用reduce方法。键相等的键值对调用一次reduce方法,每次调用会产生零个或者多个键值对。最后把这些输出的键值对写入到HDFS文件中。
在整个MapReduce程序的开发过程中,我们最大的工作量是覆盖map方法和覆盖reduce方法。
5. MapReduce分区
5.1. 分区概述
在 MapReduce 中, 通过我们指定分区, 会将同一个分区的数据发送到同一个Reduce当中进行处理。例如: 为了数据的统计, 可以把一批类似的数据发送到同一个 Reduce 当中, 在同一个 Reduce 当中统计相同类型的数据, 就可以实现类似的数据分区和统计等
其实就是相同类型的数据, 有共性的数据, 送到一起去处理, 在Reduce过程中,可以根据实际需求(比如按某个维度进行归档,类似于数据库的分组),把Map完的数据Reduce到不同的文件中。分区的设置需要与ReduceTaskNum配合使用。比如想要得到5个分区的数据结果。那么就得设置5个ReduceTask。
需求:将以下数据进行分开处理
详细数据参见partition.csv 这个文本文件,其中第五个字段表示开奖结果数值,现在需求将15以上的结果以及15以下的结果进行分开成两个文件进行保存
5.2. 分区步骤
1. 定义 Mapper
这个 Mapper 程序不做任何逻辑, 也不对 Key-Value 做任何改变, 只是接收数据, 然后往下发送
public class MyMapper extends Mapper<LongWritable,Text,Text,NullWritable>{ @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { context.write(value,NullWritable.get()); } } |
2. 自定义Partitioner
主要的逻辑就在这里, 这也是这个案例的意义, 通过 Partitioner 将数据分发给不同的 Reducer
/** * 这里的输入类型与我们map阶段的输出类型相同 */ public class MyPartitioner extends Partitioner<Text,NullWritable>{ /** * 返回值表示我们的数据要去到哪个分区 * 返回值只是一个分区的标记,标记所有相同的数据去到指定的分区 */ @Override public int getPartition(Text text, NullWritable nullWritable, int i) { String result = text.toString().split("\t")[5]; if (Integer.parseInt(result) > 15){ return 1; }else{ return 0; } } } |
3. 定义 Reducer 逻辑
这个 Reducer 也不做任何处理, 将数据原封不动的输出即可
public class MyReducer extends Reducer<Text,NullWritable,Text,NullWritable> { @Override protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException { context.write(key,NullWritable.get()); } } |
4. 主类中设置分区类和ReduceTask个数
public class PartitionRunner extends Configured implements Tool { public static void main(String[] args) throws Exception{ //1:创建job任务对象 Job job = Job.getInstance(super.getConf(), "partition_maperduce"); //2:指定job所在的jar包 job.setJarByClass(PartitionRunner.class); //3:指定源文件的读取方式类和源文件的读取路径 job.setInputFormatClass(TextInputFormat.class); TextInputFormat.addInputPath(job, new Path("file:///D:\\input\\partition")); //4:指定自定义的Mapper类和K2、V2类型 job.setMapperClass(PartitionMapper.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(NullWritable.class); //5:指定分区类 job.setPartitionerClass(MyPartitioner.class); //6:指定自定义的Reducer类和K3、V3的数据类型 job.setReducerClass(PartitionerReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(NullWritable.class); //7、设置ReduceTask的个数 job.setNumReduceTasks(2); //8、指定输出方式类和结果输出路径 job.setOutputFormatClass(TextOutputFormat.class); TextOutputFormat.setOutputPath(job, new Path("file:///D:\\out\\partition_out6")); //将job提交给yarn集群 boolean bl = job.waitForCompletion(true); System.exit(bl?0:1); } } |
6. MapReduce的排序和序列化
6.1. 概述
序列化(Serialization)是指把结构化对象转化为字节流。
反序列化(Deserialization)是序列化的逆过程。把字节流转为结构化对象。
当要在进程间传递对象或持久化对象的时候,就需要序列化对象成字节流,反之当要将接收到或从磁盘读取的字节流转换为对象,就要进行反序列化。
Java的序列化(Serializable)是一个重量级序列化框架,一个对象被序列化后,会附带很多额外的信息(各种校验信息,header,继承体系…),不便于在网络中高效传输;所以,hadoop自己开发了一套序列化机制(Writable),精简,高效。不用像java对象类一样传输多层的父子关系,需要哪个属性就传输哪个属性值,大大的减少网络传输的开销。
Writable是Hadoop的序列化格式,hadoop定义了这样一个Writable接口。
一个类要支持可序列化只需实现这个接口即可。
public interface Writable { void write(DataOutput out) throws IOException; void readFields(DataInput in) throws IOException; } |
另外 Writable 有一个子接口是 WritableComparable, WritableComparable 是既可实现序列化, 也可以对key进行比较, 我们这里可以通过自定义 Key 实现 WritableComparable 来实现我们的排序功能.
// WritableComparable分别继承Writable和Comparable public interface WritableComparable<T> extends Writable, Comparable<T> { } //Comparable public interface Comparable<T> { int compareTo(T var1); } |
Comparable接口中的comparaTo方法用来定义排序规则,用于将当前对象与方法的参数进行比较。
例如:o1.compareTo(o2);
如果指定的数与参数相等返回0。
如果指定的数小于参数返回 -1。
如果指定的数大于参数返回 1。
返回正数的话,当前对象(调用compareTo方法的对象o1)要排在比较对象(compareTo传参对象o2)后面,返回负数的话,放在前面。
6.2. 需求
数据格式如下
a 1 a 9 b 3 a 7 b 8 b 10 a 5
要求:
第一列按照字典顺序进行排列
第一列相同的时候, 第二列按照升序进行排列
6.3. 分析
实现自定义的bean来封装数据,并将bean作为map输出的key来传输
MR程序在处理数据的过程中会对数据排序(map输出的kv对传输到reduce之前,会排序),排序的依据是map输出的key。所以,我们如果要实现自己需要的排序规则,则可以考虑将排序因素放到key中,让key实现接口:WritableComparable,然后重写key的compareTo方法。
6.4. 实现
6.5. 自定义类型和比较器
public class SortBean implements WritableComparable<SortBean>{ private String word; private int num; public String getWord() { return word; } public void setWord(String word) { this.word = word; } public int getNum() { return num; } public void setNum(int num) { this.num = num; } @Override public String toString() { return word + "\t"+ num ; } //实现比较器,指定排序的规则 /* 规则: 第一列(word)按照字典顺序进行排列 // aac aad 第一列相同的时候, 第二列(num)按照升序进行排列 */ /* a 1 a 5 b 3 b 8 */ @Override public int compareTo(SortBean sortBean) { //先对第一列排序: Word排序 int result = this.word.compareTo(sortBean.word); //如果第一列相同,则按照第二列进行排序 if(result == 0){ return this.num - sortBean.num; } return result; } //实现序列化 @Override public void write(DataOutput out) throws IOException { out.writeUTF(word); out.writeInt(num); } //实现反序列 @Override public void readFields(DataInput in) throws IOException { this.word = in.readUTF(); this.num = in.readInt(); } } |
6.6. 编写Mapper代码
public class SortMapper extends Mapper<LongWritable,Text,SortBean,NullWritable> { /* map方法将K1和V1转为K2和V2: K1 V1 0 a 3 5 b 7 ---------------------- K2 V2 SortBean(a 3) NullWritable SortBean(b 7) NullWritable */ @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { //1:将行文本数据(V1)拆分,并将数据封装到SortBean对象,就可以得到K2 String[] split = value.toString().split("\t"); SortBean sortBean = new SortBean(); sortBean.setWord(split[0]); sortBean.setNum(Integer.parseInt(split[1])); //2:将K2和V2写入上下文中 context.write(sortBean, NullWritable.get()); } } |
6.7. 编写Reducer代码
public class SortReducer extends Reducer<SortBean,NullWritable,SortBean,NullWritable> { //reduce方法将新的K2和V2转为K3和V3 @Override protected void reduce(SortBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException { context.write(key, NullWritable.get()); } } |
6.8. 编写主类代码
public class SortRunner { public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); //1:创建job对象 Job job = Job.getInstance(conf, "mapreduce_sort"); //2:指定job所在的jar包 job.setJarByClass(SortRunner.class); //3:指定源文件的读取方式类和源文件的读取路径 job.setInputFormatClass(TextInputFormat.class); ///TextInputFormat.addInputPath(job, new Path("hdfs://node01:8020/input/sort_input")); TextInputFormat.addInputPath(job, new Path("file:///D:\\input\\sort_input")); //4:指定自定义的Mapper类和K2、V2类型 job.setMapperClass(SortMapper.class); job.setMapOutputKeyClass(SortBean.class); job.setMapOutputValueClass(NullWritable.class); //5:指定自定义的Reducer类和K3、V3的数据类型 job.setReducerClass(SortReducer.class); job.setOutputKeyClass(SortBean.class); job.setOutputValueClass(NullWritable.class); //6:指定输出方式类和结果输出路径 job.setOutputFormatClass(TextOutputFormat.class); TextOutputFormat.setOutputPath(job, new Path("file:///D:\\out\\sort_out")); //7:将job提交给yarn集群 boolean bl = job.waitForCompletion(true); System.exit(bl?0:1); } } |
7. MapReduce的运行机制详解
7.1. MapTask工作机制
简单概述:inputFile通过split被逻辑切分为多个split文件,通过Record按行读取内容给map(用户自己实现的)进行处理,数据被map处理结束之后交给OutputCollector收集器,对其结果key进行分区(默认使用hash分区),然后写入buffer,每个map task都有一个内存缓冲区,存储着map的输出结果,当缓冲区快满的时候需要将缓冲区的数据以一个临时文件的方式存放到磁盘,当整个map task结束后再对磁盘中这个map task产生的所有临时文件做合并,生成最终的正式输出文件,然后等待reduce task来拉数据。
7.1.1. 详细步骤
1、读取数据组件 InputFormat (默认 TextInputFormat) 会通过 getSplits 方法对输入目录中文件进行逻辑切片规划得到 block, 有多少个 block就对应启动多少个 MapTask.
2、将输入文件切分为 block 之后, 由 RecordReader 对象 (默认是LineRecordReader) 进行读取, 以 \n 作为分隔符, 读取一行数据, 返回
3、读取 block 返回
4、Mapper 逻辑结束之后, 将 Mapper 的每条结果通过 context.write 进行collect数据收集. 在 collect 中, 会先对其进行分区处理,默认使用 HashPartitioner
MapReduce 提供 Partitioner 接口, 它的作用就是根据 Key 或 Value 及 Reducer 的数量来决定当前的这对输出数据最终应该交由哪个 Reduce task 处理, 默认对 Key Hash 后再以 Reducer 数量取模. 默认的取模方式只是为了平均 Reducer 的处理能力, 如果用户自己对 Partitioner 有需求, 可以订制并设置到 Job 上
5、接下来, 会将数据写入内存, 内存中这片区域叫做环形缓冲区, 缓冲区的作用是批量收集 Mapper 结果, 减少磁盘 IO 的影响. 我们的 Key/Value 对以及 Partition 的结果都会被写入缓冲区. 当然, 写入之前,Key 与 Value 值都会被序列化成字节数组
环形缓冲区其实是一个数组, 数组中存放着 Key, Value 的序列化数据和 Key, Value 的元数据信息, 包括 Partition, Key 的起始位置, Value 的起始位置以及 Value 的长度. 环形结构是一个抽象概念
缓冲区是有大小限制, 默认是 100MB. 当 Mapper 的输出结果很多时, 就可能会撑爆内存, 所以需要在一定条件下将缓冲区中的数据临时写入磁盘, 然后重新利用这块缓冲区. 这个从内存往磁盘写数据的过程被称为 Spill, 中文可译为溢写. 这个溢写是由单独线程来完成, 不影响往缓冲区写 Mapper 结果的线程. 溢写线程启动时不应该阻止 Mapper 的结果输出, 所以整个缓冲区有个溢写的比例 spill.percent. 这个比例默认是 0.8, 也就是当缓冲区的数据已经达到阈值 buffer size * spill percent = 100MB * 0.8 = 80MB, 溢写线程启动, 锁定这 80MB 的内存, 执行溢写过程. Mapper 的输出结果还可以往剩下的 20MB 内存中写, 互不影响
6、当溢写线程启动后, 需要对这 80MB 空间内的 Key 做排序 (Sort). 排序是 MapReduce 模型默认的行为, 这里的排序也是对序列化的字节做的排序
如果 Job 设置过 Combiner, 那么现在就是使用 Combiner 的时候了. 将有相同 Key 的 Key/Value 对的 Value 加起来, 减少溢写到磁盘的数据量. Combiner 会优化 MapReduce 的中间结果, 所以它在整个模型中会多次使用
那哪些场景才能使用 Combiner 呢? 从这里分析, Combiner 的输出是 Reducer 的输入, Combiner 绝不能改变最终的计算结果. Combiner 只应该用于那种 Reduce 的输入 Key/Value 与输出 Key/Value 类型完全一致, 且不影响最终结果的场景. 比如累加, 最大值等. Combiner 的使用一定得慎重, 如果用好, 它对 Job 执行效率有帮助, 反之会影响 Reducer 的最终结果
7、合并溢写文件, 每次溢写会在磁盘上生成一个临时文件 (写之前判断是否有 Combiner), 如果 Mapper 的输出结果真的很大, 有多次这样的溢写发生, 磁盘上相应的就会有多个临时文件存在. 当整个数据处理结束之后开始对磁盘中的临时文件进行 Merge 合并, 因为最终的文件只有一个, 写入磁盘, 并且为这个文件提供了一个索引文件, 以记录每个reduce对应数据的偏移量
7.1.2. 配置
配置 |
默认值 |
解释 |
mapreduce.task.io.sort.mb |
100 |
设置环型缓冲区的内存值大小 |
mapreduce.map.sort.spill.percent |
0.8 |
设置溢写的比例 |
mapreduce.cluster.local.dir |
${hadoop.tmp.dir}/mapred/local |
溢写数据目录 |
mapreduce.task.io.sort.factor |
10 |
设置一次合并多少个溢写文件 |
7.2. ReduceTask工作机制
Reduce大致分为copy、sort、reduce三个阶段,重点在前两个阶段。copy阶段包含一个eventFetcher来获取已完成的map列表,由Fetcher线程去copy数据,在此过程中会启动两个merge线程,分别为inMemoryMerger和onDiskMerger,分别将内存中的数据merge到磁盘和将磁盘中的数据进行merge。待数据copy完成之后,copy阶段就完成了,开始进行sort阶段,sort阶段主要是执行finalMerge操作,纯粹的sort阶段,完成之后就是reduce阶段,调用用户定义的reduce函数进行处理。
详细步骤:
1、Copy阶段,简单地拉取数据。Reduce进程启动一些数据copy线程(Fetcher),通过HTTP方式请求maptask获取属于自己的文件。
2、Merge阶段,这里的merge如map端的merge动作,只是数组中存放的是不同map端copy来的数值。Copy过来的数据会先放入内存缓冲区中,这里的缓冲区大小要比map端的更为灵活。merge有三种形式:内存到内存;内存到磁盘;磁盘到磁盘。默认情况下第一种形式不启用。当内存中的数据量到达一定阈值,就启动内存到磁盘的merge。与map 端类似,这也是溢写的过程,这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多的溢写文件。第二种merge方式一直在运行,直到没有map端的数据时才结束,然后启动第三种磁盘到磁盘的merge方式生成最终的文件。
3、合并排序,把分散的数据合并成一个大的数据后,还会再对合并后的数据排序。
4、对排序后的键值对调用reduce方法,键相等的键值对调用一次reduce方法,每次调用会产生零个或者多个键值对,最后把这些输出的键值对写入到HDFS文件中。
7.3. MapReduce的shuffle过程
map阶段处理的数据如何传递给reduce阶段,是MapReduce框架中最关键的一个流程,这个流程就叫shuffle。
shuffle: 洗牌、发牌——(核心机制:数据分区,排序,分组,规约,合并等过程)。
shuffle是Mapreduce的核心,它分布在Mapreduce的map阶段和reduce阶段。一般把从Map产生输出开始到Reduce取得数据作为输入之前的过程称作shuffle。
1)Collect阶段:将MapTask的结果输出到默认大小为100M的环形缓冲区,保存的是key/value,Partition分区信息等。
2)Spill阶段:当内存中的数据量达到一定的阀值的时候,就会将数据写入本地磁盘,在将数据写入磁盘之前需要对数据进行一次排序的操作,如果配置了combiner,还会将有相同分区号和key的数据进行排序。
3)Merge阶段:把所有溢出的临时文件进行一次合并操作,以确保一个MapTask最终只产生一个中间数据文件。
4)Copy阶段:ReduceTask启动Fetcher线程到已经完成MapTask的节点上复制一份属于自己的数据,这些数据默认会保存在内存的缓冲区中,当内存的缓冲区达到一定的阀值的时候,就会将数据写到磁盘之上。
5)Merge阶段:在ReduceTask远程复制数据的同时,会在后台开启两个线程对内存到本地的数据文件进行合并操作。
6)Sort阶段:在对数据进行合并的同时,会进行排序操作,由于MapTask阶段已经对数据进行了局部的排序,ReduceTask只需保证Copy的数据的最终整体有效性即可。
Shuffle中的缓冲区大小会影响到mapreduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快
缓冲区的大小可以通过参数调整, 参数:mapreduce.task.io.sort.mb 默认100M
8. MapReuce的Combineer
8.1. 概念
每一个 map 都可能会产生大量的本地输出,Combiner 的作用就是对 map 端的输出先做一次合并,以减少在 map 和 reduce 节点之间的数据传输量,以提高网络IO 性能,是 MapReduce 的一种优化手段之一
l combiner 是 MR 程序中 Mapper 和 Reducer 之外的一种组件
l combiner 组件的父类就是 Reducer
l combiner 和 reducer 的区别在于运行的位置
- Combiner 是在每一个 maptask 所在的节点运行
- Reducer 是接收全局所有 Mapper 的输出结果
l combiner 的意义就是对每一个 maptask 的输出进行局部汇总,以减小网络传输量
8.2. 实现步骤
在这里以单词统计为例,实现Combiner
1、自定义一个 combiner 继承 Reducer,重写 reduce 方法
public class MyCombiner extends Reducer<Text,LongWritable,Text,LongWritable> { /* key : hello values: <1,1,1,1> */ @Override protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException { long count = 0; //1:遍历集合,将集合中的数字相加,得到 V3 for (LongWritable value : values) { count += value.get(); } //2:将K3和V3写入上下文中 context.write(key, new LongWritable(count)); } } |
2、在 job 中设置 job.setCombinerClass(CustomCombiner.class)
job.setCombinerClass(MyCombiner.class); |
combiner 能够应用的前提是不能影响最终的业务逻辑,而且,combiner 的输出 kv 应该跟 reducer 的输入 kv 类型要对应起来
3、对使用Combiner之前和之后的日志进行对比
通过对比发现,使用Combiner之后,Reduce输入的键值对数量降低了,提供了网络传输效率。
9. MapReduce的自定义分组
GroupingComparator是mapreduce当中reduce端的一个功能组件,主要的作用是决定哪些数据作为一组,调用一次reduce的逻辑,默认是每个不同的key,作为多个不同的组,每个组调用一次reduce逻辑,我们可以自定义GroupingComparator实现不同的key作为同一个组,调用一次reduce逻辑
9.1. 需求
有如下订单数据
订单id |
商品id |
成交金额 |
Order_0000001 |
Pdt_01 |
222.8 |
Order_0000001 |
Pdt_05 |
25.8 |
Order_0000002 |
Pdt_03 |
522.8 |
Order_0000002 |
Pdt_04 |
122.4 |
Order_0000002 |
Pdt_05 |
722.4 |
Order_0000003 |
Pdt_01 |
222.8 |
现在需要求出每一个订单中成交金额最大的一笔交易
9.2. 分析
1、利用“订单id和成交金额”作为key,可以将map阶段读取到的所有订单数据按照id分区,按照金额排序,发送到reduce
2、在reduce端利用groupingcomparator将订单id相同的kv聚合成组,然后取第一个即是最大值
9.3. 实现
9.3.1. 第一步:定义OrderBean
定义一个OrderBean,里面定义两个字段,第一个字段是我们的orderId,第二个字段是我们的金额(注意金额一定要使用Double或者DoubleWritable类型,否则没法按照金额顺序排序)
public class OrderBean implements WritableComparable<OrderBean> { private String orderId; private Double price; @Override public int compareTo(OrderBean o) { //比较订单id的排序顺序 int i = this.orderId.compareTo(o.orderId); if(i==0){ //如果订单id相同,则比较金额,金额大的排在前面 i = - this.price.compareTo(o.price); } return i; } @Override public void write(DataOutput out) throws IOException { out.writeUTF(orderId); out.writeDouble(price); } @Override public void readFields(DataInput in) throws IOException { this.orderId = in.readUTF(); this.price = in.readDouble(); } public OrderBean() { } public OrderBean(String orderId, Double price) { this.orderId = orderId; this.price = price; } public String getOrderId() { return orderId; } public void setOrderId(String orderId) { this.orderId = orderId; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } @Override public String toString() { return orderId +"\t"+price; } } |
9.3.2. 第二步:自定义分区
自定义分区,按照订单id进行分区,把所有订单id相同的数据,都发送到同一个reduce中去
public class OrderPartition extends Partitioner<OrderBean,NullWritable> { @Override public int getPartition(OrderBean orderBean, NullWritable nullWritable, int i) { //自定义分区,将相同订单id的数据发送到同一个reduce里面去 return (orderBean.getOrderId().hashCode() & Integer.MAX_VALUE)%i; } } |
9.3.3. 第三步:自定义groupingComparator
按照我们自己的逻辑进行分组,通过比较相同的订单id,将相同的订单id放到一个组里面去,进过分组之后当中的数据,已经全部是排好序的数据,我们只需要取前topN即可
public class MyGroupIngCompactor extends WritableComparator { //将我们自定义的OrderBean注册到我们自定义的MyGroupIngCompactor当中来 //表示我们的分组器在分组的时候,对OrderBean这一种类型的数据进行分组 //传入作为key的bean的class类型,以及制定需要让框架做反射获取实例对象 public MyGroupIngCompactor() { super(OrderBean.class,true); } @Override public int compare(WritableComparable a, WritableComparable b) { OrderBean first = (OrderBean) a; OrderBean second = (OrderBean) b; return first.getOrderId().compareTo(second.getOrderId()); } } |
9.3.4. 第四步:程序main函数入口
public class GroupingCompactorRunner extends Configured implements Tool { @Override public int run(String[] args) throws Exception { Job job = Job.getInstance(super.getConf(), GroupingCompactorRunner .class.getSimpleName()); job.setInputFormatClass(TextInputFormat.class); TextInputFormat.addInputPath(job,new Path("file:///D:\\input\\topn")); job.setMapperClass(MyGroupingMapper.class); job.setMapOutputKeyClass(OrderBean.class); job.setMapOutputValueClass(NullWritable.class); job.setPartitionerClass(OrderPartition.class); job.setGroupingComparatorClass(MyGroupIngCompactor.class); job.setReducerClass(MyGroupingReducer.class); job.setOutputKeyClass(OrderBean.class); job.setOutputValueClass(NullWritable.class); job.setNumReduceTasks(2); job.setOutputFormatClass(TextOutputFormat.class); TextOutputFormat.setOutputPath(job,new Path("file:///D:\\output\\topn")); boolean b = job.waitForCompletion(true); return b?0:1; } public static class MyGroupingMapper extends Mapper<LongWritable,Text,OrderBean,NullWritable>{ @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] split = value.toString().split("\t"); OrderBean orderBean = new OrderBean(split[0], Double.valueOf(split[2])); context.write(orderBean,NullWritable.get()); } } public static class MyGroupingReducer extends Reducer<OrderBean,NullWritable,OrderBean,NullWritable>{ @Override protected void reduce(OrderBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException { context.write(key,NullWritable.get()); } } public static void main(String[] args) throws Exception { ToolRunner.run(new Configuration(),new GroupingCompactorMain(),args); } } |
![](http://img.e-com-net.com/image/info8/4bf62a26c3054461a1e58947c78d1095.gif)
10. MapReduce高阶训练
10.1. 上网流量统计
数据格式如下:
10.1.1. 需求一:统计求和
统计每个手机号的上行流量总和,下行流量总和,上行总流量之和,下行总流量之和
分析:以手机号码作为key值,上行流量,下行流量,上行总流量,下行总流量四个字段作为value值,然后以这个key和value作为map阶段的输出,reduce阶段的输入。
10.1.1.1. 思路分析
10.1.1.2. 代码实现
第一步:自定义map的输出value对象FlowBean
public class FlowBean implements Writable { private Integer upFlow; private Integer downFlow; private Integer upCountFlow; private Integer downCountFlow; @Override public void write(DataOutput out) throws IOException { out.writeInt(upFlow); out.writeInt(downFlow); out.writeInt(upCountFlow); out.writeInt(downCountFlow); } @Override public void readFields(DataInput in) throws IOException { this.upFlow = in.readInt(); this.downFlow = in.readInt(); this.upCountFlow = in.readInt(); this.downCountFlow = in.readInt(); } public FlowBean() { } public FlowBean(Integer upFlow, Integer downFlow, Integer upCountFlow, Integer downCountFlow) { this.upFlow = upFlow; this.downFlow = downFlow; this.upCountFlow = upCountFlow; this.downCountFlow = downCountFlow; } public Integer getUpFlow() { return upFlow; } public void setUpFlow(Integer upFlow) { this.upFlow = upFlow; } public Integer getDownFlow() { return downFlow; } public void setDownFlow(Integer downFlow) { this.downFlow = downFlow; } public Integer getUpCountFlow() { return upCountFlow; } public void setUpCountFlow(Integer upCountFlow) { this.upCountFlow = upCountFlow; } public Integer getDownCountFlow() { return downCountFlow; } public void setDownCountFlow(Integer downCountFlow) { this.downCountFlow = downCountFlow; } @Override public String toString() { return "FlowBean{" + "upFlow=" + upFlow + ", downFlow=" + downFlow + ", upCountFlow=" + upCountFlow + ", downCountFlow=" + downCountFlow + '}'; } } |
第二步:定义FlowMapper类
public class FlowMapper extends Mapper<LongWritable,Text,Text,FlowBean> { FlowBean flowBean = new FlowBean(); @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String s = value.toString(); String[] split = s.split("\t"); flowBean.setUpFlow(Integer.parseInt(split[6])); flowBean.setDownFlow(Integer.parseInt(split[7])); flowBean.setUpCountFlow(Integer.parseInt(split[8])); flowBean.setDownCountFlow(Integer.parseInt(split[9])); context.write(new Text(split[1]),flowBean); } } |
第三步:定义FlowReducer类
public class FlowReducer extends Reducer<Text,FlowBean,Text,FlowBean> { private FlowBean flowBean = new FlowBean(); @Override protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException { Integer upFlow = 0; Integer downFlow = 0; Integer upCountFlow = 0; Integer downCountFlow = 0; for (FlowBean value : values) { upFlow += value.getUpFlow(); downFlow += value.getDownFlow(); upCountFlow += value.getUpCountFlow(); downCountFlow += value.getDownCountFlow(); } flowBean.setUpFlow(upFlow); flowBean.setDownFlow(downFlow); flowBean.setUpCountFlow(upCountFlow); flowBean.setDownCountFlow(downCountFlow); context.write(key,flowBean); } } |
第四步:程序main函数入口FlowMain
public class FlowBeanMain extends Configured implements Tool { @Override public int run(String[] args) throws Exception { Job job = Job.getInstance(super.getConf(), FlowBeanMain.class.getSimpleName()); job.setJarByClass(FlowBeanMain.class); job.setInputFormatClass(TextInputFormat.class); TextInputFormat.addInputPath(job,new Path("file:///D:\\flowcount\\input")); job.setMapperClass(FlowMapper.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(FlowBean.class); job.setReducerClass(FlowReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(FlowBean.class); job.setOutputFormatClass(TextOutputFormat.class); TextOutputFormat.setOutputPath(job,new Path("file:///D:\\flowcount\\out")); boolean b = job.waitForCompletion(true); return b?0:1; } public static void main(String[] args) throws Exception { ToolRunner.run(new Configuration(),new FlowBeanMain(),args); } } |
10.1.2. 需求二:上行流量倒序排序(递减排序)
分析,以需求一的输出数据作为排序的输入数据,自定义FlowBean,以FlowBean为map输出的key,以手机号作为Map输出的value,因为MapReduce程序会对Map阶段输出的key进行排序
第一步:定义FlowBean实现WritableComparable实现比较排序
public class FlowBean implements WritableComparable<FlowBean> { private Integer upFlow; private Integer downFlow; private Integer upCountFlow; private Integer downCountFlow; public FlowBean() { } public FlowBean(Integer upFlow, Integer downFlow, Integer upCountFlow, Integer downCountFlow) { this.upFlow = upFlow; this.downFlow = downFlow; this.upCountFlow = upCountFlow; this.downCountFlow = downCountFlow; } @Override public void write(DataOutput out) throws IOException { out.writeInt(upFlow); out.writeInt(downFlow); out.writeInt(upCountFlow); out.writeInt(downCountFlow); } @Override public void readFields(DataInput in) throws IOException { upFlow = in.readInt(); downFlow = in.readInt(); upCountFlow = in.readInt(); downCountFlow = in.readInt(); } public Integer getUpFlow() { return upFlow; } public void setUpFlow(Integer upFlow) { this.upFlow = upFlow; } public Integer getDownFlow() { return downFlow; } public void setDownFlow(Integer downFlow) { this.downFlow = downFlow; } public Integer getUpCountFlow() { return upCountFlow; } public void setUpCountFlow(Integer upCountFlow) { this.upCountFlow = upCountFlow; } public Integer getDownCountFlow() { return downCountFlow; } public void setDownCountFlow(Integer downCountFlow) { this.downCountFlow = downCountFlow; } @Override public String toString() { return upFlow+"\t"+downFlow+"\t"+upCountFlow+"\t"+downCountFlow; } @Override public int compareTo(FlowBean o) { return this.upCountFlow > o.upCountFlow ?-1:1; } } |
第二步:定义FlowMapper
public class FlowMapper extends Mapper<LongWritable,Text,FlowBean,Text> { Text outKey = new Text(); FlowBean flowBean = new FlowBean(); @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] split = value.toString().split("\t"); flowBean.setUpFlow(Integer.parseInt(split[1])); flowBean.setDownFlow(Integer.parseInt(split[2])); flowBean.setUpCountFlow(Integer.parseInt(split[3])); flowBean.setDownCountFlow(Integer.parseInt(split[4])); outKey.set(split[0]); context.write(flowBean,outKey); } } |
第三步:定义FlowReducer
public class FlowReducer extends Reducer<FlowBean,Text,Text,FlowBean> { FlowBean flowBean = new FlowBean(); @Override protected void reduce(FlowBean key, Iterable<Text> values, Context context) throws IOException, InterruptedException { context.write(values.iterator().next(),key); } } |
第四步:程序main函数入口
public class FlowMain extends Configured implements Tool { @Override public int run(String[] args) throws Exception { Configuration conf = super.getConf(); conf.set("mapreduce.framework.name","local"); Job job = Job.getInstance(conf, FlowMain.class.getSimpleName()); job.setJarByClass(FlowMain.class); job.setInputFormatClass(TextInputFormat.class); TextInputFormat.addInputPath(job,new Path("file:///D:\\flowcount\\output")); job.setMapperClass(FlowMapper.class); job.setMapOutputKeyClass(FlowBean.class); job.setMapOutputValueClass(Text.class); job.setReducerClass(FlowReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(FlowBean.class); TextOutputFormat.setOutputPath(job,new Path("file:///D:\\output\\flowcount_sort")); job.setOutputFormatClass(TextOutputFormat.class); boolean b = job.waitForCompletion(true); return b?0:1; } public static void main(String[] args) throws Exception { Configuration configuration = new Configuration(); int run = ToolRunner.run(configuration, new FlowMain(), args); System.exit(run); } } |
10.2. 社交粉丝数据分析
10.2.1. 逻辑分析
以下是qq的好友列表数据,冒号前是一个用户,冒号后是该用户的所有好友(数据中的好友关系是单向的)
A:B,C,D,F,E,O B:A,C,E,K C:F,A,D,I D:A,E,F,L E:B,C,D,M,L F:A,B,C,D,E,O,M G:A,C,D,E,F H:A,C,D,E,O I:A,O J:B,O K:A,C,D L:D,E,F M:E,F,G O:A,H,I,J |
求出哪些人两两之间有共同好友,及他俩的共同好友都有谁?
解题思路:
用户->好友 好友->用户 好友->用户 用户->好友 用户->好友 遍历(用户->好友) 聚合(用户->好友)
根据以上分析,实现该案例需要写两个MapReduce
10.2.2. 代码实现
第一步:第一个MapReduce
public class ComonsFriendsStepOne { public static class ComonsFriendsStepOneMapper extends Mapper<LongWritable,Text,Text,Text>{ @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] split = value.toString().split(":"); String person = split[0]; String[] friends = split[1].split(","); for (String friend : friends) { context.write(new Text(friend),new Text(person)); } } } public static class ComonsFriendsStepOneReducer extends Reducer<Text,Text,Text,Text>{ @Override protected void reduce(Text friend, Iterable<Text> persons, Context context) throws IOException, InterruptedException { StringBuffer buffer = new StringBuffer(); for (Text person : persons) { buffer.append(person).append("-"); } context.write(friend,new Text(buffer.toString())); } } public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); Job job = Job.getInstance(conf, ComonsFriendsStepOne.class.getSimpleName()); job.setJarByClass(ComonsFriendsStepOne.class); job.setInputFormatClass(TextInputFormat.class); TextInputFormat.addInputPath(job,new Path("file:///D:\\input\\common_friends")); job.setMapperClass(ComonsFriendsStepOneMapper.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(Text.class); job.setReducerClass(ComonsFriendsStepOneReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(Text.class); job.setOutputFormatClass(TextOutputFormat.class); TextOutputFormat.setOutputPath(job,new Path("file:///D:\\output\\common_friends")); boolean b = job.waitForCompletion(true); System.exitt(b?0:1); } } |
第二步:第二个MapReduce
public class ComonsFriendsStepTwo { @Override public int run(String[] args) throws Exception { } public static class ComonsFriendStepTwoMapper extends Mapper<LongWritable,Text,Text,Text>{ /** * A F-D-O-I-H-B-K-G-C- * B E-A-J-F- * C K-A-B-E-F-G-H- */ @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] split = value.toString().split("\t"); String friends = split[0]; String[] persons = split[1].split("-"); //排序,避免c-b 与b-c 这样的情况出现 Arrays.sort(persons); for(int i =0;i< persons.length -1 ;i++){ for(int j = i+1;j<persons.length;j++){ context.write(new Text(persons[i]+"-"+persons[j]),new Text(friends)); } } } } public static class ComonsFriendStepTwoReducer extends Reducer<Text,Text,Text,Text>{ @Override protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { StringBuffer buffer = new StringBuffer(); for (Text value : values) { buffer.append(value.toString()+"\t"); } context.write(key,new Text(buffer.toString())); } } public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); Job job = Job.getInstance(conf, ComonsFriendsStepTwo.class.getSimpleName()); job.setJarByClass(ComonsFriendsStepTwo.class); job.setInputFormatClass(TextInputFormat.class); TextInputFormat.addInputPath(job,new Path("file:///D:\\output\\topn")); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(Text.class); job.setMapperClass(ComonsFriendStepTwoMapper.class); job.setReducerClass(ComonsFriendStepTwoReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(Text.class); job.setOutputFormatClass(TextOutputFormat.class); TextOutputFormat.setOutputPath(job,new Path("file:///D:\\output\\topn2")); boolean b = job.waitForCompletion(true); System.exit(b?0:1) } } |
10.3. 倒序索引
10.3.1. 倒排索引介绍
倒排索引是文档检索系统中最常用的数据结构,被广泛应用于全文搜索引擎。倒排索引主要用来存储某个单词(或词组)在一组文档中的存储位置的映射,提供了可以根据内容来查找文档的方式,而不是根据文档来确定内容,因此称为倒排索引(Inverted Index)。带有倒排索引的文件我们称为倒排索引文件,简称倒排文件(Inverted File)。
通常情况下,倒排文件由一个单词(或词组)和相关联的文档列表组成,如图所示。
从图可以看出,建立倒排索引的目的是为了更加方便的搜索。例如,单词1出现在文档1、文档4、文档13等;单词2出现在文档2、文档6、文档10等;而单词3出现在文档3、文档7等。
在实际应用中,还需要给每个文档添加一个权值,用来指出每个文档与搜索内容的相关度。最常用的是使用词频作为权重,即记录单词或词组在文档中出现的次数,用户在搜索相关文档时,就会把权重高的推荐给客户。下面以英文单词倒排索引为例,如图所示。
加权倒排索引文件
从图可以看出,加权倒排索引文件中,文件每一行内容对每一个单词进行了加权索引,统计出单词出现的文档和次数。例如索引文件中的第一行,表示“hadoop”这个单词在文本file1.txt中出现过1次,file4.txt中出现过2次,file13.txt中出现过1次。
10.3.2. 案例需求及分析
现假设有三个源文件file1.txt、file2.txt和file3.txt,需要使用倒排索引的方式对这三个源文件内容实现倒排索引,并将最后的倒排索引文件输出,整个过程要求实现如下转换,如图所示。
倒排索引文件
接下来,我们就根据上面案例的需求结合倒排索引的实现,对该倒排索引案例的实现进行分析,具体如下。
(1)首先使用默认的TextInputFormat类对每个输入文件进行处理,得到文本中每行的偏移量及其内容。Map过程首先分析输入的
Map阶段数据转换过程
从图可以看出,在不使用Hadoop自定义数据类型的情况下,需要根据情况将单词与文档名称拼接为一个key(如“MapReduce:file1.txt”),将词频作为一个value。
(2)经过Map阶段数据转换后,同一个文档中相同的单词会出现多个的情况,而单纯依靠后续Reduce阶段无法同时完成词频统计和生成文档列表,所以必须增加一个Combine阶段,先完成每一个文档的词频统计,如图所示。
Combine阶段数据转换过程
从图4-16可以看出,在Reduce阶段,根据前面的分析先完成每一个文档的词频统计,然后又对输入的
(3)经过上述两个阶段的处理后,Reduce阶段只需将所有文件中相同key值的value值进行统计,并组合成倒排索引文件所需的格式即可,如图所示。
Reduce数据转换思路
从图可以看出,在Reduce阶段会根据所有文档中相同key进行统计,同时在处理过程中结合倒排索引文件的格式需求就可以生成对应的文件。
需要说明的是,创建倒排索引的最终目的是通过单词找到对应的文档,明确思路是MapReduce程序编写的重点,如果开发者在不了解入手阶段的Map数据格式如何设计时,不妨考虑从Reduce阶段输出的数据格式反向推导。
10.3.3. 案例实现
在完成对倒排索引的相关介绍以及案例实现的具体分析后,接下来就通过前面说明的案例分析步骤来实现具体的倒排索引,具体实现步骤如下。
1.Map阶段实现
首先,使用Eclipse开发工具打开之前创建的Maven项目HadoopDemo,并且新创建cn.itcast.mr.invertedIndex包,在该路径下编写自定义Mapper类InvertedIndexMapper。
import java.io.IOException; import org.apache.commons.lang.StringUtils; 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; public class InvertedIndexMapper extends Mapper<LongWritable, Text, Text, Text> { private static Text keyInfo = new Text();// 存储单词和文档名称 // 存储词频,初始化为1 private static final Text valueInfo = new Text("1"); @Override protected void map(LongWritable key, Text value,Context context) throws IOException, InterruptedException { String line = value.toString(); String[] fields = StringUtils.split(line, " "); // 得到这行数据所在的文件切片 FileSplit fileSplit = (FileSplit) context.getInputSplit(); // 根据文件切片得到文件名 String fileName = fileSplit.getPath().getName(); for (String field : fields) { // key值由单词和文档名称组成,如“MapReduce:file1.txt” keyInfo.set(field + ":" + fileName); context.write(keyInfo, valueInfo); } } } |
代码的作用是将文本中的单词按照空格进行切割,并以冒号拼接,“单词:文档名称”作为key,单词次数作为value,都以文本方式输出至Combine阶段。
2.Combine阶段实现
接着,根据Map阶段的输出结果形式,在cn.itcast.mr.InvertedIndex包下,自定义实现Combine阶段的类InvertedIndexCombiner,对每个文档的单词进行词频统计。
import java.io.IOException; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Reducer; public class InvertedIndexCombiner extends Reducer<Text, Text, Text, Text> { private static Text info = new Text(); // 输入: // 输出: @Override protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { int sum = 0;// 统计词频 for (Text value : values) { sum += Integer.parseInt(value.toString()); } int splitIndex = key.toString().indexOf(":"); // 重新设置value值由文档名称和词频组成 info.set(key.toString().substring(splitIndex + 1) + ":" + sum); // 重新设置 key 值为单词 key.set(key.toString().substring(0, splitIndex)); context.write(key, info); } } |
代码的作用是对Map阶段的单词次数聚合处理,并重新设置key值为单词,value值由文档名称和词频组成。
3.Reduce阶段实现
然后,根据Combine阶段的输出结果形式,同样在cn.itcast.mr.InvertedIndex包下,自定义Reducer类InvertedIndexMapper。
import java.io.IOException; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Reducer; public class InvertedIndexReducer extends Reducer<Text, Text, Text, Text> { private static Text result = new Text(); // 输入: // 输出: @Override protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { // 生成文档列表 String fileList = new String(); for (Text value : values) { fileList += value.toString() + ";"; } result.set(fileList); context.write(key, result); } } |
代码的作用是,接收Combine阶段输出的数据,并最终案例倒排索引文件需求的样式,将单词作为key,多个文档名称和词频连接作为value,输出到目标目录。
4.Driver程序主类实现
最后,编写MapReduce程序运行主类InvertedIndexDriver。
import java.io.IOException; 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; public class InvertedIndexDriver { public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException { Configuration conf = new Configuration(); Job job = Job.getInstance(conf); job.setJarByClass(InvertedIndexDriver.class); job.setMapperClass(InvertedIndexMapper.class); job.setCombinerClass(InvertedIndexCombiner.class); job.setReducerClass(InvertedIndexReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(Text.class); FileInputFormat.setInputPaths(job, new Path("D:\\input\\InvertedIndex")); // 指定处理完成之后的结果所保存的位置 FileOutputFormat.setOutputPath(job, new Path("D:\\output\\InvertedIndex")); // 向 YARN 集群提交这个job boolean res = job.waitForCompletion(true); System.exit(res ? 0 : 1); } } |
5.效果测试
为了保证MapReduce程序正常执行,需要先在本地D:\\InvertedIndex\\input目录下创建file1.txt、file2.txt和file3.txt,file1.txt输入“MapReduce is simple”,file2.txt输入“MapReduce is powerful is simple”,file3.txt输入“Hello MapReduce bye MapReduce”。
接着,执行MapReduce程序的程序入口InvertedIndexDriver类,正常执行完成后,会在指定的D:\\InvertedIndex\\output下生成结果文件,如图所示。
至此,倒排索引的案例已实现,从图可以清楚看出各个单词所对应的文档以及出现次数。
11. MapReduce并行度机制
11.1. MapTask并行度机制
MapTask的并行度指的是map阶段有多少个并行的task共同处理任务。map阶段的任务处理并行度,势必影响到整个job的处理速度。那么,MapTask并行实例是否越多越好呢?其并行度又是如何决定呢?
一个MapReducejob的map阶段并行度由客户端在提交job时决定,即客户端提交job之前会对待处理数据进行逻辑切片。切片完成会形成切片规划文件(job.split),每个逻辑切片最终对应启动一个maptask。
逻辑切片机制由FileInputFormat实现类的getSplits()方法完成。
11.1.1. FileInputFormat切片机制
FileInputFormat中默认的切片机制:
l 切片大小,默认等于block大小,即128M
l block是HDFS上物理上存储的存储的数据,切片是对数据逻辑上的划分。
l 在FileInputFormat中,计算切片大小的逻辑:
Math.max(minSize, Math.min(maxSize, blockSize)); |
l 切片举例
比如待处理数据有两个文件:
file1.txt 320M
file2.txt 10M
经过FileInputFormat的切片机制运算后,形成的切片信息如下:
file1.txt.split1—0M~128M
file1.txt.split2—128M~256M
file1.txt.split3—256M~320M
file2.txt.split1—0M~10M
FileInputFormat中切片的大小的由这几个值来运算决定:
在 FileInputFormat 中,计算切片大小的逻辑:
long splitSize = computeSplitSize(blockSize, minSize, maxSize),
切片主要由这几个值来运算决定:
blocksize:默认是 128M,可通过 dfs.blocksize 修改
minSize:默认是 1,可通过 mapreduce.input.fileinputformat.split.minsize 修改
maxsize:默认是 Long.MaxValue,可通过 mapreduce.input.fileinputformat.split.maxsize 修改
如果设置的最大值maxsize比blocksize值小,则按照maxSize切数据
如果设置的最小值minsize比blocksize值大,则按照minSize切数据
但是,不论怎么调参数,都不能让多个小文件“划入”一个 split
11.1.2. FileInputFormat切片参数设置
第一种情况(切片大小为256M):
FileInputFormat.setInputPaths(job, new Path(input)); FileInputFormat. setMaxInputSplitSize(job,1024*1024*500) ; //设置最大分片大小 FileInputFormat.setMinInputSplitSize(job,1024*1024*256); //设置最小分片大小 |
第二种情况(切片大小为100M):
FileInputFormat.setInputPaths(job, new Path(input)); FileInputFormat.setMaxInputSplitSize(job,1024*1024*100) ; //设置最大分片大小 FileInputFormat.setMinInputSplitSize(job,1024*1024*80); //设置最小分片大小 |
整个切片的核心过程在getSplit()方法中完成。
数据切片只是在逻辑上对输入数据进行分片,并不会再磁盘上将其切分成分片进行存储。InputSplit只记录了分片的元数据信息,比如起始位置、长度以及所在的节点列表等。
11.2. Reducetask并行度机制
reducetask并行度同样影响整个job的执行并发度和执行效率,与maptask的并发数由切片数决定不同,Reducetask数量的决定是可以直接手动设置:
job.setNumReduceTasks(4); |
如果数据分布不均匀,就有可能在reduce阶段产生数据倾斜。
注意: reducetask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个reducetask。
12. MapReduce性能优化策略
使用Hadoop进行大数据运算,当数据量极其大时,那么对MapReduce性能的调优重要性不言而喻,尤其是Shuffle过程中的参数配置对作业的总执行时间影响特别大。下面总结一些和MapReduce相关的性能调优方法,主要从五个方面考虑:数据输入、Map阶段、Reduce阶段、Shuffle阶段和其他调优属性。
1.数据输入
在执行MapReduce任务前,将小文件进行合并,大量的小文件会产生大量的map任务,增大map任务装载的次数,而任务的装载比较耗时,从而导致MapReduce运行速度较慢。因此我们采用CombineTextInputFormat来作为输入,解决输入端大量的小文件场景。
2.Map阶段
(1)减少溢写(spill)次数:通过调整io.sort.mb及sort.spill.percent参数值,增大触发spill的内存上限,减少spill次数,从而减少磁盘IO。
(2)减少合并(merge)次数:通过调整io.sort.factor参数,增大merge的文件数目,减少merge的次数,从而缩短mr处理时间。
(3)在map之后,不影响业务逻辑前提下,先进行combine处理,减少 I/O。
我们在上面提到的那些属性参数,都是位于mapred-default.xml文件中,这些属性参数的调优方式如表所示。
Map阶段调优属性
属性名称 |
类型 |
默认值 |
说明 |
mapreduce.task.io.sort.mb |
int |
100 |
配置排序map输出时使用的内存缓冲区的大小,默认100Mb,实际开发中可以设置大一些。 |
mapreduce.map.sort.spill.percent |
float |
0.80 |
map输出内存缓冲和用来开始磁盘溢出写过程的记录边界索引的阈值,即最大使用环形缓冲内存的阈值。一般默认是80%。也可以直接设置为100% |
mapreduce.task.io.sort.factor |
int |
10 |
排序文件时,一次最多合并的流数,实际开发中可将这个值设置为100。 |
mapreduce.task.min.num.spills.for.combine |
int |
3 |
运行combiner时,所需的最少溢出文件数(如果已指定combiner) |
3.Reduce阶段
(1)合理设置map和reduce数:两个都不能设置太少,也不能设置太多。太少,会导致task等待,延长处理时间;太多,会导致 map、reduce任务间竞争资源,造成处理超时等错误。
(2)设置map、reduce共存:调整slowstart.completedmaps参数,使map运行到一定程度后,reduce也开始运行,减少reduce的等待时间。
(3)规避使用reduce:因为reduce在用于连接数据集的时候将会产生大量的网络消耗。通过将MapReduce参数setNumReduceTasks设置为0来创建一个只有map的作业。
(4)合理设置reduce端的buffer:默认情况下,数据达到一个阈值的时候,buffer中的数据就会写入磁盘,然后reduce会从磁盘中获得所有的数据。也就是说,buffer和reduce是没有直接关联的,中间多一个写磁盘->读磁盘的过程,既然有这个弊端,那么就可以通过参数来配置,使得buffer中的一部分数据可以直接输送到reduce,从而减少IO开销。这样一来,设置buffer需要内存,读取数据需要内存,reduce计算也要内存,所以要根据作业的运行情况进行调整。
我们在上面提到的属性参数,都是位于mapred-default.xml文件中,这些属性参数的调优方式如表所示。
Reduce阶段的调优属性
属性名称 |
类型 |
默认值 |
说明 |
mapreduce.job.reduce.slowstart.completedmaps |
float |
0.05 |
当map task在执行到5%,就开始为reduce申请资源。开始执行reduce操作,reduce可以开始拷贝map结果数据和做reduce shuffle操作。 |
mapred.job.reduce.input.buffer.percent |
float |
0.0 |
在reduce过程,内存中保存map输出的空间占整个堆空间的比例。如果reducer需要的内存较少,可以增加这个值来最小化访问磁盘的次数。 |
4.Shuffle阶段
Shuffle阶段的调优就是给Shuffle过程尽量多地提供内存空间,以防止出现内存溢出现象,可以由参数mapred.child.java.opts来设置,任务节点上的内存大小应尽量大。
我们在上面提到的属性参数,都是位于mapred-site.xml文件中,这些属性参数的调优方式如表所示。
表4-1 shuffle阶段的调优属性
属性名称 |
类型 |
默认值 |
说明 |
mapred.map.child.java.opts |
-Xmx200m |
当用户在不设置该值情况下,会以最大1G jvm heap size启动map task,有可能导致内存溢出,所以最简单的做法就是设大参数,一般设置为-Xmx1024m |
|
mapred.reduce.child.java.opts |
-Xmx200m |
当用户在不设置该值情况下,会以最大1G jvm heap size启动Reduce task,也有可能导致内存溢出,所以最简单的做法就是设大参数,一般设置为-Xmx1024m |
5.其他调优属性
除此之外,MapReduce还有一些基本的资源属性的配置,这些配置的相关参数都位于mapred-default.xml文件中,我们可以合理配置这些属性提高MapReduce性能,表4-4列举了部分调优属性。
表4-2 MapReduce资源调优属性
属性名称 |
类型 |
默认值 |
说明 |
mapreduce.map.memory.mb |
int |
1024 |
一个Map Task可使用的资源上限。如果Map Task实际使用的资源量超过该值,则会被强制杀死。 |
mapreduce.reduce.memory.mb |
int |
1024 |
一个Reduce Task可使用的资源上限。如果Reduce Task实际使用的资源量超过该值,则会被强制杀死。 |
mapreduce.map.cpu.vcores |
int |
1 |
每个Map task可使用的最多cpu core数目 |
mapreduce.reduce.cpu.vcores |
int |
1 |
每个Reduce task可使用的最多cpu core数目 |
mapreduce.reduce.shuffle.parallelcopies |
int |
5 |
每个reduce去map中拿数据的并行数。 |
mapreduce.map.maxattempts |
int |
4 |
每个Map Task最大重试次数,一旦重试参数超过该值,则认为Map Task运行失败 |
mapreduce.reduce.maxattempts |
int |
4 |
每个Reduce Task最大重试次数,一旦重试参数超过该值,则认为Map Task运行失败 |
二、Apache Hadoop YARN
13. Yarn通俗介绍
Apache Hadoop YARN (Yet Another Resource Negotiator,另一种资源协调者)是一种新的 Hadoop 资源管理器,它是一个通用资源管理系统和调度平台,可为上层应用提供统一的资源管理和调度,它的引入为集群在利用率、资源统一管理和数据共享等方面带来了巨大好处。
可以把yarn理解为相当于一个分布式的操作系统平台,而mapreduce等运算程序则相当于运行于操作系统之上的应用程序,Yarn为这些程序提供运算所需的资源(内存、cpu)。
l yarn并不清楚用户提交的程序的运行机制
l yarn只提供运算资源的调度(用户程序向yarn申请资源,yarn就负责分配资源)
l yarn中的主管角色叫ResourceManager
l yarn中具体提供运算资源的角色叫NodeManager
l yarn与运行的用户程序完全解耦,意味着yarn上可以运行各种类型的分布式运算程序,比如mapreduce、storm,spark,tez ……
l spark、storm等运算框架都可以整合在yarn上运行,只要他们各自的框架中有符合yarn规范的资源请求机制即可
l yarn成为一个通用的资源调度平台.企业中以前存在的各种运算集群都可以整合在一个物理集群上,提高资源利用率,方便数据共享
14.
Yarn基本架构
YARN是一个资源管理、任务调度的框架,主要包含三大模块:ResourceManager(RM)、NodeManager(NM)、ApplicationMaster(AM)。
ResourceManager负责所有资源的监控、分配和管理;
ApplicationMaster负责每一个具体应用程序的调度和协调;
NodeManager负责每一个节点的维护。
对于所有的applications,RM拥有绝对的控制权和对资源的分配权。而每个AM则会和RM协商资源,同时和NodeManager通信来执行和监控task。
15. Yarn三大组件介绍
15.1. ResourceManager
l ResourceManager负责整个集群的资源管理和分配,是一个全局的资源管理系统。
l NodeManager以心跳的方式向ResourceManager汇报资源使用情况(目前主要是CPU和内存的使用情况)。RM只接受NM的资源回报信息,对于具体的资源处理则交给NM自己处理。
l YARN Scheduler根据application的请求为其分配资源,不负责application job的监控、追踪、运行状态反馈、启动等工作。
15.2. NodeManager
l NodeManager是每个节点上的资源和任务管理器,它是管理这台机器的代理,负责该节点程序的运行,以及该节点资源的管理和监控。YARN集群每个节点都运行一个NodeManager。
l NodeManager定时向ResourceManager汇报本节点资源(CPU、内存)的使用情况和Container的运行状态。当ResourceManager宕机时NodeManager自动连接RM备用节点。
l NodeManager接收并处理来自ApplicationMaster的Container启动、停止等各种请求。
15.3. ApplicationMaster
l 用户提交的每个应用程序均包含一个ApplicationMaster,它可以运行在ResourceManager以外的机器上。
l 负责与RM调度器协商以获取资源(用Container表示)。
l 将得到的任务进一步分配给内部的任务(资源的二次分配)。
l 与NM通信以启动/停止任务。
l 监控所有任务运行状态,并在任务运行失败时重新为任务申请资源以重启任务。
16. Yarn运行流程
l client向RM提交应用程序,其中包括启动该应用的ApplicationMaster的必须信息,例如ApplicationMaster程序、启动ApplicationMaster的命令、用户程序等。
l ResourceManager启动一个container用于运行ApplicationMaster。
l 启动中的ApplicationMaster向ResourceManager注册自己,启动成功后与RM保持心跳。
l ApplicationMaster向ResourceManager发送请求,申请相应数目的container。
l ResourceManager返回ApplicationMaster的申请的containers信息。申请成功的container,由ApplicationMaster进行初始化。container的启动信息初始化后,AM与对应的NodeManager通信,要求NM启动container。AM与NM保持心跳,从而对NM上运行的任务进行监控和管理。
l container运行期间,ApplicationMaster对container进行监控。container通过RPC协议向对应的AM汇报自己的进度和状态等信息。
l 应用运行期间,client直接与AM通信获取应用的状态、进度更新等信息。
l 应用运行结束后,ApplicationMaster向ResourceManager注销自己,并允许属于它的container被收回。
17. Yarn的调度器Scheduler
理想情况下,我们应用对Yarn资源的请求应该立刻得到满足,但现实情况资源往往是有限的,特别是在一个很繁忙的集群,一个应用资源的请求经常需要等待一段时间才能的到相应的资源。在Yarn中,负责给应用分配资源的就是Scheduler。其实调度本身就是一个难题,很难找到一个完美的策略可以解决所有的应用场景。为此,Yarn提供了多种调度器和可配置的策略供我们选择。
在Yarn中有三种调度器可以选择:FIFO Scheduler ,Capacity Scheduler,Fair Scheduler。
17.1. FIFO Scheduler
FIFO Scheduler把应用按提交的顺序排成一个队列,这是一个先进先出队列,在进行资源分配的时候,先给队列中最头上的应用进行分配资源,待最头上的应用需求满足后再给下一个分配,以此类推。
FIFO Scheduler是最简单也是最容易理解的调度器,也不需要任何配置,但它并不适用于共享集群。大的应用可能会占用所有集群资源,这就导致其它应用被阻塞。在共享集群中,更适合采用Capacity Scheduler或Fair Scheduler,这两个调度器都允许大任务和小任务在提交的同时获得一定的系统资源。
17.2. Capacity Scheduler
Capacity 调度器允许多个组织共享整个集群,每个组织可以获得集群的一部分计算能力。通过为每个组织分配专门的队列,然后再为每个队列分配一定的集群资源,这样整个集群就可以通过设置多个队列的方式给多个组织提供服务了。除此之外,队列内部又可以垂直划分,这样一个组织内部的多个成员就可以共享这个队列资源了,在一个队列内部,资源的调度是采用的是先进先出(FIFO)策略。
容量调度器 Capacity Scheduler 最初是由 Yahoo 最初开发设计使得 Hadoop 应用能够被多用户使用,且最大化整个集群资源的吞吐量,现被 IBM BigInsights 和 Hortonworks HDP 所采用。
Capacity Scheduler 被设计为允许应用程序在一个可预见的和简单的方式共享集群资源,即"作业队列"。Capacity Scheduler 是根据租户的需要和要求把现有的资源分配给运行的应用程序。Capacity Scheduler 同时允许应用程序访问还没有被使用的资源,以确保队列之间共享其它队列被允许的使用资源。管理员可以控制每个队列的容量,Capacity Scheduler 负责把作业提交到队列中。
17.3. Fair Scheduler
在Fair调度器中,我们不需要预先占用一定的系统资源,Fair调度器会为所有运行的job动态的调整系统资源。如下图所示,当第一个大job提交时,只有这一个job在运行,此时它获得了所有集群资源;当第二个小任务提交后,Fair调度器会分配一半资源给这个小任务,让这两个任务公平的共享集群资源。
需要注意的是,在下图Fair调度器中,从第二个任务提交到获得资源会有一定的延迟,因为它需要等待第一个任务释放占用的Container。小任务执行完成之后也会释放自己占用的资源,大任务又获得了全部的系统资源。最终效果就是Fair调度器即得到了高的资源利用率又能保证小任务及时完成。
公平调度器 Fair Scheduler 最初是由 Facebook 开发设计使得 Hadoop 应用能够被多用户公平地共享整个集群资源,现被 Cloudera CDH 所采用。
Fair Scheduler 不需要保留集群的资源,因为它会动态在所有正在运行的作业之间平衡资源。
17.4. 示例:Capacity调度器配置使用
调度器的使用是通过yarn-site.xml配置文件中的
yarn.resourcemanager.scheduler.class参数进行配置的,默认采用Capacity Scheduler调度器。
假设我们有如下层次的队列:
root
├── prod
└── dev
├── mapreduce
└── spark
下面是一个简单的Capacity调度器的配置文件,文件名为capacity-scheduler.xml。在这个配置中,在root队列下面定义了两个子队列prod和dev,分别占40%和60%的容量。需要注意,一个队列的配置是通过属性yarn.sheduler.capacity.
|
我们可以看到,dev队列又被分成了mapreduce和spark两个相同容量的子队列。dev的maximum-capacity属性被设置成了75%,所以即使prod队列完全空闲dev也不会占用全部集群资源,也就是说,prod队列仍有25%的可用资源用来应急。我们注意到,mapreduce和spark两个队列没有设置maximum-capacity属性,也就是说mapreduce或spark队列中的job可能会用到整个dev队列的所有资源(最多为集群的75%)。而类似的,prod由于没有设置maximum-capacity属性,它有可能会占用集群全部资源。
关于队列的设置,这取决于我们具体的应用。比如,在MapReduce中,我们可以通过mapreduce.job.queuename属性指定要用的队列。如果队列不存在,我们在提交任务时就会收到错误。如果我们没有定义任何队列,所有的应用将会放在一个default队列中。
注意:对于Capacity调度器,我们的队列名必须是队列树中的最后一部分,如果我们使用队列树则不会被识别。比如,在上面配置中,我们使用prod和mapreduce作为队列名是可以的,但是如果我们用root.dev.mapreduce或者dev. mapreduce是无效的。
18. .关于yarn常用参数设置
设置container分配最小内存
yarn.scheduler.minimum-allocation-mb 1024 给应用程序container分配的最小内存
设置container分配最大内存
yarn.scheduler.maximum-allocation-mb 8192 给应用程序container分配的最大内存
设置每个container的最小虚拟内核个数
yarn.scheduler.minimum-allocation-vcores 1 每个container默认给分配的最小的虚拟内核个数
设置每个container的最大虚拟内核个数
yarn.scheduler.maximum-allocation-vcores 32 每个container可以分配的最大的虚拟内核的个数
设置NodeManager可以分配的内存大小
yarn.nodemanager.resource.memory-mb 8192 nodemanager 可以分配的最大内存大小,默认8192Mb
定义每台机器的内存使用大小
yarn.nodemanager.resource.memory-mb 8192
定义交换区空间可以使用的大小
交换区空间就是讲一块硬盘拿出来做内存使用,这里指定的是nodemanager的2.1倍
yarn.nodemanager.vmem-pmem-ratio 2.1