mapper
package wordcount;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class WCMapper extends Mapper<LongWritable, Text,Text,LongWritable> {
private Text KeyOut = new Text();
private final static LongWritable valueOut = new LongWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//将读取的一行内容根据分隔符进行切分
String[] words =value.toString().split("\\s+");
for(String word:words){
KeyOut.set(word);
//输出单词
context.write(new Text(word),valueOut);
}
}
}
reducer
package wordcount;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.io.Text;
import java.io.IOException;
public class WCReducer extends Reducer <Text, LongWritable,Text,LongWritable>{
private LongWritable result = new LongWritable();
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
//统计变量
long count = 0;
//遍历一组数据,取出该组所有的value
for (LongWritable value:values){
count += value.get();
}
result.set(count);
context.write(key,result);
}
}
driver
package wordcount;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
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;
public class WCdriver {
public static void main(String[] args) throws Exception{
//配置文件对象
Configuration conf = new Configuration();
//创建作业实例
Job job = Job.getInstance(conf, WCdriver.class.getSimpleName());
//设置作业驱动类
job.setJarByClass(WCdriver.class);
//设置作业mapper reducer类
job.setMapperClass(WCMapper.class);
job.setReducerClass(WCReducer.class);
//设置作业mapper阶段输出key value数据类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
//设置作业reducer阶段的输出key value数据类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
//配置作业输入路径
FileInputFormat.addInputPath(job,new Path("E:\\InAndOut\\hadoop\\Input\\word.txt"));
//配置作业输出路径
Path out = new Path("E:\\InAndOut\\hadoop\\Output\\WC");
FileOutputFormat.setOutputPath(job,out);
//判断输出路径是否存在
FileSystem fs = FileSystem.get(conf);
if (fs.exists(out)){
fs.delete(out,true);
}
//提交作业并等待执行完成
boolean resultFlag = job.waitForCompletion(true);
//程序退出
System.exit(resultFlag?0:1);
}
}
注意事项:
hadoop序列化数据类型实在org.apache.hadoop.io包下的,导错包会导致编译错误。
配置作业输入( FileInputFormat)输出(FileOutputFormat)路径时,这两个类必须得是org.apache.hadoop.mapreduce.lib包下的类,导错包会出现编码错误。
输出路径为文件夹,因为输出的不是一个文件,还有相应的矫正文件,所以得指定为文件夹。
概述:
格式:
public class WordCountDriver_v1 {
public static void main(String[] args) throws Exception {
……
Job job = Job.getInstance(conf, WordCountDriver_v1.class.getSimpleName()); job.setJarByClass(WordCountDriver_v1.class);
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
boolean resultFlag = job.waitForCompletion(true);
……
}
}
作用:
概述:
客户端驱动程序最后执行了Job.waitForCompletion方法;从名字上可以看出该方法的功能是等待MapReduce程序执行完毕。
点击进入Job.waitForCompletion方法内部:
在判断状态state可以提交Job后,执行submit方法提交作业。
monitorAndPrintJob()方法会不断的刷新获取job运行的进度信息,并打印。verbose为true表明要实时监视打印进度,该参数由用户提交程序时指定。
isSuccessful用于判断Job的最终状态是否成功完成,返回布尔类型结果。
概述:submit方法核心分为两个方面:一是跟MR运行集群环境建立连接;二是提交MR程序到集群运行。
集群建立连接:
connect方法:整个connect方法的核心是创建cluster对象实例。
Cluster类中最重要的两个成员变量是客户端通信协议提供者ClientProtocolProvider、客户端通信协议ClientProtocol,其实例叫做client,依托ClientProtocolProvider的create()方法产生。
ClientProtocolProvider分为LocalClientProtocolProvider(本地模式),YarnClientProtocolProvider(yarn集群模式)。通过new Cluster–>this–>initialize可以发现,会根据配置信息,调用不同的ClientProtocolProvider创建不同的ClientProtocol。
ClientProtocol是与集群进行通信的客户端通信协议,其实例叫做client,有两种不同的具体实现:Yarn模式的YARNRunner、Local模式的LocalJobRunner
在ClientProtocol中,定义了很多方法,用户可以使用这些方法进行job的提交、杀死、或是获取一些程序状态信息。
提交MR程序到集群:
在job.submit方法的最后,调用了submitter.submitJobInternal方法进行作业的提交
JobSubmitter作业提交器共有四个类成员变量,分别为:
文件系统FileSystem实例jtFs:用于操作作业运行需要的各种文件等;
客户端通信协议ClientProtocol实例submitClient:用于与集群交互,完成作业提交、作业状态查询等。
提交作业的主机名submitHostName;
提交作业的主机地址submitHostAddress。
submitJobInternal实现了提交作业的全逻辑。包括输出规范性检测、作业属性参数设置、作业准备区创建与准备资源提交(依赖资源、job.split、job.xml)、最终提交作业等。
submitJobInternal–真正提交作业,分为两种模式提交程序,本地模式运行提交任务、YARN模式运行提交任务。
整体概述:
概述:
第一层调用(MapTask.run):
在MapTask.run方法的第一层调用中,有下面两个重要的代码段。
第二层调用(runNewMapper)准备部分:
runNewMapper内第一大部分代码为maptask运行的准备部分,其主要逻辑是创建maptask运行时需要的各种对象。
第二层调用(runNewMapper)工作部分:
(1)如何从切片读取数据(initialize逻辑)
根据切片信息读取数据获得输入流in
判断切片是否被压缩,使用的压缩算法是否为可切分算法。
判断自己是否属于第一个切片,如果不是,舍弃第一行数据不处理。
最终读取数据的实现在in.readLine方法中。默认行为是:根据回车换行符一行一行读取数据,返回
key:每一行起始位置偏移量
value:这一行的内容
(2)调用map方法处理数据,就是用户重写方法map()来实现业务逻辑的地方。
(3)如何调用OutputCollector收集map输出的结果
概述:
整个MapReduce以InputFormat开始,其负责读取待处理的数据。默认的实现叫做TextInputFormat。
InputFormat核心逻辑体现在两个方面:
一是:如何读取待处理目录下的文件。一个一个读?还是一起读?
二是:读取数据的行为是什么以及返回什么样的结果?是一行一行读?还是按字节读?
可以说,不同的实现有不同的处理逻辑。
概述:
MapTask切片机制(逻辑规划):
首先需要计算出split size切片大小(split size=block size)
然后以split size逐个遍历待处理的文件,形成逻辑规划文件。默认情况下,有多少个split就对应启动多少个MapTask。
在getSplits方法中,创建了一个集合splits,用于保存最终的切片信息。生成的切片信息在客户端提交job时,也就是JobSubmitter. writeSplits方法中,把所有切片信息进行排序,大的切片在前,然后序列化到一个文件中,此文件叫做逻辑切片文件(job.split),提交到作业准备区路径下。
在进行逻辑切片的时候,假如说一个文件恰好是129M大小,那么根据默认的逻辑切片规则将会形成一大一小两个切片(0-128 128-129),并且将启动两个maptask。这明显对资源的利用效率不高。因此在设计中,MapReduce时刻会进行bytesRemaining,剩下文件大小,如果剩下的不满足 bytesRemaining/splitSize > SPLIT_SLOP,那么将不再继续split,而是剩下的所有作为一个切片整体。SPLIT_SLOP默认值是1.1。
概述:
initialize属于LineRecordReader的初始化方法,会被MapTask调用且调用一次。里面描述了如何从切片读取数据。
nextKeyValue方法用于判断是否还有下一行数据以及定义了按行读取数据的逻辑:一行一行读取,返回
优化措施:由于文件在HDFS上进行存储的时候,物理上会进行分块存储,可能会导致文件内容的完整性被破坏。比如:一个单词hello被分开成he 和 llo存储在不同的block中,就导致单词计数的结果错误。为了避免这个问题,在实际读取split数据的时候,每个maptask会进行读取行为的调整。
一是:每个maptask都多处理下一个split的第一行数据;
除了第一个,每个maptask都舍去自己的第一行数据不处理。
概述:
map最终调用context.write方法将结果输出。
至于输出的数据到哪里,取决于MR程序是否有Reducer阶段?
如果有reducer阶段,则创建输出收集器OutputCollector,对结果收集。
如果没有reducer阶段,则创建OutputFormat,默认实现是TextOutputFormat,直接将处理的结果输出到指定目录文件中。
在MapReduce具有reducetask阶段的时候,maptask的输出并不只是直接输出到磁盘上的;而是被输出收集器首先收集到内存缓冲区,最终持久化到磁盘。这个过程称之为MapReduce在Map端的Shuffle过程。主要包括:
(1)Partitioner 分区
通过debug不断进入发现,最终调用的是MapTask中的Write方法。Write方法中把输出的数据kv通过收集器写入了环形缓冲区,在写入之前这里还进行了数据分区计算。partitioner.getPartition(key, value, partitions)就是计算每个mapper的输出分区编号是多少。注意,只有当reducetask >1的时候。才会进行分区的计算。
默认的分区器在JobContextImpl中定义,是HashPartitioner。默认的分区规则也很简单: key.hashCode() %numReduceTasks。为了避免hashcode值为负数,通过和Integer最大值进行与计算修正hashcode为正。
(2)Circular buffer 内存环形缓冲区
环形缓冲区(Circular buffer)本质就是字节数组,叫做kvbuffer ,默认100M大小。缓冲区的作用是批量收集map方法的输出结果,减少磁盘IO的影响。想一下,一个一个写和一个批次一个批次写,哪种效率高?环形缓冲区里面不仅存放着key、value的序列化之后的数据,还存储着一些元数据,存储key,value对应的元数据的区域,叫kvmeta。
每个key、value都对应一个元数据,元数据由4个int组成:value的起始位置、key的起始位置、partition、value的长度。
key/value序列化的数据和元数据在环形缓冲区中的存储是由equator(赤道)分隔的。是相邻不重叠的两个区域。key/value按照索引递增的方向存储,meta则按照索引递减的方向存储,将其数组抽象为一个环形结构之后,以equator为界,key/value顺时针存储,meta逆时针存储。数据的索引叫做bufindex,元数据的索引叫做kvindex。
创建过程:
初始化:在MapTask中创建OutputCollector(实现是MapOutputBuffer)的时候,对环形缓冲区进行了初始化的动作。初始化的过程中,主要是构造环形缓冲区的抽象数据结构。包括不限于:设置缓冲区大小、溢出比、初始化kvbuffer|kvmeta、设置Equator标识分界线、构造排序的实现类、combiner、压缩编码等。(MapOutputBuffer.init)
数据收集:收集数据到环形缓冲区核心逻辑有:序列化key到字节数组,序列化value到字节数组,写入该条数据的元数据(起始位置、partition、长度)、更新kvindex。(MapOutputBuffer.collect)
(3)Spill、Sort 溢写、排序
概述:环形缓冲区虽然可以减少IO次数,但是内存总归有容量限制,不能把所有数据一直写入内存,数据最终还是要落入磁盘上存储的,所以需要在一定条件下将缓冲区中的数据临时写入磁盘,然后重新利用这块缓冲区。这个从内存往磁盘写数据的过程被称为Spill,中文可译为溢写。
溢写过程:
触发Spill阈值:整个缓冲区有个溢写的比例spill.percent。这个比例默认是0.8。当环形缓冲区的数据已经达到阈值(buffer size * spill percent = 100MB * 0.8 = 80MB),spill线程启动。
startSpill():spill线程是由startSpill()方法唤醒的,在进行spill操作的时候,此时map向buffer的写入操作并没有阻塞,剩下20M可以继续使用。(MapOutputBuffer.collect)
SpillThread:溢写的线程叫做SpillThread,查看其线程run方法,run中主要是sortAndSpill。每个spill文件都有一个索引,其中包含有关每个文件中分区的信息-分区的开始位置和结束位置。这些索引存储在内存中,叫做SpillRecord,可使用内存量为mapreduce.task.index.cache.limit.bytes,默认情况下等于1MB。如果不足以将SpillRecord存储在内存中,则所有下一个创建的溢出文件的索引都将与溢出文件一起写入磁盘。
溢写数据到临时文件中
更新spillRecord
将内存中的spillRecord写入磁盘变成索引文件
Sort排序:在溢写的过程中,会对kvmeta元数据进行排序。排序规则是MapOutputBuffer.compare。先对partition进行排序其次对key值排序。这样,数据首先按分区排序,并且在每个分区内按key对数据排序。Spill线程根据排过序的Kvmeta逐个分区的把数据溢出到磁盘临时文件中,一个partition对应的数据写完之后顺序地写下个partition,直到把所有的partition遍历完。
(4)Merge 合并
(5)Combiner 规约
作用:
生效阶段:当job设置了Combiner,在spill和merge的两个阶段都可能执行。
概述:
第一层调用(ReduceTask.run)阶段划分:整个reducetask分为3个阶段:copy拉取数据、sort排序数据、reduce处理数据。
第一层调用(ReduceTask.run)Shuffle操作:
对于MapReduce程序来说,MapTask输出的结果并不会主动发送给各个ReduceTask;因此需要各个ReduceTask主动到各个Map端拉取属于自己分区的数据。从拉取数据开始到reduce方法处理数据之前,称之为reduce端的shuffle操作。包括copy、merge、sort。
在ReduceTask.run方法中跟shuffle相关的操作,除了shuffle核心任务之外,还创建了reducetask工作相关的一些组件,包括但不限于:
第一层调用(ReduceTask.run)运行Reducer:shuffle完的结果将进入到reducer进行最终的reduce聚合处理。
第二层调用(runNewReducer)准备部分:
默认情况下,框架使用new API来运行,所以将执行runNewReducer()。runNewReducer内第一大部分代码我们称之为reducetask运行的准备部分。其主要逻辑是创建reducetask运行时需要的各种依赖。包括:taskContext上下文、创建用户编写设置的reducer类、outputFormat输出数据组件、ReducerContext上下文。
第二层调用(runNewReducer)工作部分:
概述:
init初始化:
初始化的过程中,核心逻辑就是创建MergeManagerImpl类。
在MergeManagerImpl类中,核心的有:
确定shuffle时的一些条件参数
启动MemToMemMerge线程,因为fetch来数据首先放入在内存中的,正常情况下在内存中对数据进行合并是最快的。可惜的是,默认情况下,是不开启内存到内存的合并的。
启动inMemoryMerger(内存到磁盘合并)、onDiskMerger(磁盘到磁盘合并)线程
run运行:
概述:
Fetcher线程run方法:
(1)获得所有maptask处于PENDING待处理状态的主机。针对maptask的几种状态,在MapHost类中有记载。
(2)copyFromHost
从处于PENDING状态的maptask拉取数据。下面进入copyFromHost方法内部。
建立拉取数据的输入流。
拉取拷贝数据。下面进入copyMapOutput方法看是如何拉取拷贝数据的。
首先进行判断copy过来的数据放置在哪里?优先内存,超过限制放置磁盘。因此获得的mapOutput就有两种具体的实现。然后通过mapOutput.shuffle开始拉取数据。
InMemoryMapOutput:把copy来的数据放置到reducetask内存中。
OnDiskMapOutput:把copy来的数据放置到磁盘上。
概述:
inMemoryMerger:
onDiskMerger:
finalMerger:
概述:
reducer.run方法:
首先在Reduce.run中调用context.nextKey()决定是否进入while循环,然后调用nextKeyValue将key/value的值从input中读出,其次通过context.getValues将Iterator传入reduce中,在reduce中通过Iterator.hasNext查看此key是否有下个value,然后通过Iterator.next调用nextKeyValue去input中读取value。然后循环迭代Iterator,读取input中相同key的value。
也就是说reduce中相同key的value值在Iterator.next中通过nextKeyValue读取的,每调用一次next就从input中读一个value。通俗理解:key相同的被分为一组,一组中所有的value会组成一个Iterable。key则是当前的value与之对应的key。
reducer.reduce方法:
对于reduce方法,如果用户不重写,父类中也有默认实现逻辑。其逻辑为:输入什么,原封不动的输出什么,也就意味着不对数据进行任何处理。通常会基于业务需求重新父类的reduce方法。
概述: