十一、MapReduce开发总结

本文深入探讨MapReduce框架的优化策略,包括大量小文件处理、分区、排序、合并、分组、数据倾斜、计数器及数据压缩等关键环节,旨在提升大数据处理效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、大量小文件的优化策略

  • 默认情况下,每一个输入文件单独切片。切片大小=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和网络流量。
压缩格式对应的编码/解码器
DEFLATEorg.apache.hadoop.io.compress.DefaultCodec
gziporg.apache.hadoop.io.compress.GzipCodec
bzip2org.apache.hadoop.io.compress.BZip2Codec
LZOcom.hadoop.compression.lzo.LzopCodec
LZ4org.apache.hadoop.io.compress.Lz4Codec
Snappyorg.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.compressfalsemapper输出这个参数设为true启用压缩
mapreduce.map.output.compress.codecorg.apache.hadoop.io.compress.DefaultCodecmapper输出使用LZO、LZ4或snappy编解码器在此阶段压缩数据
mapreduce.output.fileoutputformat.compressfalsereducer输出这个参数设为true启用压缩
mapreduce.output.fileoutputformat.compress.codecorg.apache.hadoop.io.compress. DefaultCodecreducer输出使用标准工具或者编解码器,如gzip和bzip2
mapreduce.output.fileoutputformat.compress.typeRECORDreducer输出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); 	
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值