Hadoop学习第三天 mapreduce的原理和编程

MapReduce概述

最简单的单机问题一旦涉及到大体量级迁移到分布式系统中的时候都会涉及到方方面面的各种编程,需要考虑的东西太多,为解决每次都要考虑这些东西提炼出MapReduce框架。目的就是将分布式编程像本地编程一样。

  1. MapReduce是一种分布式计算模型,由Google提出,主要用于搜索领域,解决海量数据的计算问题.
  2. MR由两个阶段组成:MapReduce,用户只需要实现map()和reduce()两个函数,即可实现分布式计算,非常简单。这两个函数的形参是key、value对,表示函数的输入信息。

MR过程各个角色的作用

  1. jobClient:提交作业
  1. 是用户作业与JobTracker交互的主要接口。负责提交作业的,负责启动、跟踪任务执行、访问任务状态和日志等。
  1. JobTracker:初始化作业,分配作业,TaskTracker与其进行通信,协调监控整个作业
  1. 负责接收用户提交的作业,负责启动、跟踪任务执行。
  2. JobSubmissionProtocol 是 JobClient 与 JobTracker 通信的接口。
  3. InterTrackerProtocol 是 TaskTracker 与 JobTracker 通信的接口。
  1. TaskTracker:定期与JobTracker通信,执行Map和Reduce任务。
  1. 负责执行任务,若干Task。
  1. HDFS:保存作业的数据、配置、jar包、结果

作业提交

  1. 提交作业之前,需要对作业进行配置,编写自己的MR程序,配置作业,包括输入输出路径等等.
  2. 提交作业 配置完成后,通过JobClient提交,具体功能,与JobTracker通信得到一个jar的存储路径和JobID,输入输出路径检查,将job jar拷贝到的HDFS,计算输入分片,将分片信息写入到job.split中,写job.xml,真正提交作业。

作业初始化

客户端提交作业后,JobTracker会将作业加入到队列,然后进行调度,默认是FIFO方式
具体功能:作业初始化主要是指JobInProgress中完成的
读取分片信息
创建task包括Map和Reduce任创建task包括Map和Reduce任务
创建TaskInProgress执行task,包括map任务和reduce任务

任务分配

  1. TaskTrackerJobTracker之间的通信和任务分配是通过心跳机制实现的
  2. TaskTracker会主动定期向JobTracker发送心态信息,询问是否有任务要做,如果有,就会申请到任务。

任务执行

  1. 如果TaskTracker拿到任务,会将所有的信息拷贝到本地,包括代码、配置、分片信息等
  2. TaskTracker中的==localizeJob()==方法会被调用进行本地化,拷贝job.jar,jobconf,job.xml到本地
  3. TaskTracker调用==launchTaskForJob()==方法加载启动任务
  4. MapTaskRunner和ReduceTaskRunner分别启动java child进程来执行相应的任务

状态更新

  1. Task会定期向TaskTraker汇报执行情况
  2. TaskTracker会定期收集所在集群上的所有Task的信息,并向JobTracker汇报
  3. JobTracker会根据所有TaskTracker汇报上来的信息进行汇总

作业完成

  1. JobTracker是在接收到最后一个任务完成后,才将任务标记为成功。将数结果据写入到HDFS中

错误处理

  1. JobTracker失败 存在单点故障,hadoop2.0解决了这个问题
  2. TaskTracker失败
  1. TaskTracker崩溃了会停止向JobTracker发送心跳信息。
  2. JobTracker会将TraskTracker从等待的任务池中移除,并将该任务转移到其他的地方执行
  3. JobTracker将TaskTracker加入到黑名单中
  1. Task失败 任务失败,会向TraskTracker抛出异常,任务挂起

MapReduce 原理

在这里插入图片描述
在这里插入图片描述
执行步骤:

  1. map任务处理

    1. 读取输入文件内容,解析成key、value对。对输入文件的每一行,解析成key、value对。每一个键值对调用一次map函数。
    2. 写自己的逻辑,对输入的key、value处理,转换成新的key、value输出。
  2. reduce任务处理

    1. 在reduce之前,有一个shuffle的过程对多个map任务的输出进行合并、排序。
    2. 写reduce函数自己的逻辑,对输入的key、value处理,转换成新的key、value输出。
    3. 把reduce的输出保存到文件中。
      例子:实现WordCountApp
      在这里插入图片描述

Mapper

package com.demo.mywordcount;

import java.io.IOException;

import org.apache.commons.lang.StringUtils;
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 javax.naming.Context;

