MapReduce框架结构简介(二)--流量统计数据的分析

本文探讨了Hadoop中MapTask和ReduceTask的并行度决定机制,详细解析了切片大小的计算逻辑,以及如何通过自定义Partitioner实现数据按需分组,最后通过一个流量统计案例展示了自定义Partitioner的具体应用。

MapTask并行度决定机制

maptask的并行度决定map阶段的任务处理并发度,进而影响到整个job的处理速度。那么,mapTask并行实例是否越多越好呢?其并行度又是如何决定呢?

  • 一个job的map阶段并行度由客户端在提交job时决定,而客户端对map阶段并行度的规划的基本逻辑为

    • 将待处理数据执行逻辑切片(即按照一个特定切片大小,将待处理数据划分成逻辑上的多个split),然后每一个split分配一个mapTask并行实例处理

    • 切片定义在InputFormat类中的getSplit()方法,默认的切片机制:

      • 简单地按照文件的内容长度进行切片
      • 切片大小,默认等于block大小
      • 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
    • 比如待处理数据有两个文件:

      • file1.txt 320M
      • file2.txt 10M
    • 经过 经过FileInputFormat的切片机制运算后,形成的切片信息如下:

      • file1.txt.split1-- 0~128
      • file1.txt.split2-- 128~256
      • file1.txt.split3-- 256~320
      • file2.txt.split1-- 0~10M
  • Hadoop 中FileInputFormat类中的getSplits代码如下:
    在这里插入图片描述

  • 计算切片大小逻辑代码如下:

      protected long computeSplitSize(long blockSize, long minSize,
                                  long maxSize) {
    return Math.max(minSize, Math.min(maxSize, blockSize));
  }
  • 因此切片大小主要由这几个值来运算决定

    • minsize:默认值:1
      • 配置参数: mapreduce.input.fileinputformat.split.minsize
    • maxsize:默认值:Long.MAXValue
      • 配置参数:mapreduce.input.fileinputformat.split.maxsize
    • blocksize HDFS存储的数据块大小
  • 故而,默认情况下,切片大小=blocksize。maxsize(切片最大值):参数如果调得比blocksize小,则会让切片变小,而且就等于配置的这个参数的值。minsize (切片最小值):参数调的比blockSize大,则可以让切片变得比blocksize还大。

ReduceTask并行度的决定机制

reducetask的并行度同样影响整个job的执行并发度和执行效率,但与maptask的并发数由切片数决定不同,Reducetask数量的决定是可以直接手动设置。

  • job.setNumReduceTasks(4); //默认值是1,手动设置为4
  • 设置ReduceTask时需要指相应数据分区器Partitioner
  • 如果数据分布不均匀,就有可能在reduce阶段产生数据倾斜。注意: reducetask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个reducetask
  • 尽量不要运行太多的reduce task。对大多数job来说,最好rduce的个数最多和集群中的reduce持平,或者比集群的 reduce slots小。这个对于小集群而言,尤其重要。

流量统计需求

根据归属地输出流量统计数据结果到不同文件,以便于在查询统计结果时可以定位到省级范围进行

  • 现有运营商流量日志如下,其中左起第二列为手机号,倒数三列为上行流量,倒数第二列为下行浏览量
1363157985066 	13726230503	00-FD-07-A4-72-B8:CMCC	120.196.100.82	i02.c.aliimg.com		24	27	2481	24681	200
1363157995033 	15920133257	5C-0E-8B-C7-BA-20:CMCC	120.197.40.4	sug.so.360.cn		    20	20	3156	2936	200
  • Mapreduce中会将map输出的kv对,按照相同key分组,然后分发给不同的reducetask默认的分发规则为:根据key的hashcode%reducetask数来分发。所以:如果要按照我们自己的需求进行分组,则需要改写数据分发(分组)组件Partitioner自定义一个ProvincePartitioner继承抽象类:Partitioner。然后在job对象中,设置自定义partitioner job.setPartitionerClass(CustomPartitioner.class)
  • reduceTask默认分发规则如下:
    在这里插入图片描述
  • 自定义ProvincePartitioner:
  import java.util.HashMap;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
/**
 * K2  V2 对应的是map输出的KV类型
 * @author potter
 *
 */
public class ProvincePartitioner extends Partitioner<Text,FlowBean>{
	
	private static HashMap<String , Integer> provinceDict = new HashMap<String , Integer>();
	
	static {
		provinceDict.put("136", 0);
		provinceDict.put("137", 1);
		provinceDict.put("138", 2);
		provinceDict.put("139", 3);
	}

	@Override
	public int getPartition(Text key, FlowBean value, int numPartitions) {

		String prefix = key.toString().substring(0,3);
		Integer provinceId = provinceDict.get(prefix);
		return provinceId == null ? 4:provinceId;
	}
}

  • 自定义流量FlowBean实体类如下:
 import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.WritableComparable;
public class FlowBean implements WritableComparable<FlowBean>{
	private long upFlow;
	private long dFlow;
	private long sumFlow;
	
