1、 MapReduce数据压缩
1) 概述
压缩技术能够有效减少底层存储系统(HDFS)读写字节数。压缩提高了网络带宽和磁盘空间的效率。在Hadoop下,尤其是数据规模很大和工作负载密集的情况下,使用数据压缩显得非常重要。在这种情况下,I/O操作和网络数据传输要花大量的时间。还有,Shuffle与Merge过程同样也面临着巨大的I/O压力。
鉴于磁盘I/O和网络带宽是Hadoop的宝贵资源,数据压缩对于节省资源、最小化磁盘I/O和网络传输非常有帮助。不过,尽管压缩与解压操作的CPU开销不高,其性能的提升和资源的节省并非没有代价。
如果磁盘I/O和网络带宽影响了MapReduce作业性能,在任意MapReduce阶段启用压缩都可以改善端到端处理时间并减少I/O和网络流量。
压缩MapReduce的一种优化策略:通过压缩编码对mapper或者reducer的输出进行压缩,以减少磁盘IO,提高MR程序运行速度(但相应增加了cpu运算负担)。
基本原则:
(1)运算密集型的job,少用压缩
(2)IO密集型的job,多用压缩
2) MR支持的压缩编码
3) 采用压缩的位置
压缩可以在MapReduce作用的任意阶段启用。
1)输入压缩:
在有大量数据并计划重复处理的情况下,应该考虑对输入进行压缩。然而,你无须显示指定使用的编解码方式。Hadoop自动检查文件扩展名,如果扩展名能够匹配,就会用恰当的编解码方式对文件进行压缩和解压。否则,Hadoop就不会使用任何编解码器。
2)压缩mapper输出:
当map任务输出的中间数据量很大时,应考虑在此阶段采用压缩技术。这能显著改善内部数据Shuffle过程,而Shuffle过程在Hadoop处理过程中是资源消耗最多的环节。如果发现数据量大造成网络传输缓慢,应该考虑使用压缩技术。可用于压缩mapper输出的快速编解码器包括LZO、LZ4或者Snappy。
- 注:LZO是供Hadoop压缩数据用的通用压缩编解码器。其设计目标是达到与硬盘读取速度相当的压缩速度,因此速度是优先考虑的因素,而不是压缩率。与gzip编解码器相比,它的压缩速度是gzip的5倍,而解压速度是gzip的2倍。同一个文件用LZO压缩后比用gzip压缩后大50%,但比压缩前小25%~50%。这对改善性能非常有利,map阶段完成时间快4倍。
3)压缩reducer输出:
在此阶段启用压缩技术能够减少要存储的数据量,因此降低所需的磁盘空间。当mapreduce作业形成作业链条时,因为第二个作业的输入也已压缩,所以启用压缩同样有效。
2、 压缩/解压缩
0)数据流的压缩和解压缩
package com.atguigu.mapreduce.compress;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.compress.CompressionCodec;
import org.apache.hadoop.io.compress.CompressionCodecFactory;
import org.apache.hadoop.io.compress.CompressionInputStream;
import org.apache.hadoop.io.compress.CompressionOutputStream;
import org.apache.hadoop.util.ReflectionUtils;
public class TestCompress {
public static void main(String[] args) throws Exception {
compress("e:/hello.txt","org.apache.hadoop.io.compress.BZip2Codec");
// decompress("e:/hello.txt.bz2");
}
// 1、压缩
private static void compress(String filename, String method) throws Exception {
// (1)获取输入流
FileInputStream fis = new FileInputStream(new File(filename));
Class codecClass = Class.forName(method);
CompressionCodec codec = (CompressionCodec) ReflectionUtils.newInstance(codecClass, new Configuration());
// (2)获取输出流
FileOutputStream fos = new FileOutputStream(new File(filename + codec.getDefaultExtension()));
CompressionOutputStream cos = codec.createOutputStream(fos);
// (3)流的对拷
IOUtils.copyBytes(fis, cos, 1024*1024*5, false);
// (4)关闭资源
cos.close();
fos.close();
fis.close();
}
// 2、解压缩
private static void decompress(String filename) throws FileNotFoundException, IOException {
// (0)校验是否能解压缩
CompressionCodecFactory factory = new CompressionCodecFactory(new Configuration());
CompressionCodec codec = factory.getCodec(new Path(filename));
if (codec == null) {
System.out.println("cannot find codec for file " + filename);
return;
}
// (1)获取输入流
CompressionInputStream cis = codec.createInputStream(new FileInputStream(new File(filename)));
// (2)获取输出流
FileOutputStream fos = new FileOutputStream(new File(filename + ".decoded"));
// (3)流的对拷
IOUtils.copyBytes(cis, fos, 1024*1024*5, false);
// (4)关闭资源
cis.close();
fos.close();
}
}
1) 在Map输入端采用压缩
输入采用压缩无需额外设置,默认开启。只需要将压缩好的文件,放入输入路径,则在执行mr程序时能自动识别。
可以利用上节压缩出所支持的压缩格式放入wordcount案例的输入目录进行测试。
2) 在Map输出端采用压缩
即使你的MapReduce的输入输出文件都是未压缩的文件,你仍然可以对map任务的中间结果输出做压缩,因为它要写在硬盘并且通过网络传输到reduce节点,对其压缩可以提高很多性能,这些工作只要设置两个属性即可,我们来看下代码怎么设置:
给大家提供的hadoop源码支持的压缩格式有:BZip2Codec 、Lz4Codec、DefaultCodec。
以wordcount案例为例:
public class WordCountDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Configuration configuration = new Configuration();
// 开启map端输出压缩
configuration.setBoolean("mapreduce.map.output.compress", true);
// 设置map端输出压缩方式
configuration.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class, CompressionCodec.class);
Job job = Job.getInstance(configuration);
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(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
boolean result = job.waitForCompletion(true);
System.exit(result ? 1 : 0);
}
}
3) 在Reduce输出端采用压缩
public class WordCountDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
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(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 设置reduce端输出压缩开启
FileOutputFormat.setCompressOutput(job, true);
// 设置压缩的方式
FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);
// FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);
// FileOutputFormat.setOutputCompressorClass(job, Lz4Codec.class);
// FileOutputFormat.setOutputCompressorClass(job, DefaultCodec.class);
boolean result = job.waitForCompletion(true);
System.exit(result?1:0);
}
}
3、 数据清洗
在运行核心业务Mapreduce程序之前,往往要先对数据进行清洗,清理掉不符合要求的数据。清理的过程因为不涉及累加,所以往往只需要运行mapper程序,不需要运行reduce程序。
1) 简单解析版
(1) 需求:
将日志按照空格分隔,去除每条日志中字段长度小于等于11的日志。
(2) 输入数据
194.237.142.21 - - [18/Sep/2013:06:49:18 +0000] "GET /wp-content/uploads/2013/07/rstudio-git3.png HTTP/1.1" 304 0 "-" "Mozilla/4.0 (compatible;)"
183.49.46.228 - - [18/Sep/2013:06:49:23 +0000] "-" 400 0 "-" "-"
163.177.71.12 - - [18/Sep/2013:06:49:33 +0000] "HEAD / HTTP/1.1" 200 20 "-" "DNSPod-Monitor/1.0"
163.177.71.12 - - [18/Sep/2013:06:49:36 +0000] "HEAD / HTTP/1.1" 200 20 "-" "DNSPod-Monitor/1.0"
101.226.68.137 - - [18/Sep/2013:06:49:42 +0000] "HEAD / HTTP/1.1" 200 20 "-" "DNSPod-Monitor/1.0"
101.226.68.137 - - [18/Sep/2013:06:49:45 +0000] "HEAD / HTTP/1.1" 200 20 "-" "DNSPod-Monitor/1.0"
(3) 实现代码:
a) 编写LogMapper
package com.bigdata.log.easy;
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;
// keyout是符合条件的每行数据,用Text表示
public class EasyLogMapper extends Mapper<LongWritable,Text,Text,NullWritable>{
Text k = new Text();
NullWritable v =NullWritable.get();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1 讲每行数据转换为字符串
//194.237.142.21 - - [18/Sep/2013:06:49:18 +0000] "GET /wp-content/uploads/2013/07/rstudio-git3.png HTTP/1.1" 304 0 "-" "Mozilla/4.0 (compatible;)"
String line = value.toString();
// 2 解析每行数据
boolean flag = parseLog(line,context);
if(!flag){ //如果不符合要求,停止每次map方法的运行
return;
}
// 3 保留符合要求的数据,组建kv并写出
k.set(line);
context.write(k,v);
}
private boolean parseLog(String line, Context context) {
String[] split = line.split(" ");
if(split.length > 11){
//设置计数器(计数器组名,计数器名),累加。结果在控制台显示
context.getCounter("logGroup","logTrueCounter").increment(1);
return true;
}else {
context.getCounter("logGroup","logFalseCounter").increment(1);
return false;
}
}
}
b) 编写LogDriver
package com.bigdata.log.easy;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class EasyLogDriver {
public static void main(String[] args) throws Exception {
// 1 创建一个配置对象
Configuration conf = new Configuration();
// 2 通过配置对象获取一个job对象
Job job = Job.getInstance(conf);
// 3 设置job的jar包
job.setJarByClass(EasyLogDriver.class);
// 4 设置job的mapper类,reduce类
job.setMapperClass(EasyLogMapper.class);
// 5 设置mapper的keyout和valueout
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
// 6 设置最终输出数据的keyout和valueout
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// 7 设置输入数据的路径和输出数据的路径
FileInputFormat.setInputPaths(job,new Path(args[0]));
//注意输出目录不能事先存在,必须设置一个不存在的目录,框架会自行创建,否则就会报错
FileOutputFormat.setOutputPath(job,new Path(args[1]));
// 如果没有reduce阶段,需要把reduce task的数量设置为0
job.setNumReduceTasks(0);
// 8 向yarn或者本地yarn模拟器提交任务
boolean res = job.waitForCompletion(true);
System.out.println("是否成功:"+res);
}
}
2) 复杂解析版
(1) 需求:
对web访问日志中的各字段识别切分。
去除日志中不合法的记录。
(2) 实现代码:
a) 定义一个bean,用来记录日志数据中的各数据字段。
package com.bigdata.log.complex;
//194.237.142.21 - - [18/Sep/2013:06:49:18 +0000] "GET /wp-content/uploads/2013/07/rstudio-git3.png HTTP/1.1" 304 0 "-" "Mozilla/4.0 (compatible;)"
public class LogBean {
private String userip;
private String username;
private String timezone;
private String resource;
private String status;
private String size;
private String refer;
private String agent;
private boolean valid;//用于标记某行数据是否合法有效
public String getUserip() {
return userip;
}
public void setUserip(String userip) {
this.userip = userip;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getTimezone() {
return timezone;
}
public void setTimezone(String timezone) {
this.timezone = timezone;
}
public String getResource() {
return resource;
}
public void setResource(String resource) {
this.resource = resource;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getSize() {
return size;
}
public void setSize(String size) {
this.size = size;
}
public String getRefer() {
return refer;
}
public void setRefer(String refer) {
this.refer = refer;
}
public String getAgent() {
return agent;
}
public void setAgent(String agent) {
this.agent = agent;
}
public boolean isValid() {
return valid;
}
public void setValid(boolean valid) {
this.valid = valid;
}
@Override
public String toString() {
return this.userip+"\t"+this.username+"\t"+this.timezone+"\t"+this.resource+"\t"+this.status+"\t"+this.size+"\t"+this.refer+"\t"+this.agent;
}
}
b) 编写LogMapper程序
package com.bigdata.log.complex;
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 ComplexLogMapper extends Mapper<LongWritable,Text,Text,NullWritable>{
Text k = new Text();
NullWritable v = NullWritable.get();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1 将每行数据转换为字符串
String line = value.toString();
//2 解析日志为Logbean
LogBean log = parseLog(line);
//3 如果是非法数据,停止本次map方法的运行
if(!log.isValid()){
return;
}
//4 组建kv并写出
k.set(log.toString());
context.write(k,v);
}
private LogBean parseLog(String line) {
//194.237.142.21 - - [18/Sep/2013:06:49:18 +0000] "GET /wp-content/uploads/2013/07/rstudio-git3.png HTTP/1.1" 304 0 "-" "Mozilla/4.0 (compatible;)"
String[] split = line.split(" ");
LogBean logBean = new LogBean();
if(split.length > 11){
logBean.setUserip(split[0]);
logBean.setUsername(split[1]);
logBean.setTimezone(split[4]);
logBean.setResource(split[6]);
logBean.setStatus(split[8]);
logBean.setSize(split[9]);
logBean.setRefer(split[10]);
if(split.length > 12){
logBean.setAgent(split[11]+split[12]);
}else {
logBean.setAgent(split[11]);
}
if(Integer.parseInt(logBean.getStatus()) >= 400){
logBean.setValid(false);
}else {
logBean.setValid(true);
}
}else {
logBean.setValid(false);
}
return logBean;
}
}
c) 编写LogDriver程序
package com.bigdata.log.complex;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class ComplexLogDriver {
public static void main(String[] args) throws Exception {
// 1 创建一个配置对象
Configuration conf = new Configuration();
// 2 通过配置对象获取一个job对象
Job job = Job.getInstance(conf);
// 3 设置job的jar包
job.setJarByClass(ComplexLogDriver.class);
// 4 设置job的mapper类,reduce类
job.setMapperClass(ComplexLogMapper.class);
// 5 设置mapper的keyout和valueout
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
// 6 设置最终输出数据的keyout和valueout
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// 7 设置输入数据的路径和输出数据的路径
FileInputFormat.setInputPaths(job,new Path(args[0]));
//注意输出目录不能事先存在,必须设置一个不存在的目录,框架会自行创建,否则就会报错
FileOutputFormat.setOutputPath(job,new Path(args[1]));
// 如果没有reduce阶段,需要把reduce task的数量设置为0
job.setNumReduceTasks(0);
// 8 向yarn或者本地yarn模拟器提交任务
boolean res = job.waitForCompletion(true);
System.out.println("是否成功:"+res);
}
}
4、 MapReduce Join关联
所谓的map reduce join是指采用mapreduce程序把多个输入文件关联到一个文件中去并输出,类似于在mysql中,多个表之间的关联。
Map reduce join按照实现不同,分为两类,一类是在reduce端实现的关联,称之为reduce join,另一类是在map端实现的关联,称之为map join。
Map join(合并)
(1) 使用场景
一张表十分小、一张表很大。
MapJoin 适用于有一份数据较小的连接情况。做法是每个Map Task直接把该小份数据直接全部加载到内存当中。然后大份数据就作为 MapTask 的输入,对 map()方法的每次输入都去内存当中直接去匹配join。然后把连接结果按 key 输出,这种方法要使用 hadoop中的 DistributedCache 把小份数据分布到各个计算节点,每个 maptask 执行任务的节点都需要加载该数据到内存。
(2) 具体办法:采用distributedcache
a) 在mapper的setup阶段,将文件读取到缓存集合中。
b) 在驱动函数中加载缓存。
job.addCacheFile(new URI(“file:///D:/test/mapjoinnew/user/user.txt”));// 缓存普通文件到task运行节点,如图所示:
order.txt
20010203001 手机 19156
20010203002 电脑 19157
20010203003 平板 19158
20010203004 手表 19156
20010203005 手环 19157
20010203006 耳机 19158
user.txt
19156 张三
19157 李四
19158 王五
package com.bigdata.mapjoin;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.net.URI;
public class MapJoinDriver {
public static void main(String[] args) throws Exception {
// 1 创建一个配置对象
Configuration conf = new Configuration();
// 2 通过配置对象获取一个job对象
Job job = Job.getInstance(conf);
// 3 设置job的jar包
job.setJarByClass(MapJoinDriver.class);
// 4 设置job的mapper类,reduce类
job.setMapperClass(MapJoinMapper.class);
// 5 设置mapper的keyout和valueout
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
// 6 设置最终输出数据的keyout和valueout
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// 7 设置输入数据的路径和输出数据的路径
FileInputFormat.setInputPaths(job,new Path(args[0]));
//注意输出目录不能事先存在,必须设置一个不存在的目录,框架会自行创建,否则就会报错
FileOutputFormat.setOutputPath(job,new Path(args[1]));
job.addCacheFile(new URI("file:///D:/test/mapjoin/user/user.txt"));
//设置为0,表示本mr程序没有reduce 阶段
job.setNumReduceTasks(0);
// 8 向yarn或者本地yarn模拟器提交任务
boolean res = job.waitForCompletion(true);
System.out.println("是否成功:"+res);
}
}
package com.bigdata.mapjoin;
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.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
// 之前在给订单加username的时候,是在reduce端完成的,称之为reducejoin,但这种join的方式有缺点,reduce 要从每个map task所在
// 的机器下载大量的数据,导致处理较慢,还有可能导致reduce task的所在机器内存溢出(oom),
// 不光本案例,写其他mr程序的时候,也要把握一个原则,就是尽量把大量的业务逻辑运算放在map端完成,只让reduce端进行一些简单的结果全局合并
// 现在,我们把给订单加username的动作放在map完成。
public class MapJoinMapper extends Mapper<LongWritable,Text,Text,NullWritable>{
Map<String,String> userMap = new HashMap<>();
// 该方法只在map任务执行之前执行一次,用于为任务做准备工作,在本案例中,我们在这个方法里面读取用户的信息
// 把用户信息存储到内存里面,也就是读取user.txt 把用户信息放到Map集合里面 Map<userid,username>
@Override
protected void setup(Context context) throws IOException, InterruptedException {
URI[] cacheFiles = context.getCacheFiles();
String path = cacheFiles[0].getPath().toString();
// 读取每一行用户信息文件,将其放到Map集合
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("D:/test/mapjoin/user/user.txt"), "UTF-8"));
String line = null;
while((line = br.readLine()) != null){
//19156 张三
String[] split = line.split("\t");
String userid = split[0];
String username = split[1];
userMap.put(userid,username);
}
br.close();
}
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 20010203001 手机 19156
// 1 将每行数据转为字符串
String line = value.toString();
// 2 按照tab切分,挑出userid
String[] split = line.split("\t");
// 3 根据userid,去userMap找到对应的username
String userid = split[2];
String username = userMap.get(userid);
context.write(new Text(split[0]+"\t"+split[1]+"\t"+username),NullWritable.get());
}
}
5、 Yarn
1、 Yarn概述
Yarn是一个资源调度平台,负责为运算程序提供服务器运算资源,相当于一个分布式的操作系统平台,而MapReduce等运算程序则相当于运行于操作系统之上的应用程序。
2、 Yarn基本架构
YARN主要由ResourceManager、NodeManager、ApplicationMaster(AM)和Container
3、 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申请容器,运行reduce task。
(14)reduce task向maptask获取相应分区的数据。
(15)程序运行完毕后,MR会向RM申请注销自己。
4、 资源调度器
理想情况下,我们应用对Yarn资源的请求应该立刻得到满足,但现实情况资源往往是有限的,特别是在一个很繁忙的集群,一个应用资源的请求经常需要等待一段时间才能的到相应的资源。在Yarn中,负责给应用分配资源的就是Scheduler。其实调度本身就是一个难题,很难找到一个完美的策略可以解决所有的应用场景。为此,Yarn提供了多种调度器和可配置的策略供我们选择。
目前,Hadoop作业调度器主要有三种:FIFO、Capacity Scheduler和Fair Scheduler。Hadoop2.7.2默认的资源调度器是Capacity Scheduler,可以在yarn-default.xml文件中查看。
(1) 先进先出调度器(FIFO),如图所示:
FIFO Scheduler把应用按提交的顺序排成一个队列,这是一个先进先出队列,在进行资源分配的时候,先给队列中最头上的应用进行分配资源,待最头上的应用需求满足后再给下一个分配,以此类推。
FIFO Scheduler是最简单也是最容易理解的调度器,不需要做复杂的配置只需要在yarn-site.xml文件中设置即可,但它并不适用于共享集群。大的应用可能会占用所有集群资源,这就导致其它应用被阻塞。在共享集群中,更适合采用Capacity Scheduler或Fair Scheduler,这两个调度器都允许大任务和小任务在提交的同时获得一定的系统资源。
在yarn-site.xml文件中添加如下设置:
FIFO调度器设置
<property>
<description>The class to use as the resource scheduler.</description>
<name>yarn.resourcemanager.scheduler.class</name>
<value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.fifo.FifoScheduler</value>
</property>
在hadoop003修改完成之后,要将该配置分发到hadoop004,hadoop005,并重启hdfs,yarn集群。
[root@hadoop003 hadoop-2.7.2]# scp etc/hadoop/yarn-site.xml hadoop004:/opt/module/hadoop-2.7.2/etc/hadoop/
[root@hadoop003 hadoop-2.7.2]# scp etc/hadoop/yarn-site.xml hadoop005:/opt/module/hadoop-2.7.2/etc/hadoop/
可以将wordcount案例中的输入文件准备200个,作为大任务先提交,在提交一个只有一个输入文件的小任务,发现小任务只能在大任务执行完毕后才能执行。
(2) 容量调度器(Capacity Scheduler)
在FIFO 调度器中,小任务会被先提交的大任务阻塞。那么有没有一种调度方式,可以同时运行先提交的大任务和后提交的小任务呢?有的,可以使用容量调度器,配置多个队列,把两个任务提交到不同队列就可以了。
Capacity 调度器允许创建多个队列分给多个组织以共享整个集群,每个组织可以获得集群的一部分计算能力。通过为每个组织分配专门的队列,然后再为每个队列分配一定的集群资源,这样整个集群就可以通过设置多个队列的方式给多个组织提供服务了。在一个队列内部,资源的调度是采用的是先进先出(FIFO)策略。
在yarn-site.xml文件中添加如下设置:
Capacity Scheduler调度器设置
<property>
<description>The class to use as the resource scheduler.</description>
<name>yarn.resourcemanager.scheduler.class</name>
<value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler</value>
</property>
然后,在etc/hadoop/capacity-scheduler.xml文件中进行多个队列的配置。
<!--
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. See accompanying LICENSE file.
-->
<configuration>
<property>
<name>yarn.scheduler.capacity.maximum-applications</name>
<value>10000</value>
<description>
Maximum number of applications that can be pending and running.
</description>
</property>
<property>
<name>yarn.scheduler.capacity.maximum-am-resource-percent</name>
<value>0.1</value>
<description>
Maximum percent of resources in the cluster which can be used to run
application masters i.e. controls number of concurrent running
applications.
</description>
</property>
<property>
<name>yarn.scheduler.capacity.resource-calculator</name>
<value>org.apache.hadoop.yarn.util.resource.DefaultResourceCalculator</value>
<description>
The ResourceCalculator implementation to be used to compare
Resources in the scheduler.
The default i.e. DefaultResourceCalculator only uses Memory while
DominantResourceCalculator uses dominant-resource to compare
multi-dimensional resources such as Memory, CPU etc.
</description>
</property>
<property>
<name>yarn.scheduler.capacity.root.queues</name>
<value>test,dev</value>
<description>
The queues at the this level (root is the root queue).
</description>
</property>
<property>
<name>yarn.scheduler.capacity.root.dev.queues</name>
<value>java,bigdata</value>
<description>
The queues at the this level (root is the root queue).
</description>
</property>
<property>
<name>yarn.scheduler.capacity.root.test.capacity</name>
<value>40</value>
<description>Default queue target capacity.</description>
</property>
<property>
<name>yarn.scheduler.capacity.root.dev.capacity</name>
<value>60</value>
<description>Default queue target capacity.</description>
</property>
<property>
<name>yarn.scheduler.capacity.root.dev.maximum-capacity</name>
<value>75</value>
<description>
The maximum capacity of the default queue.
</description>
</property>
<property>
<name>yarn.scheduler.capacity.root.dev.java.capacity</name>
<value>50</value>
<description>Default queue target capacity.</description>
</property>
<property>
<name>yarn.scheduler.capacity.root.dev.bigdata.capacity</name>
<value>50</value>
<description>Default queue target capacity.</description>
</property>
<property>
<name>yarn.scheduler.capacity.root.dev.java.state</name>
<value>STOPPED</value>
<description>
The state of the queue. State can be one of RUNNING or STOPPED.
</description>
</property>
<property>
<name>yarn.scheduler.capacity.root.default.user-limit-factor</name>
<value>1</value>
<description>
Default queue user limit a percentage from 0.0 to 1.0.
</description>
</property>
<property>
<name>yarn.scheduler.capacity.root.default.acl_submit_applications</name>
<value>*</value>
<description>
The ACL of who can submit jobs to the default queue.
</description>
</property>
<property>
<name>yarn.scheduler.capacity.root.default.acl_administer_queue</name>
<value>*</value>
<description>
The ACL of who can administer jobs on the default queue.
</description>
</property>
<property>
<name>yarn.scheduler.capacity.node-locality-delay</name>
<value>40</value>
<description>
Number of missed scheduling opportunities after which the CapacityScheduler
attempts to schedule rack-local containers.
Typically this should be set to number of nodes in the cluster, By default is setting
approximately number of nodes in one rack which is 40.
</description>
</property>
<property>
<name>yarn.scheduler.capacity.queue-mappings</name>
<value></value>
<description>
A list of mappings that will be used to assign jobs to queues
The syntax for this list is [u|g]:[name]:[queue_name][,next mapping]*
Typically this list will be used to map users to queues,
for example, u:%user:%user maps all users to queues with the same name
as the user.
</description>
</property>
<property>
<name>yarn.scheduler.capacity.queue-mappings-override.enable</name>
<value>false</value>
<description>
If a queue mapping is present, will it override the value specified
by the user? This can be used by administrators to place jobs in queues
that are different than the one specified by the user.
The default is false.
</description>
</property>
</configuration>
在hadoop003修改完成之后,要将该配置分发到hadoop004,hadoop005,并重启hdfs,yarn集群。
[root@hadoop003 hadoop-2.7.2]# scp etc/hadoop/yarn-site.xml hadoop004:/opt/module/hadoop-2.7.2/etc/hadoop/
[root@hadoop003 hadoop-2.7.2]# scp etc/hadoop/yarn-site.xml hadoop005:/opt/module/hadoop-2.7.2/etc/hadoop/
[root@hadoop003 hadoop-2.7.2]# scp etc/hadoop/capacity-scheduler.xml hadoop004:/opt/module/hadoop-2.7.2/etc/hadoop/
[root@hadoop003 hadoop-2.7.2]# scp etc/hadoop/capacity-scheduler.xml hadoop005:/opt/module/hadoop-2.7.2/etc/hadoop/
我们可以看到,dev队列又被分成了java和bigdata两个相同容量的子队列。dev的maximum-capacity属性被设置成了75%,所以即使test队列完全空闲dev也不会占用全部集群资源,也就是说,test队列仍有25%的可用资源用来应急。我们注意到,java和bigdata两个队列没有设置maximum-capacity属性,也就是说java或bigdata队列中的job可能会用到整个dev队列的所有资源(最多为集群的75%)。而类似的,test由于没有设置maximum-capacity属性,它有可能会占用集群全部资源。
测试:可以把刚刚准备的大任务提交到bigdata队列,小任务提交到test队列,发现能够同时执行。
注意:在将mr程序提交到容量调度器的队列时,必须给mr程序制定要提交的队列名称,而且队列的名称不能写队列的全名称,例如root.bigdata,而只能写bigdata。
Configuration conf = new Configuration();
conf.set("mapreduce.job.queuename", "bigdata");
(3) 公平调度器(Fair Scheduler)
在FIFO 调度器中,小任务会被大任务阻塞。
而对于Capacity调度器,有一个专门的队列用来运行小任务,但是为小任务专门设置一个队列会预先占用一定的集群资源,这就导致大任务的执行时间会落后于使用FIFO调度器时的时间。
在Fair调度器中,我们不需要预先占用一定的系统资源,Fair调度器会为所有运行的job动态的调整系统资源。当第一个大job提交时,只有这一个job在运行,此时它获得了所有集群资源;当第二个小任务提交后,Fair调度器会分配一半资源给这个小任务,让这两个任务公平的共享集群资源。
在yarn-site.xml文件中进行修改。
Fair Scheduler调度器设置
<property>
<description>The class to use as the resource scheduler.</description>
<name>yarn.resourcemanager.scheduler.class</name>
<value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler</value>
</property>
公平调度器支持多个队列,每个队列也支持多种调度方式,需要在配置文件 etc/hadoop/fair-scheduler.xml进行修改。需要创建该文件。
在这里配置的所有队列都是root的字队列,每个队列都可以单独指定
<?xml version="1.0"?>
<allocations>
<defaultQueueSchedulingPolicy>fair</defaultQueueSchedulingPolicy>
<queue name="test">
<weight>40</weight>
<schedulingPolicy>fifo</schedulingPolicy>
</queue>
<queue name="dev">
<weight>60</weight>
<queue name="java" />
<queue name="bigdata" />
</queue>
<queuePlacementPolicy>
<rule name="specified" />
<rule name="user" />
</queuePlacementPolicy>
</allocations>
在hadoop003修改完成之后,要将该配置分发到hadoop004,hadoop005,并重启hdfs,yarn集群。
[root@hadoop003 hadoop-2.7.2]# scp etc/hadoop/yarn-site.xml hadoop004:/opt/module/hadoop-2.7.2/etc/hadoop/
[root@hadoop003 hadoop-2.7.2]# scp etc/hadoop/yarn-site.xml hadoop005:/opt/module/hadoop-2.7.2/etc/hadoop/
[root@hadoop003 hadoop-2.7.2]# scp etc/hadoop/fair-scheduler.xml hadoop004:/opt/module/hadoop-2.7.2/etc/hadoop/
[root@hadoop003 hadoop-2.7.2]# scp etc/hadoop/fair-scheduler.xml hadoop005:/opt/module/hadoop-2.7.2/etc/hadoop/
用户提交的任务进入哪个队列,根据queuePlacementPolicy里面规则来判断。
如果任务明确指定了队列,则进入指定的队列,否则根据用户的名字临时创建一个队列,这个用户的所有应用共享这个队列。
或者采取如下方式设置,所有应用都进入到default队列,实现所有用户的所有应用共享这个队列。
<queuePlacementPolicy>
<rule name="default" />
</queuePlacementPolicy>
6、 Hadoop企业优化
1、 MapReduce跑的慢的原因
MapReduce 程序效率的瓶颈在于两点:
计算机性能
CPU、内存、磁盘健康、网络
I/O 操作优化
(1)数据倾斜
(2)map和reduce数设置不合理 combineTextinputformat,分区
(3)map运行时间太长,导致reduce等待过久
(4)小文件过多
(5)spill(溢出)次数过多,溢出数据到磁盘
(6)merge次数过多等。Shuffule,溢出之后会有合并,reduce端也会有合并
2、 MapReduce优化方法
MapReduce优化方法主要从六个方面考虑:数据输入、Map阶段、Reduce阶段、IO传输、数据倾斜问题和常用的调优参数。
1) 数据输入
(1)合并小文件:在执行MR任务前将小文件进行合并,大量的小文件会产生大量的map任务,大量节点资源被占用,从而导致MR整体运行较慢。
(2)采用CombineTextInputFormat来作为输入,解决输入端大量小文件场景。
2) Map阶段
(1) 减少溢写(spill)次数:通过调整io.sort.mb及sort.spill.percent参数值(在mapred-default.xml),增大触发spill的内存上限,减少spill次数,从而减少磁盘IO。
(2) 减少合并(merge)次数:通过调整io.sort.factor参数(在mapred-default.xml),增大merge的文件数目,减少merge的次数,从而缩短MR处理时间。
(3) 在map之后,不影响业务逻辑前提下,先进行combiner处理,减少 I/O。
3) Reduce阶段
(1) 合理设置map和reduce数:两个都不能设置太少,也不能设置太多。太少,会导致task等待,延长处理时间;太多,会导致 map、reduce任务间竞争资源,造成处理超时等错误。
(2) 设置map、reduce共存:调整slowstart.completedmaps参数,使map运行到一定程度后,reduce也开始运行,减少reduce的等待时间。
(3) 合理设置reduce端的buffer:默认情况下,数据达到一个阈值的时候,buffer中的数据就会写入磁盘,然后reduce会从磁盘中获得所有的数据。也就是说,buffer和reduce是没有直接关联的,中间多个一个写磁盘->读磁盘的过程,既然有这个弊端,那么就可以通过参数来配置,使得buffer中的一部分数据可以直接输送到reduce,从而减少IO开销:mapred.job.reduce.input.buffer.percent,默认为0.0。当值大于0的时候,会保留指定比例的内存读buffer中的数据直接拿给reduce使用。这样一来,设置buffer需要内存,读取数据需要内存,reduce计算也要内存,所以要根据作业的运行情况进行调整。
4) 数据倾斜问题
(1) 数据倾斜现象
数据频率倾斜——某一个区域的数据量要远远大于其他区域。
(2) 减少数据倾斜的方法
方法1:自定义分区
基于输出键的背景知识进行自定义分区。例如,如果map输出键的单词来源于一本书。且其中某几个专业词汇较多。那么就可以自定义分区将这这些专业词汇发送给固定的一部分reduce实例。而将其他的都发送给剩余的reduce实例。
方法2:Combine
使用Combine可以大量地减小数据倾斜。在可能的情况下,combine的目的就是聚合并精简数据。
方法3:采用Map Join,尽量避免Reduce Join。
5)i/o传输
采用数据压缩方式,安装snappy和lzo压缩编码器
7、 常见错误及解决方案
(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.bigdata.mapreduce.wordcount.WordCountDriver /user/bigdata/ /user/bigdata/output
报如下错误:
Exception in thread “main” java.lang.UnsupportedClassVersionError: com/bigdata/mapreduce/wordcount/WordCountDriver : Unsupported major.minor version 52.0
原因是Windows环境用的jdk1.7,Linux环境用的jdk1.8。
解决方案:统一jdk版本。
(6) 缓存user.txt小文件案例中,报找不到user.txt文件。
原因:大部分为路径书写错误。还有就是要检查user.txt的问题。还有个别电脑写相对路径找不到user.txt,可以修改为绝对路径。
(7) 报类型转换异常。
通常都是在驱动函数中设置map输出和最终输出时编写错误。
Map输出的key如果没有排序,也会报类型转换异常。
(8) 集群中运行wc.jar时出现了无法获得输入文件。
原因:wordcount案例的输入文件不能放在hdfs集群的根目录。