一、大量小文件的优化策略
- 默认情况下,每一个输入文件单独切片。切片大小=blocksize=128M。每次切片时,都要判断切完剩下的部分是否大于块(128M)的1.1倍,不大于块(128M)的1.1倍就划分一块切片。比如一个257M的文件,就切为2块:128M + 129M
- 默认情况下,TextInputformat对任务的切片机制是按文件规划切片,不管文件多小,都会是一个单独的切片,都会提交一个maptask。如果有大量小文件,就会产生大量的maptask,处理效率极其低下。
优化策略:
- 最好的办法:在数据处理系统的最前端(预处理/采集),就将小文件先合并成大文件,再上传到HDFS做后续分析。
- 补救措施:如果已经是大量小文件在HDFS中了,可以使用另一种Inputformat来做切片(CombineTextInputFormat),它的切片逻辑跟FileInputFormat不同;
- 它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个maptask来处理。
- CombineTextInputFormat,优先满足最小切片大小,不超过最大切片大小。举例:0.5m+1m+0.3m+5m = 2m+4.8m = 2m+4m+0.8m
- 在Driver中如果不设置InputFormat,它默认用的是TextInputformat.class
job.setInputFormatClass(CombineTextInputFormat.class);
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304); //最大切片4M
CombineTextInputFormat.setMinInputSplitSize(job, 2097152); //最小切片2M
二、Partitioner分区
默认partition分区
public class HashPartitioner<K, V> extends Partitioner<K, V> {
/** Use {@link Object#hashCode()} to partition. */
public int getPartition(K key, V value, int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
- 一个hash值与setNumReduceTasks的设置值求余数。getPartition方法返回的分区个数由setNumReduceTasks设置值确定。
- setNumReduceTasks可以随便设置,getPartition方法动态的确定分区个数。
自定义Partitioner步骤
-
问题引出:要求将统计结果按照条件输出到不同文件中(分区)。
-
比如:将统计结果按照手机归属地不同省份输出到不同文件中(分区)
-
参见案例:《八、MapReduce实战篇-流量汇总》
-
(1)自定义类继承Partitioner,重新getPartition()方法
package com.hadoop.mapreduce.flowsum;
import java.util.HashMap;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
/**
* KEY,VALUE对应的是map输出kv的类型
*/
public class FlowSumPartitioner extends Partitioner<Text, FlowSumBean>{
public static HashMap<String, Integer> proviceDict = new HashMap<String, Integer>();
static {
proviceDict.put("136", 0);
proviceDict.put("137", 1);
proviceDict.put("138", 2);
proviceDict.put("139", 3);
}
@Override
public int getPartition(Text key, FlowSumBean value, int numPartitions) {
//截取手机号码前3位
String prefix = key.toString().substring(0, 3);
Integer provinceId = proviceDict.get(prefix);
return provinceId == null ? 4 : provinceId;
}
}
- (2)在job驱动中,设置自定义partitioner:
// 8.指定自定义数据分区
job.setPartitionerClass(FlowSumPartitioner.class);
- (3)自定义partition后,要根据自定义partitioner的逻辑设置相应数量的reduce task
// 9.同时指定相应数量的reduce task
job.setNumReduceTasks(5);
- 如果reduceTask的数量 > getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;
- 如果1 < reduceTask的数量 < getPartition的结果数,则有一部分分区数据无处安放,会Exception;
- 如果reduceTask的数量 = 1,则不管mapTask端输出多少个分区文件,最终结果都交给这一个reduceTask,最终也就只会产生一个结果文件 part-r-00000;
或者setNumReduceTasks个数等于1。setNumReduceTasks需要人为控制,getPartition决定了setNumReduceTasks的个数。 - 例如:假设自定义分区数为5,则
- (1)job.setNumReduceTasks(1);会正常运行,只不过会产生一个输出文件
- (2)job.setNumReduceTasks(2);会报错
- (3)job.setNumReduceTasks(6);大于5,程序会正常运行,会产生空文件
三、compareTo排序
- 排序是MapReduce框架中最重要的操作之一。Map Task和Reduce Task均会对数据(按照key)进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。
- 对于Map Task,它会将处理的结果暂时放到一个缓冲区中,当缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次排序,并将这些有序数据写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行一次合并,以将这些文件合并成一个大的有序文件。
- 对于Reduce Task,它从每个Map Task上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则放到磁盘上,否则放到内存中。如果磁盘上文件数目达到一定阈值,则进行一次合并以生成一个更大文件;如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据写到磁盘上。当所有数据拷贝完毕后,Reduce Task统一对内存和磁盘上的所有数据进行一次合并。
- Mapreduce框架在记录到达reducer之前按键对记录排序,但键所对应的值并没有被排序。
- 比如:将统计结果按照总流量倒序排序。
- 参见案例:《八、MapReduce实战篇-流量汇总》
- 如果需要排序的话,就必须实现WritableComparable接口,然后多重写一个方法(compareTo)。
@Override
public int compareTo(FlowSumBean bean) {
// 从大到小, 当前对象和要比较的对象比, 如果当前对象大, 返回-1, 交换他们的位置
return this.sumFlow > bean.getSumFlow() ? -1 : 1;
}
四、Combiner合并
- 1)combiner是MR程序中Mapper和Reducer之外的一种组件
- 2)combiner组件的父类就是Reducer
- 3)combiner和reducer的区别在于运行的位置:
- Combiner是在每一个maptask所在的节点运行;Reducer是接收全局所有Mapper的输出结果;
- 4)combiner的意义就是对每一个maptask的输出进行局部汇总,以减小网络传输量
- 5)自定义Combiner实现步骤:
(1)自定义一个combiner继承Reducer,重写reduce方法
public class WordcountCombiner extends Reducer<Text, IntWritable, Text, IntWritable>{
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int count = 0;
for(IntWritable v :values){
count = v.get();
}
context.write(key, new IntWritable(count));
}
}
(2)在job中设置:
job.setCombinerClass(WordcountCombiner.class);
- 6)combiner能够应用的前提是不能影响最终的业务逻辑,而且,combiner的输出k-v应该跟reducer的输入k-v类型要对应起来
- 比如:求和、求最大值、求最小值可以应用combiner,但求平均值就不能应用combiner
五、GroupingComparator分组
- 对reduce阶段的数据根据某一个或几个字段进行分组。
- Reducetask进程对每一组相同k的<k,v>组调用一次reduce()方法。
- GroupingComparator分组就是确定一批数据该调用几次reduce()方法。
- 参见案例:《九、MapReduce实战篇-最贵商品》
六、端表合并时数据倾斜的优化策略
- 端表合并可以在reduce端join,也可以在map端join(适用于关联表中有小表的情形)
- 参见案例:《十、MapReduce实战篇-端表合并》
- 如果是多张表的join操作都在reduce阶段完成,reduce端的处理压力太大,map节点的运算负载则很低,资源利用率不高,且在reduce阶段极易产生数据倾斜。
- 数据倾斜原因:比如当小米手机销量非常高,而锤子手机销量非常小的时候。
- reduce端join在处理小米手机就会非常慢,在处理锤子手机就很快,这种现象就叫数据倾斜。
- 解决方案:在map端缓存多张表,提前处理业务逻辑,这样增加map端业务,减少reduce端数据的压力,尽可能的减少数据倾斜。
- 具体办法:采用distributedcache
- (1)在mapper的setup阶段,将文件读取到缓存集合中
- (2)在驱动函数中加载缓存。
七、Counter计数器
- Hadoop为每个作业维护若干内置计数器,以描述多项指标。例如,某些计数器记录已处理的字节数和记录数,使用户可监控已处理的输入数据量和已产生的输出数据量。
- (1)采用枚举的方式统计计数
enum MyCounter{MALFORORMED,NORMAL}
//对枚举定义的自定义计数器加1
context.getCounter(MyCounter.MALFORORMED).increment(1);
- (2)采用计数器组、计数器名称的方式统计
// 计数器
if("hadoop".equals(word)){
context.getCounter("WordCount", "hadoop_count").increment(1);
}else{
context.getCounter("WordCount", "no_hadoop_count").increment(1);
}
- (3)计数结果在程序运行后的控制台上查看。
WordCount
hadoop_count=9
no_hadoop_count=15
八、数据压缩
- 如果磁盘I/O和网络带宽影响了MapReduce作业性能,在任意MapReduce阶段启用压缩都可以改善端到端处理时间并减少I/O和网络流量。
压缩格式 | 对应的编码/解码器 |
---|---|
DEFLATE | org.apache.hadoop.io.compress.DefaultCodec |
gzip | org.apache.hadoop.io.compress.GzipCodec |
bzip2 | org.apache.hadoop.io.compress.BZip2Codec |
LZO | com.hadoop.compression.lzo.LzopCodec |
LZ4 | org.apache.hadoop.io.compress.Lz4Codec |
Snappy | org.apache.hadoop.io.compress.SnappyCodec |
- 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作业形成作业链条时,因为第二个作业的输入也已压缩,所以启用压缩同样有效。
压缩配置参数
- 要在Hadoop中启用压缩,可以配置如下参数(mapred-site.xml文件中):
参数 | 默认值 | 阶段 | 建议 |
---|---|---|---|
io.compression.codecs(在core-site.xml中配置) | org.apache.hadoop.io.compress.DefaultCodec,org.apache.hadoop.io.compress.GzipCodec,org.apache.hadoop.io.compress.BZip2Codec,org.apache.hadoop.io.compress.Lz4Codec | 输入压缩 | Hadoop使用文件扩展名判断是否支持某种编解码器 |
mapreduce.map.output.compress | false | mapper输出 | 这个参数设为true启用压缩 |
mapreduce.map.output.compress.codec | org.apache.hadoop.io.compress.DefaultCodec | mapper输出 | 使用LZO、LZ4或snappy编解码器在此阶段压缩数据 |
mapreduce.output.fileoutputformat.compress | false | reducer输出 | 这个参数设为true启用压缩 |
mapreduce.output.fileoutputformat.compress.codec | org.apache.hadoop.io.compress. DefaultCodec | reducer输出 | 使用标准工具或者编解码器,如gzip和bzip2 |
mapreduce.output.fileoutputformat.compress.type | RECORD | reducer输出 | SequenceFile输出使用的压缩类型:NONE和BLOCK |
在Map输出端采用压缩
// 开启map端输出压缩
conf.setBoolean("mapreduce.map.output.compress", true);
// 设置map端输出压缩方式
conf.setClass("mapreduce.map.output.compress.codec", GzipCodec.class, CompressionCodec.class);
在Reduce输出端采用压缩
// 设置reduce端输出压缩开启
FileOutputFormat.setCompressOutput(job, true);
// 设置压缩的方式
FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);