//4个泛型中,前两个是指定mapper输入数据的类型,KEYIN是输入的key的类型,VALUEIN是输入的value的类型
//map 和 reduce 的数据输入输出都是以 key-value对的形式封装的
//默认情况下,框架传递给我们的mapper的输入数据中,key是要处理的文本中一行的起始偏移量,这一行的内容作为value
public class WCMapper extends Mapper<LongWritable, Text, Text, LongWritable>{

	//mapreduce框架每读一行数据就调用一次该方法
	@Override
	protected void map(LongWritable key, Text value,Context context)
			throws IOException, InterruptedException {
		//具体业务逻辑就写在这个方法体中,而且我们业务要处理的数据已经被框架传递进来,在方法的参数中 key-value
		//key 是这一行数据的起始偏移量     value 是这一行的文本内容

		//将这一行的内容转换成string类型
		String line = value.toString();

		//对这一行的文本按特定分隔符切分
		String[] words = StringUtils.split(line, " ");

		//遍历这个单词数组输出为kv形式  k:单词   v : 1
		for(String word : words){
			context.write(new Text(word), new LongWritable(1));
		}
	}
}

Reducer

package com.demo.mywordcount;

import java.io.IOException;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

public class WCReducer extends Reducer<Text, LongWritable, Text, LongWritable>{

	//框架在map处理完成之后,将所有kv对缓存起来,进行分组,然后传递一个组<key,valus{}>,调用一次reduce方法
	//<hello,{1,1,1,1,1,1.....}>
	@Override
	protected void reduce(Text key, Iterable<LongWritable> values,Context context)
			throws IOException, InterruptedException {
		long count = 0;
		//遍历value的list,进行累加求和
		for(LongWritable value:values){
			count += value.get();
		}
		//输出这一个单词的统计结果
		context.write(key, new LongWritable(count));
	}
}

Worker

package com.demo.mywordcount;

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.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

/**
 * 用来描述一个特定的作业
 * 比如,该作业使用哪个类作为逻辑处理中的map,哪个作为reduce
 * 还可以指定该作业要处理的数据所在的路径
 * 还可以指定改作业输出的结果放到哪个路径
 * @author ljj
 */
public class WCRunner {

	public static void main(String[] args) throws Exception {

		Configuration conf = new Configuration();
		Job wcjob = Job.getInstance(conf);

		//设置整个job所用的那些类在哪个jar包
		wcjob.setJarByClass(WCRunner.class);

		//本job使用的mapper和reducer的类
		wcjob.setMapperClass(WCMapper.class);
		wcjob.setReducerClass(WCReducer.class);

		//指定reduce的输出数据kv类型
		wcjob.setOutputKeyClass(Text.class);
		wcjob.setOutputValueClass(LongWritable.class);

		//指定mapper的输出数据kv类型
		wcjob.setMapOutputKeyClass(Text.class);
		wcjob.setMapOutputValueClass(LongWritable.class);

		//指定要处理的输入数据存放路径
		FileInputFormat.setInputPaths(wcjob, new Path("hdfs://weekend110:9000/wc/srcdata/"));

		//指定处理结果的输出数据存放路径
		FileOutputFormat.setOutputPath(wcjob, new Path("hdfs://weekend110:9000/wc/output3/"));

		//将job提交给集群运行
		wcjob.waitForCompletion(true);
	}
}

MR 任务的本地运行模式

一般情况下我们是写好jar包,然后将文件传到服务器,然后通过 hadoop jar *.jar com.demo.MaincClass 来实现分布式任务提交的,但是有时候我们也可以在本地进行任务的提交。将conf 配置设置为空,或者本地代码中没有hdfs-site.xml 跟 mapred-site.xml 那么会自动在本地的IDE(eclipse/IDEA) 中运行,此时可以在mapper跟reducer中打断点进行debug调试。

  1. 本地调试时候 输入输出文件路径可以是本地指定的目录
  2. 本地调试的时候,如果本地可以连接HDFS 也可以指定HDFS上的输入输出文件路径。 要理解 HDFS只是做个分布式文件存储而已。不过如果本地调用HDFS层文件涉及到文件传输,还是不如分布式好。

YARN

yarn是一个资源调度平台,负责为运算程序提供服务器运算资源,作业监督,相当于一个分布式操作系统平台,而mapreduce等运算程序则相当于运行与操作系统之上的应用程序。yarn在hadoop2.x系列中被加入的资源管理器,取代hadoop1.x中的jobtracker,将资源管理与作业调度分离

yarn的重要概念

  1. yarn不需要清楚用户提交程序的运行机制。
  2. yarn只负责提供运算资源(为运算程序提供一个容器,容器的资源由yarn负责分配)。
  3. yarn的实现是主从关系,主动角色为ResourceManager、被动角色为== NodeManager== 负责提供运算资源。
  4. yarn与用户进程完全解耦,意味着yarn上可以运行各种类型的分布式运算程序,如mapreduce、storm、spark和tez等。所以通过yarn提供的接口同样可以实现自己的运算框架。
  5. yarn是一个通用的资源调度平台,从此,生产中存在各种运算集群都可以整合在一个物理集群上,提高资源利用率,方便数据共享。