	public long getUpFlow() {
		return upFlow;
	}
	public void setUpFlow(long upFlow) {
		this.upFlow = upFlow;
	}
	public long getdFlow() {
		return dFlow;
	}
	public void setdFlow(long dFlow) {
		this.dFlow = dFlow;
	}
	
	public long getSumFlow() {
		return sumFlow;
	}
	public void setSumFlow(long sumFlow) {
		this.sumFlow = sumFlow;
	}
	public FlowBean(long upFlow, long dFlow) {
		super();
		this.upFlow = upFlow;
		this.dFlow = dFlow;
	    this.sumFlow = upFlow+dFlow;
	}
	
	//反序列化时需要使用空参构造方法
	public FlowBean() {
	}
	public void setBean(long upFlow,long dFlow){
		this.upFlow = upFlow;
		this.dFlow = dFlow;
		this.sumFlow = upFlow + dFlow;
	}
	/**
	 * 序列化方法
	 */
	@Override
	public void write(DataOutput out) throws IOException {
		// TODO Auto-generated method stub
		out.writeLong(upFlow);
		out.writeLong(dFlow);
		out.writeLong(sumFlow);
	}
	/**
	 * 反序列化方法
	 */
	@Override
	public void readFields(DataInput in) throws IOException {
		// TODO Auto-generated method stub
		this.upFlow = in.readLong();
		this.dFlow = in.readLong();
	     this.sumFlow = in.readLong();
	}
	@Override
	public String toString() {
		return upFlow + "\t" +dFlow + "\t" + sumFlow;
	}
	@Override
	public int compareTo(FlowBean o) {
		return this.sumFlow > o.getSumFlow() ? -1 : 1;
	}
}
  • Map 、Reduce以及相应的Job 代码如下:
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class FlowCount {
	static class FlowCountMapper extends Mapper<LongWritable,Text,Text,FlowBean>{
		protected void map(LongWritable key,Text value , Context context) throws IOException, InterruptedException{
			//将一行内容转化成String
			String line = value.toString();
			//切分字段
			String[] fields = line.split("\t");
			//取出手机号
			String phoneNbr = fields[1];
			//取出上行、下行流量
			long upFlow = Long.parseLong(fields[fields.length-3]);
			long dFlow = Long.parseLong(fields[fields.length-2]);
			
			context.write(new Text(phoneNbr),new FlowBean(upFlow,dFlow));
		}
	}
	/**
	 * 接收的数据格式:<183532421,bean1><183532421,bean2><183532421,bean3><183532421,bean4>
	 * @author potter
	 *
	 */
	static class FlowCountReducer extends Reducer<Text,FlowBean,Text,FlowBean>{
		protected void reduce(Text key,Iterable<FlowBean> values , Context context) throws IOException, InterruptedException{
			long sum_upFlow = 0;
			long sum_dFlow = 0; 
			//遍历所有bean,将其中的上行流量,下行流量分别累加
			for(FlowBean bean : values){
				sum_upFlow += bean.getUpFlow();
				sum_dFlow += bean.getdFlow();
			}
			FlowBean resultBean = new FlowBean(sum_upFlow,sum_dFlow);
			context.write(key, resultBean);
		}
	}
    public static void main(String args[]) throws IOException, Exception{
    	Configuration conf = new Configuration();
    	Job job = Job.getInstance(conf);
    	
    	//指定本程序的jar包所在的本地路径
    	job.setJarByClass(FlowCount.class);
    	
    	//指定本业务job要使用的mapper/Reducer业务类
    	job.setMapperClass(FlowCountMapper.class);
    	job.setReducerClass(FlowCountReducer.class);
    	
    	//指定自定义的数据分区器
    	job.setPartitionerClass(ProvincePartitioner.class);
    	//指定相应分区数量的reducetask
    	job.setNumReduceTasks(5);
    	
    	//指定mapper输出数据的KV类型
    	job.setMapOutputKeyClass(Text.class);
    	job.setMapOutputValueClass(FlowBean.class);
    	
    	//指定最终输出的数据的KV类型
    	job.setOutputKeyClass(Text.class);
    	job.setOutputValueClass(FlowBean.class);
    	
    	//指定job输出的原始文件所在目录
    	FileInputFormat.setInputPaths(job, new Path(args[0]));
    	
    	//指定job输出结果所在目录
    	FileOutputFormat.setOutputPath(job, new Path(args[1]));
    	
    	//将job中配置的相关参数,以及job所用的java类所在的jar包,提交给yarn运行
    	boolean res = job.waitForCompletion(true);
    	System.exit(res?0:1);
    }

}

  • 打jar包放入集群运行,同时在HDFS的 /flowcount/input/ 目录下放入指定的流量日志文件 通过hadoop jar flowcount.jar cn.itcast.bigdata.mr.provinceflow.FlowCount /flowsum/input /flowsum/output2 命令运行jar包,并指定输入输出目录。运行结果如下,产生了5个文件,对应了5个ReduceTask。
    在这里插入图片描述
  • 查看其中的一个结果文件,可以看出手机号以136开头的被统计进了同一文件,即省代号为“0”的,用户流量被统计归入了同一文件。
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值