Yarn进程

  1. ResourceManager ------>yarn的老大
  2. NodeManager ------>yarn的小弟
  3. ResourceManager调度器 a.默认调度器------>先进先出FIFO,b.公平调度器------>每个任务都有执行的机会
  4. 心跳机制 ------>NodeManager可通过心跳机制将节点健康状况实时汇报给ResourceManager,而ResourceManager则会根据每个NodeManager的健康状况适当调整分配的任务数目。当NodeManager认为自己的健康状况欠佳时,可让ResourceManager不再分配任务,待健康状况好转时,再分配新任务。
  5. NodeManager子进程------>独立于NodeManager,不在NodeManager内部
    其中,ResourceManager负责将集群的资源分配给各个应用使用,而资源分配和调度的基本单位是Container,其中封装了机器资源,如内存、CPU、磁盘和网络等,每个任务会被分配一个Container,该任务只能在该Container中执行,并使用该Container封装的资源。NodeManager是一个个的计算节点,主要负责启动Application所需的Container,监控资源(内存、CPU、磁盘和网络等)的使用情况并将之汇报给ResourceManager。ResourceManager与NodeManagers共同组成整个数据计算框架,ApplicationMaster与具体的Application相关,主要负责同ResourceManager协商以获取合适的Container,并跟踪这些Container的状态和监控其进度。参考

MapReduce任务提交大概流程图

在这里插入图片描述

1.执行MR的命令

hadoop jar <jar在linux的路径> <main方法所在的类的全类名> <参数>
hadoop jar /root/wc1.jar cn.itcast.d3.hadoop.mr.WordCount hdfs://weekend110:9000/words /out2

2. MR执行流程

  1. 客户端提交一个mr的jar包给JobClient(提交方式:hadoop jar …),此时客户端会产生一个RunJar进程进行任务的提交跟Resource Manager 通讯 申请一个job
    2. JobClient通过RPC和JobTracker进行通信,返回一个存放jar包的地址(HDFS)和jobId,Resource Manager 返回给RunJar 一个staging-dir 跟 本任务jobID
    3. client将jar包写入到HDFS当中(path = hdfs上的地址 + jobId),客户端将代码等配置资源提交到 staging-dir/jobID
    4. 开始提交任务(任务的描述信息,不是jar, 包括jobid,jar存放的位置,配置信息等等),RunJar告知RM代码提交结果。
    5. JobTracker进行初始化任务,将本job加入到任务队列。其中加入的时候会 读取HDFS上的要处理的文件,开始计算输入分片,每一个分片对应一个MapperTask
    6. TaskTracker通过心跳机制领取任务(任务的描述信息) 下载所需的jar,配置文件等
    7. 领取到任务的node manager 会为本次任务分配运行资源容器 包括CPU 内存等信息。
    8. ResourceManager 会随选一个NodeManager来启动MapReduce 的主进程 MPAppMaster
    9. 该主进程会向ResourceManager 进行任务的注册
    10. 找容器启动map任务进程, 在map Tasker 中会启动 yarnChild
    11. map完毕后会启动Reduce task。
    12. job完成后会将结果写入HDFS中,然后向ResourceManager 告知任务结束。

第三跟第四步操作截图如下:
在这里插入图片描述
任务提交过程后台进行查询截图如下
在这里插入图片描述
==MRAppMaster ==管理 maptask、reducetask 还负责监控任务,有maptask 任务太慢的时候,MRAppMaster 会 将该任务复制一个在另外一个机器上 ,那个先跑完就完毕了。
==RunJar ==负责跟ResourceManager交流则内部一定通过RPC 任务提交。

Job 任务提交源码分析

在这里插入图片描述

MR程序的几种提交运行模式

本地模型运行

linux

在windows的eclipse里面直接运行main方法,就会将job提交给本地执行器localjobrunner执行
----输入输出数据可以放在本地路径下(c:/wc/srcdata/)
----输入输出数据也可以放在hdfs中(hdfs://weekend110:9000/wc/srcdata)

windows

在linux的eclipse里面直接运行main方法,但是不要添加yarn相关的配置,也会提交给localjobrunner执行
----输入输出数据可以放在本地路径下(/home/hadoop/wc/srcdata/)
----输入输出数据也可以放在hdfs中(hdfs://weekend110:9000/wc/srcdata)

集群模式运行

jar

将工程打成jar包,上传到服务器,然后用hadoop命令提交 hadoop jar wc.jar cn.itcast.hadoop.mr.wordcount.WCRunner

linux

在linux的eclipse中直接运行main方法,也可以提交到集群中去运行,但是,必须采取以下措施:
----在工程src目录下加入 mapred-site.xml 和 yarn-site.xml
----将工程打成jar包(wc.jar),同时在main方法中添加一个conf的配置参数 conf.set(“mapreduce.job.jar”,“wc.jar”);

windows

在windows的eclipse中直接运行main方法,也可以提交给集群中运行,但是因为平台不兼容,需要做很多的设置修改
----要在windows中存放一份hadoop的安装包(解压好的)
----要将其中的lib和bin目录替换成根据你的windows版本重新编译出的文件
----再要配置系统环境变量 HADOOP_HOME 和 PATH
----修改YarnRunner这个类的源码

序列化(java.io.Serializable)

  1. 序列化(Serialization)是指把结构化对象转化为字节流。
  2. 反序列化(Deserialization)是序列化的逆过程。即把字节流转回结构化对象。

序列化在分布式环境的两大作用:进程间通信永久存储
Hadoop节点间通信
在这里插入图片描述

Hadoop 序列化格式特点 Writable

  1. 紧凑:高效使用存储空间。
  2. 快速:读写数据的额外开销小
  3. 可扩展:可透明地读取老格式的数据
  4. 互操作:支持多语言的交互

文件读取注意点

任务进行分发调度的时候会进行代码跟配置文件的传输,此时涉及到序列化跟反序列化
HDFS文件在进行map的时候,也会调用FileInputFormat来读取本次任务所涉及到的文件。其中还有三个功能。

  1. 验证作业的输入是否规范.
  2. 把输入文件切分成InputSplit.
  3. 提供RecordReader 的实现类,把InputSplit读到Mapper中进行处理.

InputSplit在执行MapReduce之前,原始数据被分割成若干split,每个split作为一个map任务的输入,在map执行过程中split会被分解成一个个记录(key-value对),map会依次处理每一个记录。split具体讲解调优
FileInputFormat只划分比HDFS block大的文件,所以FileInputFormat划分的结果是这个文件或者是这个文件中的一部分.如果一个文件的大小比block小,将不会被划分,这也是Hadoop处理大文件的效率要比处理很多小文件的效率高的原因。
当Hadoop处理很多小文件(文件大小小于hdfs block大小)的时候,由于FileInputFormat不会对小文件进行划分,所以每一个小文件都会被当做一个split并分配一个map任务,导致效率底下。
例如:一个1G的文件,会被划分成16个64MB的split,并分配16个map任务处理,而10000个100kb的文件会被10000个map任务处理。 因此引入了split
在这里插入图片描述
在这里插入图片描述
一个split对应一个map,要学会动态调节map数,split具体讲解
Combiner是MR程序中Mapper和Reducer之外的一种组件。
Combiner组件的父类就是Reducer,Combiner和Reducer的区别在于运行的位置。Combiner是在每一个MapTask所在的节点运行,Reducer是接收全局所有Mapper的输出结果
Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量
注:Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combiner的输出kv应该跟Reducer的输入kv类型要对应起来。

Mapper
3 5 7 ->(3+5+7)/3=5 
2 6 ->(2+6)/2=4
Reducer
(3+5+7+2+6)/5=23/5    不等于    (5+4)/2=9/2
package mywordcount;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;


/**
 * combiner 必须遵循 reducer的规范
 * 可以把它看成一种在map任务本地运行的reducer
 * 使用combiner的时候要注意两点
 * 1、combiner的输入输出数据泛型类型要能跟mapper和reducer匹配
 * 2、combiner加入之后不能影响最终的业务逻辑运算结果
 * @author ljj
 *
 */
public class WCCombiner extends Reducer<Text, LongWritable, Text, LongWritable>{

}
=====
package mywordcount;

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.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

/**
 * 用来描述一个特定的作业
 * 比如,该作业使用哪个类作为逻辑处理中的map,哪个作为reduce
 * 还可以指定该作业要处理的数据所在的路径
 * 还可以指定改作业输出的结果放到哪个路径
 * @author ljj
 */
public class WCRunner {

	public static void main(String[] args) throws Exception {

		Configuration conf = new Configuration();

		Job wcjob = Job.getInstance(conf);

		//设置整个job所用的那些类在哪个jar包
		wcjob.setJarByClass(WCRunner.class);


		//本job使用的mapper和reducer的类
		wcjob.setMapperClass(WCMapper.class);
		wcjob.setReducerClass(WCReducer.class);


		//指定本job使用combiner组件,组件所用的类为
		wcjob.setCombinerClass(WCReducer.class);


		//指定reduce的输出数据kv类型
		wcjob.setOutputKeyClass(Text.class);
		wcjob.setOutputValueClass(LongWritable.class);

		//指定mapper的输出数据kv类型
		wcjob.setMapOutputKeyClass(Text.class);
		wcjob.setMapOutputValueClass(LongWritable.class);


		//指定要处理的输入数据存放路径
		FileInputFormat.setInputPaths(wcjob, new Path("hdfs://weekend110:9000/wc/srcdata/"));

		//指定处理结果的输出数据存放路径
		FileOutputFormat.setOutputPath(wcjob, new Path("hdfs://weekend110:9000/wc/output3/"));

		//将job提交给集群运行
		wcjob.waitForCompletion(true);

	}
}

TextInputformat是默认的处理类

  1. TextInputformat是默认的处理类,处理普通文本文件
  2. 文件中每一行作为一个记录,他将每一行在文件中的起始偏移量作为key,每一行的内容作为value。
  3. 默认以\n或回车键作为一行记录。
  4. TextInputFormat继承了FileInputFormat。

MR常用方法实现

数据自定义序列化 WritableComparable

当我们自定义了MapReduce过程的的 输入输出类型的时候就可以这样搞了,比如数据集如下 第二列是手机号,第七列第八列分别是上行跟下行流量

1363157985066 13726230503 00-FD-07-A4-72-B8:CMCC 120.196.100.82 i02.c.aliimg.com 24 27 2481 24681 200
1363157995052 13826544101 5C-0E-8B-C7-F1-E0:CMCC 120.197.40.4 4 0 264 0 200
1363157991076 13926435656 20-10-7A-28-CC-0A:CMCC 120.196.100.99 2 4 132 1512 200
1363154400022 13926251106 5C-0E-8B-8B-B1-50:CMCC 120.197.40.4 4 0 240 0 200
1363157993044 18211575961 94-71-AC-CD-E6-18:CMCC-EASY 120.196.100.99 iface.qiyi.com 视频网站 15 12 1527 2106 200
1363157995074 84138413 5C-0E-8B-8C-E8-20:7DaysInn 120.197.40.4 122.72.52.12 20 16 4116 1432 200
1363157993055 13560439658 C4-17-FE-BA-DE-D9:CMCC 120.196.100.99 18 15 1116 954 200
1363157995033 15920133257 5C-0E-8B-C7-BA-20:CMCC 120.197.40.4 sug.so.360.cn 信息安全 20 20 3156 2936 200
1363157983019 13719199419 68-A1-B7-03-07-B1:CMCC-EASY 120.196.100.82 4 0 240 0 200
1363157984041 13660577991 5C-0E-8B-92-5C-20:CMCC-EASY 120.197.40.4 s19.cnzz.com 站点统计 24 9 6960 690 200
1363157973098 15013685858 5C-0E-8B-C7-F7-90:CMCC 120.197.40.4 rank.ie.sogou.com 搜索引擎 28 27 3659 3538 200
1363157986029 15989002119 E8-99-C4-4E-93-E0:CMCC-EASY 120.196.100.99 www.umeng.com 站点统计 3 3 1938 180 200
1363157992093 13560439658 C4-17-FE-BA-DE-D9:CMCC 120.196.100.99 15 9 918 4938 200
1363157986041 13480253104 5C-0E-8B-C7-FC-80:CMCC-EASY 120.197.40.4 3 3 180 180 200
1363157984040 13602846565 5C-0E-8B-8B-B6-00:CMCC 120.197.40.4 2052.flash2-http.qq.com 综合门户 15 12 1938 2910 200
1363157995093 13922314466 00-FD-07-A2-EC-BA:CMCC 120.196.100.82 img.qfc.cn 12 12 3008 3720 200
1363157982040 13502468823 5C-0A-5B-6A-0B-D4:CMCC-EASY 120.196.100.99 y0.ifengimg.com 综合门户 57 102 7335 110349 200
1363157986072 18320173382 84-25-DB-4F-10-1A:CMCC-EASY 120.196.100.99 input.shouji.sogou.com 搜索引擎 21 18 9531 2412 200
1363157990043 13925057413 00-1F-64-E1-E6-9A:CMCC 120.196.100.55 t3.baidu.com 搜索引擎 69 63 11058 48243 200
1363157988072 13760778710 00-FD-07-A4-7B-08:CMCC 120.196.100.82 2 2 120 120 200
1363157985066 13726238888 00-FD-07-A4-72-B8:CMCC 120.196.100.82 i02.c.aliimg.com 24 27 2481 24681 200
1363157993055 13560436666 C4-17-FE-BA-DE-D9:CMCC 120.196.100.99 18 15 1116 954 200

此时统计手机号码的上下行流量如下:

package myflowsum;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableComparable;

public class FlowBean implements WritableComparable<FlowBean>
{

	private String phoneNB;// 手机号
	private long up_flow; // 上行流量
	private long d_flow; // 下行流量
	private long s_flow; //  总流量

	//在反序列化时,反射机制需要调用空参构造函数,所以显示定义了一个空参构造函数
	public FlowBean()
	{
	}

	//为了对象数据的初始化方便,加入一个带参的构造函数
	public FlowBean(String phoneNB, long up_flow, long d_flow)
	{
		this.phoneNB = phoneNB;
		this.up_flow = up_flow;
		this.d_flow = d_flow;
		this.s_flow = up_flow + d_flow;
	}

	// 将对象数据序列化到流中
	@Override
	public void write(DataOutput out) throws IOException
	{

		out.writeUTF(phoneNB);
		out.writeLong(up_flow);
		out.writeLong(d_flow);
		out.writeLong(s_flow);

	}

	// 从数据流中反序列出对象的数据
	//从数据流中读出对象字段时,必须跟序列化时的顺序保持一致
	@Override
	public void readFields(DataInput in) throws IOException
	{

		phoneNB = in.readUTF();
		up_flow = in.readLong();
		d_flow = in.readLong();
		s_flow = in.readLong();

	}

	@Override
	public String toString()
	{

		return "" + up_flow + "\t" + d_flow + "\t" + s_flow;
	}

	// 继承特定接口 实现对比函数 从大到小排序
	@Override
	public int compareTo(FlowBean o)
	{
		return s_flow > o.getS_flow() ? -1 : 1;
	}
	public String getPhoneNB()
	{
		return phoneNB;
	}
	public void setPhoneNB(String phoneNB)
	{
		this.phoneNB = phoneNB;
	}
	public long getUp_flow()
	{
		return up_flow;
	}
	public void setUp_flow(long up_flow)
	{
		this.up_flow = up_flow;
	}
	public long getD_flow()
	{
		return d_flow;
	}
	public void setD_flow(long d_flow)
	{
		this.d_flow = d_flow;
	}
	public long getS_flow()
	{
		return s_flow;
	}
	public void setS_flow(long s_flow)
	{
		this.s_flow = s_flow;
	}
	
}
=======
package myflowsum;
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;

/**
 * FlowBean 是我们自定义的一种数据类型,要在hadoop的各个节点之间传输,应该遵循hadoop的序列化机制
 * 就必须实现hadoop相应的序列化接口
 * @author ljj
 *
 */
public class FlowSumMapper extends Mapper<LongWritable, Text, Text, FlowBean>{

	// 拿到日志中的一行数据,切分各个字段,抽取出我们需要的字段:手机号,上行流量,下行流量,然后封装成kv发送出去
	@Override
	protected void map(LongWritable key, Text value,Context context)
			throws IOException, InterruptedException {

		// 拿一行数据
		String line = value.toString();
		//切分成各个字段
		String[] fields = StringUtils.split(line, "\t");

		// 拿到我们需要的字段
		String phoneNB = fields[1];
		long u_flow = Long.parseLong(fields[7]);
		long d_flow = Long.parseLong(fields[8]);

		// 封装数据为k-v并输出 根据 电话号码进行分类
		context.write(new Text(phoneNB), new FlowBean(phoneNB,u_flow,d_flow));
	}
}
====== 
package myflowsum;

import java.io.IOException;

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

public class FlowSumReducer extends Reducer<Text, FlowBean, Text, FlowBean>{

	//框架每传递一组数据<1387788654,{flowbean,flowbean,flowbean,flowbean.....}>调用一次我们的reduce方法
	//reduce中的业务逻辑就是遍历values,然后进行累加求和再输出
	@Override
	protected void reduce(Text key, Iterable<FlowBean> values,Context context)
			throws IOException, InterruptedException {

		long up_flow_counter = 0;
		long d_flow_counter = 0;

		for(FlowBean bean : values){
			up_flow_counter += bean.getUp_flow();
			d_flow_counter += bean.getD_flow();
		}
		context.write(key, new FlowBean(key.toString(), up_flow_counter, d_flow_counter));
	}
}
======
package myflowsum;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputFormat;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.OutputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;

//这是job描述和提交类的规范写法
public class FlowSumRunner extends Configured implements Tool{

	@Override
	public int run(String[] args) throws Exception {

		Configuration conf = new Configuration();
		Job job = Job.getInstance(conf);

		job.setJarByClass(FlowSumRunner.class);

		job.setMapperClass(FlowSumMapper.class);
		job.setReducerClass(FlowSumReducer.class);

		job.setMapOutputKeyClass(Text.class);
		job.setMapOutputValueClass(FlowBean.class);

		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(FlowBean.class);

		FileInputFormat.setInputPaths(job, new Path(args[0]));
		FileOutputFormat.setOutputPath(job, new Path(args[1]));

		return job.waitForCompletion(true)?0:1;
	}

	public static void main(String[] args) throws Exception {
		int res = ToolRunner.run(new Configuration(), new FlowSumRunner(), args);
		System.exit(res);
	}
}

自定义排序

根据上一步的结果集,进行流量使用的从大到小的排序实现。排序默认是以Key 排序。

package myflowsum;

import java.io.IOException;

import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
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;

import cn.itcast.hadoop.mr.flowsum.FlowBean;

public class SortMR {
	public static class SortMapper extends Mapper<LongWritable, Text, FlowBean, NullWritable>{

		//拿到一行数据,切分出各字段,封装为一个flowbean,作为 key 输出
		@Override
		protected void map(LongWritable key, Text value,Context context)
				throws IOException, InterruptedException {

			String line = value.toString();

			String[] fields = StringUtils.split(line, "\t");

			String phoneNB = fields[0];
			long u_flow = Long.parseLong(fields[1]);
			long d_flow = Long.parseLong(fields[2]);

			context.write(new FlowBean(phoneNB, u_flow, d_flow), NullWritable.get()); // 会根据key 进行排序的,因为此时只是排序value可以不要,
		}
	}

	public static class SortReducer extends Reducer<FlowBean, NullWritable, Text, FlowBean>{
		@Override
		protected void reduce(FlowBean key, Iterable<NullWritable> values,Context context)
				throws IOException, InterruptedException {
			String phoneNB = key.getPhoneNB();
			context.write(new Text(phoneNB), key);
		}
	}

	public static void main(String[] args) throws Exception {

		Configuration conf = new Configuration();
		Job job = Job.getInstance(conf);

		job.setJarByClass(SortMR.class);

		job.setMapperClass(SortMapper.class);
		job.setReducerClass(SortReducer.class);

		job.setMapOutputKeyClass(FlowBean.class);
		job.setMapOutputValueClass(NullWritable.class);

		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(FlowBean.class);

		FileInputFormat.setInputPaths(job, new Path(args[0]));
		FileOutputFormat.setOutputPath(job, new Path(args[1]));

		System.exit(job.waitForCompletion(true)?0:1);
	}
}

======
package myflowsum;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableComparable;

public class FlowBean implements WritableComparable<FlowBean>
{

	private String phoneNB;// 手机号
	private long up_flow; // 上行流量
	private long d_flow; // 下行流量
	private long s_flow; //  总流量

	//在反序列化时,反射机制需要调用空参构造函数,所以显示定义了一个空参构造函数
	public FlowBean()
	{
	}

	//为了对象数据的初始化方便,加入一个带参的构造函数
	public FlowBean(String phoneNB, long up_flow, long d_flow)
	{
		this.phoneNB = phoneNB;
		this.up_flow = up_flow;
		this.d_flow = d_flow;
		this.s_flow = up_flow + d_flow;
	}

	// 将对象数据序列化到流中
	@Override
	public void write(DataOutput out) throws IOException
	{

		out.writeUTF(phoneNB);
		out.writeLong(up_flow);
		out.writeLong(d_flow);
		out.writeLong(s_flow);

	}

	// 从数据流中反序列出对象的数据
	//从数据流中读出对象字段时,必须跟序列化时的顺序保持一致
	@Override
	public void readFields(DataInput in) throws IOException
	{

		phoneNB = in.readUTF();
		up_flow = in.readLong();
		d_flow = in.readLong();
		s_flow = in.readLong();

	}

	@Override
	public String toString()
	{

		return "" + up_flow + "\t" + d_flow + "\t" + s_flow;
	}

	// 继承特定接口 实现对比函数 从大到小排序
	@Override
	public int compareTo(FlowBean o)
	{
		return s_flow > o.getS_flow() ? -1 : 1;
	}

	public String getPhoneNB()
	{
		return phoneNB;
	}

	public void setPhoneNB(String phoneNB)
	{
		this.phoneNB = phoneNB;
	}

	public long getUp_flow()
	{
		return up_flow;
	}

	public void setUp_flow(long up_flow)
	{
		this.up_flow = up_flow;
	}

	public long getD_flow()
	{
		return d_flow;
	}

	public void setD_flow(long d_flow)
	{
		this.d_flow = d_flow;
	}

	public long getS_flow()
	{
		return s_flow;
	}

	public void setS_flow(long s_flow)
	{
		this.s_flow = s_flow;
	}
}

Reduce过程

在我们操作上面两个步骤的时候我们发现 map跟reduce的任务进程数我们是无法控制的,不同的key 可能partiton到同一个reduce 中了。
默认partition

public class HashPartitioner<K, V> extends Partitioner<K, V> {
  public int getPartition(K key, V value, int numReduceTasks) {
    return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;// 其中numReduceTasks 默认为1 因此导致所以数据都汇总到一个reduce中了
  }
}

默认分区是根据key的hashCode和Integer.MAX_VALUE进行与运算对ReduceTasks个数取模得到的。用户没法控制哪个key存储到哪个分区。 我们可以自定义partition函数

  1. 改造分区的逻辑,自定义一个partitioner
  2. 自定义reduer task的并发任务数
  3. 如果ReduceTask的数量> getPartition的结果数,则会多产生几个的输出文件part-r-000xx;
  4. 如果1<ReduceTask的数量<getPartition的结果数,则有一部分分区数据无处安放,会Exception
  5. 如果ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个ReduceTask,最终也就只会产生一个结果文件 part-r-00000;
package myareapartition;

import java.util.HashMap;

import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
// 实现reduce 任务的自定义分区,也就是说 不同的key 怎么划分到不同的reduce 中。
public class AreaPartitioner<KEY, VALUE> extends Partitioner<KEY, VALUE>
{

	private static HashMap<String, Integer> areaMap = new HashMap<>();

	static
	{
		areaMap.put("135", 0);
		areaMap.put("136", 1);
		areaMap.put("137", 2);
		areaMap.put("138", 3);
		areaMap.put("139", 4);
	}

	@Override
	public int getPartition(KEY key, VALUE value, int numPartitions)
	{
		//从key中拿到手机号,查询手机归属地字典,不同的省份返回不同的组号
		int areaCoder = areaMap.get(key.toString().substring(0, 3)) == null ? 5 : areaMap.get(key.toString().substring(0, 3));
		return areaCoder;
	}
}

==========
package myareapartition;

import java.io.IOException;

import org.apache.commons.lang.StringUtils;
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;

import cn.itcast.hadoop.mr.flowsum.FlowBean;


/**
 * 对流量原始日志进行流量统计,将不同省份的用户统计结果输出到不同文件
 * 需要自定义改造两个机制:
 * 1、改造分区的逻辑,自定义一个partitioner
 * 2、自定义reduer task的并发任务数
 * @author ljj
 */
public class FlowSumArea {

	public static class FlowSumAreaMapper extends Mapper<LongWritable, Text, Text, FlowBean>{

		@Override
		protected void map(LongWritable key, Text value,Context context)
				throws IOException, InterruptedException {

			//拿一行数据
			String line = value.toString();
			//切分成各个字段
			String[] fields = StringUtils.split(line, "\t");

			//拿到我们需要的字段
			String phoneNB = fields[1];
			long u_flow = Long.parseLong(fields[7]);
			long d_flow = Long.parseLong(fields[8]);

			//封装数据为kv并输出
			context.write(new Text(phoneNB), new FlowBean(phoneNB,u_flow,d_flow));
		}
	}

	public static class FlowSumAreaReducer extends Reducer<Text, FlowBean, Text, FlowBean>{

		@Override
		protected void reduce(Text key, Iterable<FlowBean> values,Context context)
				throws IOException, InterruptedException {

			long up_flow_counter = 0;
			long d_flow_counter = 0;

			for(FlowBean bean: values){
				up_flow_counter += bean.getUp_flow();
				d_flow_counter += bean.getD_flow();
			}
			context.write(key, new FlowBean(key.toString(), up_flow_counter, d_flow_counter));
		}
	}

	public static void main(String[] args) throws Exception {

		Configuration conf = new Configuration();
		Job job = Job.getInstance(conf);

		job.setJarByClass(FlowSumArea.class);

		job.setMapperClass(FlowSumAreaMapper.class);
		job.setReducerClass(FlowSumAreaReducer.class);

		// 设置我们自定义的分组逻辑定义
		job.setPartitionerClass(AreaPartitioner.class);

		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(FlowBean.class);

		//设置reduce的任务并发数,应该跟分组的数量保持一致
		job.setNumReduceTasks(6);

		FileInputFormat.setInputPaths(job, new Path(args[0]));
		FileOutputFormat.setOutputPath(job, new Path(args[1]));

		System.exit(job.waitForCompletion(true)?0:1);
	}
}

参考

shuffle
shuffle图解
split具体讲解

发布了441 篇原创文章 · 获赞 870 · 访问量 111万+

猜你喜欢

转载自blog.csdn.net/qq_31821675/article/details/103452600