@R星校长
mapreduce 计算流程:
首先将 block 块进行逻辑切片的计算,每个切片(split)对应一个 map 任务
切片是为了将 block 数量和 map 任务数量解耦。
map 读取切片数据,默认按行读取,作为键值对交给 map 方法,其中 key 是当前读取的行在文件中的字节偏移量,value 就是读取的当前行的内容。
map 开始计算,自定义的逻辑。
map 将输出的 kv 首先写到环形缓冲区,在写之前计算分区号(默认按照 key 的 hash 值对 reducer 的个数取模)。
环形缓冲区默认 100MB ,阈值 80% ,如果写入的 kv 对达到了 80% 则发生溢写,溢写的时候要先对键值对按照分区号进行分区,相同分区按照 key 的字典序排序,溢写到磁盘。如果溢写的文件数量达到了三个,则发生 map 端归并操作,此时如果指定了combiner,则按照 combiner 合并数据。
当一个 map 任务完成之后,所有的 reducertask 向其发送 http get 请求,下载它们所属的分区数据。此过程称为 shuffle ,洗牌。
当所有 map 任务运行结束,开始 reduce 任务
在 reduce 开始之前,根据设定的归并因子,进行多伦的归并操作,非最后一轮的归并的结果文件被存入到硬盘上,最后一轮归并的结果直接传递给 reduce,reduce 迭代计算。
reduce 计算结束后将结果写到 HDFS 文件中,每一个 reducer task 任务都会在作业输出路径下产生一个结果文件 part-r-00xxx。同时执行成功时会产生一个空的 _SUCCESS 文件,该文件是一个标识文件。MR1->MR2->MR3
作业提交流程:
1、 客户端向 RM 取号(获取作业的 ID)
2、 客户端检查作业输入输出(如果输入路径不存在则抛出异常;如果输出路径存在也抛出异常),计算切片,解析配置信息
3、 客户端将 jar 包、配置信息以及切片信息上传到 HDFS
4、 客户端向 RM 发送提交作业的请求
5、 RM 调度一个 NM ,在 NM 上的一个容器中运行 MRAppMaster ,一个作业对应一个 MRAppMaster。
6、 MRAppMaster 首先获取 HDFS 中的作业信息,计算出当前作业需要多少个 map 任务,多少个 reduce 任务
7、 MRAppMaster(AM)向 RM 为 map 任务申请容器,AM 跟 NM 通信把容器启动起来,运行 map 任务,容器中的 YARNChild 会首先本地化 conf 、切片信息以及 jar 包
8、 当 map 任务完成达到 5% 的时候,AM 向 RM 为 reduce 任务申请容器
9、 当 MR 中最后一个任务运行结束,AM 向客户端发送作业完成信息。MR 的中间数据销毁,容器销毁,计算结果存档到历史服务器。
课程内容
客户端作业提交源码分析
框架:MapTask
框架:ReduceTask
客户端作业提交源码分析
Mapper 类的 map 方法中添加:
Thread.sleep(99999999);
MainClass 类中注释掉:
//configuration.set("mapreduce.framework.name","local");
重新打 jar 包,发布到服务器上运行:
yarn jar mywc.jar com.bjsxt.mr.wc.MainClass <inputPath> <outputPath>
[root@node1 ~]# hdfs dfs -ls -R /tmp/hadoop-yarn/
[root@node1 ~]# hdfs dfs -get /tmp/hadoop-yarn/staging/root/.staging/job_1582529707113_0005
查看 job.xml(Ctrl+Alt+L 格式化代码)
配置信息默认值一部分来自于 MRJobConfig 接口
一部分来自 *-default.xml 文件
一部分来自用户的设置:程序设置和 xxx-site.xml 设置
Configuration
xx-default.xml
xx-site.xml
MRJobConfig
观察默认值
JobContextImpl
JobConf conf
观察默认值
Job
set*()
waitForCompletion(true)
MainClass 中使用
提交作业:
提交作业属于异步操作,当执行 submit() 方法之后,由于 job.waitForCompletion(true) 参数为 true,所以会执行 monitorAndPrintJob() 方法不断打印输出执行的过程。
如果参数为 false,则等待作业完成而不做过程的日志输出。
客户端在提交作业之后,每秒轮询一次作业的执行进度。
设置值:
看默认值:
如何提交作业:
该方法提交之后立即返回。
submitJobInternal 真正提交作业。
提交作业的流程:
1.检查作业的输入和输出的指定路径
2.计算作业的切片
3.如果需要,给分布式缓存设置必要的计数器信息。
4.将 jar 包和配置信息以及切片信息拷贝到 hdfs 上的 map-reduce 系统目录。
5.将作业提交给 JobTracker(hadoopV1.x)/ResourceManager(hadoopV2.x+),并可选地,监控作业的执行状态。
上图是计算切片的代码。conf.setInt 表示 maps 就是 map 任务的数量。
切片如何计算?看源码
计算切片的细节:
splits 这个 list 中有多少个元素,就有多少个 map 任务
上图决定过了 InputFormat 是哪个类,通过这个类就可以直到如何截取的切片
上图说明首先根据字符串到 conf 中查找 InputFormat 类,如果没有设置,则使用给默认值 TextInputFormat 类,由于我们没有设置过,所以就是 TextInputFormat 类。
此时查看 TextInputFormat 类中的 getSplit 方法如何实现。
上面的注释表示:文件分割为行,按照回车或换行分割文件,key 就是当前行在文件中的字节偏移量,value 就是读到的行。
该类中没有 getSplit 方法,去它的父类 FileInputFormat 类中查看如何实现 getSplit 方法:
切片需要确定:哪个文件的切片
切片的偏移量是多少(对文件唯一) seek(偏移量)
切片的长度是多少
切片在哪个主机上
path 表示切片属于哪个文件
length-bytesRemaining 表示切片的偏移量
splitSize 表示切片的大小
getHosts 表示切片在哪些主机上(因为一个 block 块可能有多个副本,默认值是 3)
切片大小:
//mapreduce.input.fileinputformat.split.maxsize
//mapreduce.input.fileinputformat.split.minsize
minSize?多大?1
maxSize 多大?Long.MAX_VALUE
blockSize 就是文件的 block 大小
splitSize 多大?
默认情况下,splitSize 就是 block 大小。
如何设置切片大小?
可以设置 maxSize 和 minSize
maxSize:
设置:
MainClass 中设置切片的大小:
将最小值和最大值设置为一样的,可以设置 split 大小为小于 block 的任意大小,必须大于 1
那么 256MB 如何设置?两个参数都设置 256*1024*1024
最后,上传 jar 包,配置信息以及切片信息,并真正提交作业。
源码:
waitForCompletion
submit();
monitorAndPrintJob();
return isSuccessful();
submit
connect();
submitter.submitJobInternal(Job.this, cluster);
submitJobInternal
checkSpecs(job);
copyAndConfigureFiles(job, submitJobDir);
int maps = writeSplits(job, submitJobDir);
status = submitClient.submitJob( jobId, submitJobDir.toString(), job.getCredentials());
writeNewSplits
input->TextInputFormat.class
input.getSplits(job);
minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));(1)
maxSize = getMaxSplitSize(job);(特别大)
blkLocations = fs.getFileBlockLocations(file, 0, length);
blockSize = file.getBlockSize();
splitSize = computeSplitSize(blockSize, minSize, maxSize);
Math.max(minSize, Math.min(maxSize, blockSize));
splits.add(makeSplit(path, length-bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
切片要素:
1、切片属于哪个文件
2、切片的文件中的偏移量
3、切片长度
4、该切片所在的 block 块位于哪个主机(主机列表)
配置完善:个性化
检查路径
计算 Split: maps
blocklocation:位置信息
file1.txt,0, 1048576,node02,node03,node04
file1.txt,1048576,1048576,node01,node02,node03
资源提交到 HDFS
提交任务
AppMaster>RM 申请资源(参照 split)
作业:用普通话描述 client 都做了哪些事情!!!
框架:MapTask
从切片读取数据,给 map 方法,map 方法输出的键值对写到环形缓冲区
在写到环形缓冲区之前计算分区号
环形缓冲区排序后溢写到 map 端本地磁盘,可能会有合并的过程,3
可以设置环形缓冲区大小和阈值
排序所使用的算法:默认是快排,可以设置
可以自定义 key 和 value
排序比较器可以自定义
可以设置 Combiner
map 输入 input
源码从哪里开始看?
从 YarnChild 开始
在 idea 中通过 alt+ctrl+<- 或 -> 后退或前进
Ctrl+Shift+N -> 输入 YarnChild->Ctrl+G (定位到哪一行) -> 输入 163:1
YarnChild
光标点到 run() 方法名称上,Ctrl+Alt+B-> 选择 MapTask 类
MapTask
MapTask 实现了 run 方法:
runNewMapper 真正执行 Map 任务 (Hadoop2.x+)
主要做什么事情:
private <INKEY,INVALUE,OUTKEY,OUTVALUE>
void runNewMapper(final JobConf job,
final TaskSplitIndex splitIndex,
final TaskUmbilicalProtocol umbilical,
TaskReporter reporter
) throws IOException, ClassNotFoundException,
InterruptedException {
……
try {//782行
input.initialize(split, mapperContext);//初始化input
//运行mapper任务,调用自定义Mapper类覆写的map()方法
mapper.run(mapperContext);
mapPhase.complete();//
setPhase(TaskStatus.Phase.SORT);//设置任务的当前阶段
statusUpdate(umbilical);//更新状态
input.close();//input关闭与置空
input = null;
output.close(mapperContext);//output关闭与置空
output = null;
} finally {
closeQuietly(input);
closeQuietly(output, mapperContext);
}
}
再研究涉及到的对象分别如何创建的:
实例化 mapper 对象
getMapperClass 方法调用的是 TaskAttemptContextImpl 类的方法,该类中没有这个方法,则看它的父类 JobContextImpl(Ctrl+Alt+B):
根据字符串 MAP_CLASS_ATTR 到 conf 中查找一个 Class 对象,该对象是我们指定的 Mapper 类。如果没有指定,则返回默认值 Mapper.class
看我们自己设置的 Mapper 类是如何设置的?
自定义 Mapper 的设置,该方法的调用做了什么事儿?
使用相同的字符串设置 Mapper 类。
为什么 MainClass 设置好之后可以在 MapTask中直接使用?谁先谁后?
答: MainClass 在客户端运行,搞定一切配置信息之后才提交的作业,而只有提交作业之后才开始运行 MapTask 。
创建 InputFormat 对象:
这个对象是谁?
我们在 wordcount 中没有设置,所以使用默认值:TextInputFormat 类
runNewMapper 的重建切片信息,切片所在的位置以及切片的偏移量
就知道了当前 map 任务从哪里从什么偏移量读取数据。
创建输入流:需要知道从哪里读取,split ,怎么读取,inputFormat
NewTrackingRecordReader 如何读取?
切片是逻辑切片,没有发生任何数据的拷贝等工作
切片在计算的时候也只需要元数据即可。
1、 切片属于哪个文件
2、 切片在哪些 block 块上,block 在哪些 node 上
3、 切片的偏移量(对整个文件唯一)
4、 切片的长度
看 NewTrackingRecordReader 如何实现
NewTrackingRecordReader(org.apache.hadoop.mapreduce.InputSplit split,
org.apache.hadoop.mapreduce.InputFormat<K, V> inputFormat,
TaskReporter reporter,
org.apache.hadoop.mapreduce.TaskAttemptContext taskContext)
throws InterruptedException, IOException {
……
512 this.real = inputFormat.createRecordReader(split, taskContext);
……
}
Ctrl+单击 createRecordReader (split, taskContext)
public abstract class InputFormat<K, V> {
……
public abstract
RecordReader<K,V> createRecordReader(InputSplit split,
TaskAttemptContext context
) throws IOException,
InterruptedException;
}
直接在 createRecordReader() 方法处 Ctrl+Alt+B
最终实现 LineRecordReader(行读取器)
initialize 方法![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/a9f25c8852d2a8d6f84688845cf71fab.png)
Ctrl+T (Ctrl+Alt+B)->LineRecordReader
public void initialize(InputSplit genericSplit,
TaskAttemptContext context) throws IOException {
FileSplit split = (FileSplit) genericSplit;
Configuration job = context.getConfiguration();
this.maxLineLength = job.getInt(MAX_LINE_LENGTH, Integer.MAX_VALUE);
start = split.getStart();
end = start + split.getLength();
final Path file = split.getPath();
// open the file and seek to the start of the split
final FileSystem fs = file.getFileSystem(job);
fileIn = fs.open(file);
CompressionCodec codec = new CompressionCodecFactory(job).getCodec(file);
if (null!=codec) {
isCompressedInput = true;
decompressor = CodecPool.getDecompressor(codec);
if (codec instanceof SplittableCompressionCodec) {
final SplitCompressionInputStream cIn =
((SplittableCompressionCodec)codec).createInputStream(
fileIn, decompressor, start, end,
SplittableCompressionCodec.READ_MODE.BYBLOCK);
in = new CompressedSplitLineReader(cIn, job,
this.recordDelimiterBytes);
start = cIn.getAdjustedStart();
end = cIn.getAdjustedEnd();
filePosition = cIn;
} else {
in = new SplitLineReader(codec.createInputStream(fileIn,
decompressor), job, this.recordDelimiterBytes);
filePosition = fileIn;
}
} else {
fileIn.seek(start);
in = new UncompressedSplitLineReader(
fileIn, job, this.recordDelimiterBytes, split.getLength());
filePosition = fileIn;
}
// If this is not the first split, we always throw away first record
// because we always (except the last split) read one extra line in
// next() method.
if (start != 0) {
start += in.readLine(new Text(), 0, maxBytesToConsume(start));
}
this.pos = start;
}
从第二个切片开始,大家放弃第一行,更新 pos 指向第二行,那么一会 map 计算读到的第一条记录就是完整的第二行;间接:每个切片对应的 map , 都要向后多读取一行~!!!
run(mapperContext)
Ctrl+ 单击 run 方法
Ctrl+Alt+B
Ctrl+Alt+B
LineRecordReader- nextKeyValue()
为 key 和 value 的赋值,并返回布尔!
public boolean nextKeyValue() throws IOException {
if (key == null) {
key = new LongWritable();
}
key.set(pos);
if (value == null) {
value = new Text();
}
int newSize = 0;
// We always read one extra line, which lies outside the upper
// split limit i.e. (end - 1)
while (getFilePosition() <= end || in.needAdditionalRecordAfterSplit()) {
if (pos == 0) {
newSize = skipUtfByteOrderMark();
} else {
newSize = in.readLine(value, maxLineLength, maxBytesToConsume(pos));
pos += newSize;
}
if ((newSize == 0) || (newSize < maxLineLength)) {
break;
}
// line too long. try again
LOG.info("Skipped line of size " + newSize + " at pos " +
(pos - newSize));
}
if (newSize == 0) {
key = null;
value = null;
return false;
} else {
return true;
}
}
以上源码才看了红色矩形中的一部分,后续的源码还没有查看。
map 输出 output
概述
设置输出:如果该作业只有 map 任务没有 reduce 任务,则直接通过 NewDirectOutputCollector 写到 HDFS 。如果有 reduce 任务,则通过 NewOutputCollector 写到环形缓冲区
有 reduce 任务
NewOutputCollector(org.apache.hadoop.mapreduce.JobContext jobContext,
JobConf job,
TaskUmbilicalProtocol umbilical,
TaskReporter reporter
) throws IOException, ClassNotFoundException {
collector = createSortingCollector(job, reporter);
//获取reduce任务的数量
partitions = jobContext.getNumReduceTasks();
if (partitions > 1) {//大于1通过反射创建对应的自定义分区类的对象
partitioner = (org.apache.hadoop.mapreduce.Partitioner<K,V>)
ReflectionUtils.newInstance(jobContext.getPartitionerClass(), job);
} else {//等于1,创建默认的分区对象,内部返回0
partitioner = new org.apache.hadoop.mapreduce.Partitioner<K,V>() {
@Override
public int getPartition(K key, V value, int numPartitions) {
return partitions - 1;//返回0
}
};
}
}
分区
如果 reduce 任务数量大于 1,单击 getPartitionerClass():
Ctrl+Alt+B:
如果没有指定分区类,使用默认的 HashPartitioner 分区类
Key 的 hashCode 值 %numReduceTasks 。
如何指定分区类:
计算当前键值对所在的分区:
partitioner.getPartition(key, value, partitions)
该 partitioner 是哪个类?
在 NewOutputCollector 的构造器中,有 partitioner 的赋值
partitions 分区数量:
reduce 任务的数量,通过字符串获取设置的值,如果没有设置,则默认值是 1
想一下,我们如何设置?
设置的细节:
一个 reduce 任务一个分区
我们自己如何实现分区器?
好的分区器起码得能够处理数据倾斜
继承 Partitioner 类,实现其中的方法 getPartition
在 job 中设置
job.setPartitionerClass(XxxxPartitioner.class);
接着看 write 方法:
WCMapper 类的 map 方法中 context.write(key,value)
该类中要计算分区号
collector.collect(k,v,p)
k&v 要做序列化~!!!
计算出分区号,接着调用 collector 的 collect 方法
collector 又是谁?
构造器中实例化 collector
该 collector 就是 MapOutputBuffer 类
因为 getClasses 方法第一个参数是字符串的名字,第二个是默认值,我们没有设置过,所以使用默认值:MapOutputBuffer
MapOutputBuffer 环形缓冲区
public void init(MapOutputCollector.Context context
) throws IOException, ClassNotFoundException {
job = context.getJobConf();
reporter = context.getReporter();
mapTask = context.getMapTask();
mapOutputFile = mapTask.getMapOutputFile();
sortPhase = mapTask.getSortPhase();
spilledRecordsCounter = reporter.getCounter(TaskCounter.SPILLED_RECORDS);
partitions = job.getNumReduceTasks();
rfs = ((LocalFileSystem)FileSystem.getLocal(job)).getRaw();
//sanity checks
//阈值0.8
final float spillper =
job.getFloat(JobContext.MAP_SORT_SPILL_PERCENT, (float)0.8);
//100MB
final int sortmb = job.getInt(JobContext.IO_SORT_MB, 100);
......
//没有指定排序类则模式使用QuickSort
sorter = ReflectionUtils.newInstance(job.getClass("map.sort.class",
QuickSort.class, IndexedSorter.class), job);
......
// k/v serialization
//排序比较器
comparator = job.getOutputKeyComparator();
keyClass = (Class<K>)job.getMapOutputKeyClass();
valClass = (Class<V>)job.getMapOutputValueClass();
serializationFactory = new SerializationFactory(job);
keySerializer = serializationFactory.getSerializer(keyClass);
keySerializer.open(bb);
valSerializer = serializationFactory.getSerializer(valClass);
valSerializer.open(bb);
// output counters
mapOutputByteCounter = reporter.getCounter(TaskCounter.MAP_OUTPUT_BYTES);
mapOutputRecordCounter =
reporter.getCounter(TaskCounter.MAP_OUTPUT_RECORDS);
fileOutputByteCounter = reporter
.getCounter(TaskCounter.MAP_OUTPUT_MATERIALIZED_BYTES);
// compression
if (job.getCompressMapOutput()) {
Class<? extends CompressionCodec> codecClass =
job.getMapOutputCompressorClass(DefaultCodec.class);
codec = ReflectionUtils.newInstance(codecClass, job);
} else {
codec = null;
}
// combiner
final Counters.Counter combineInputCounter =
reporter.getCounter(TaskCounter.COMBINE_INPUT_RECORDS);
combinerRunner = CombinerRunner.create(job, getTaskID(),
combineInputCounter,
reporter, null);
if (combinerRunner != null) {
final Counters.Counter combineOutputCounter =
reporter.getCounter(TaskCounter.COMBINE_OUTPUT_RECORDS);
combineCollector= new CombineOutputCollector<K,V>(combineOutputCounter, reporter, job);
} else {
combineCollector = null;
}
spillInProgress = false;
//大于等于3个spill时进行combiner
minSpillsForCombine = job.getInt(JobContext.MAP_COMBINE_MIN_SPILLS, 3);
//spill线程如果达到阈值,那么,buffer会用spillThread线程去溢写数据
spillThread.setDaemon(true);
spillThread.setName("SpillThread");
spillLock.lock();
try {
spillThread.start();
while (!spillThreadRunning) {
spillDone.await();
}
} catch (InterruptedException e) {
throw new IOException("Spill thread failed to initialize", e);
} finally {
spillLock.unlock();
}
if (sortSpillException != null) {
throw new IOException("Spill thread failed to initialize",
sortSpillException);
}
}
1)取用户配置的比较器
2)如果取不到,取用户 mapOutputKeyClass 自身的比较器
环形缓冲区是什么?
该类的初始化方法中有两个需要注意的:spillper 表示缓冲区阈值 80%,可以自行设置:
sortmb 表示环形缓冲区大小:默认 100MB
可以自己设置:
sorter 是排序器,如果设置了就是用自己的,否则使用默认值:QuickSort(快排)
maxMemUsage = sortmb << 20 等价于 sortmb10241024 由 MB 变成字节数
kvbuffer 代表环形缓冲区
排序比较器
将环形缓冲区数据排序,溢写到本地磁盘
sortAndSpill
排序
sorter=QuickSort 快排
快排中的比较和排序。其中 compare 方法用于比较 key 的大小
如何比较?看 compare 方法
compare 方法使用的是 MapOutputBuffer 的 compare 方法
比较的返回值是 0,1,-1
当 a>b 返回 1
当 a=b 返回 0
当 a<b 返回 -1
comparator.compare 方法用于比较分区内两个键值对的大小
首先 comparator 是谁?
该方法返回值是谁?
设置排序比较器的方式
通过 KEY_COMPARATOR 字符串设置排序比较器
源码:如果没有设置排序比较器,则使用下述的方式找一个排序比较器
WritableComparator.get(getMapOutputKeyClass().asSubclass(WritableComparable.class), this);
首先获取 map 端输出 key 的类型,wordcount 中有设置:
job.setMapOutputKeyClass(Text.class);
同时可以看出 key 需要实现 WritableComparable 接口
通过 map 端的输出 key 的类型到 WritableComparator 类中通过 get 方法获取排序比较器。
通过 key 的类型的 Class 对象从 comparators 这个 ConcurrentHashMap 集合中获取一个排序比较器。
如果要获取,肯定得有人放进去
放的方法:
肯定在 Text 类中调用这个方法把 Text 的比较器放进去
Text 在加载到虚拟机中的时候要执行静态代码块,将自己的比较器放到 map 集合中
Text 的比较器就是 Comparator 内部类
LongWritable 类的注册方法:
LongWritable 的比较器:
按照数值序排序
第一次通过 key 的类型获取比较器,如果获取不到,则强制初始化 key 类,再次获取,再没有,就真没有,直接亲自下手
这个方法是最终要调用的比较方法
自定义比较器或者自定义 key 类型,最简单的就是实现这个方法:compareTo 方法
前两种还需要结合自定义排序比较器类一起使用,在自定义排序比较器中可以调用自定义 Key 类中的 compareTo 方法。
第三种方式(选取):
自定义 key 的时候,注册一下比较器
溢写的文件如果要 combine,需要达到 3 个小文件
没有 reduce 的情况(了解)
先看 NewDirectOutputCollector ,在没有 reduce 任务的时候直接将键值对写到 HDFS 中。
NewDirectOutputCollector 中的 write 方法调用了 out 的 write 方法
out 谁?
构造器中赋值
JobConf 类中的方法:找到默认的 OutputFormat 类就是 TextOutputFormat 类
表明调用的是 TextOutputFormat 类的 getRecordWriter 方法获取输出流
在 TextOutputFormat 类中有两个返回值,默认使用的是:
LineRecordWriter(fileOut, keyValueSeparator)
使用的是 LineRecordWriter 的 write 方法:
首先通过 writeObject 方法将 key 写出去,然后写一个键值对的分隔符,再写 value ,最后写一个换行符
分隔符是谁?
writeObject 如何写?
out 负责写,out 谁?
out 就是 FSDataOutputStream 输出流
file是谁?file是Path对象
file是谁?
如果没有 reduce 任务,则 map 的输出直接写到 HDFS 上。
总结:![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/d34764e33a7a93677d041dd2ab126a4f.png)
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/5a1ee211c728b6ed8c028858ab08aaba.png)
hadoop-mapreduce-client-core-2.6.5.jar
input real<<【TextInputFormat】LineRecordReader
mapper 被反射
input = new NewTrackingRecordReader<INKEY,INVALUE> (split, inputFormat, reporter, taskContext);
real:LineRecordReader(TextInputFormat)
nextKeyVlaue()
key,value赋值(pos,in.readline)
布尔返回
pos更新
getCurrentKey()
getCurrentValue()
in:SplitLineReader
input.initialize(split, mapperContext);
LineRecordReader. initialize
if (start != 0) { start += in.readLine(new Text(), 0, maxBytesToConsume(start)); }
this.pos = start;
本地化数据读取
反射map
准备input
本地读取(只读取自己计算的数据)seek
首行放弃(非第一个split)
pos更新
执行map
input. nextKeyVlaue()
更新key 和 value
通过getCurrentKey(),getCurrentValue(),取到更新后的值,传参给map
context.write(k,v)
output = new NewOutputCollector(taskContext, job, umbilical, reporter);
partitioner =自定义:hashpartitioner
collector = createSortingCollector(job, reporter);
MapOutputBuffer.init()
阈值:80%
size:100MB
【sorter:QuickSort算法】
环形缓冲区:字节数组
comparator = job.getOutputKeyComparator();
先取用户单独设置的比较器
再取map输出key的比较器
minSpillsForCombine = job.getInt(JobContext.MAP_COMBINE_MIN_SPILLS, 3);
spillThread:
sort
combiner
spill
write(k,v)
collector.collect(k,v,p)
output.close(mapperContext);
sortAndSpill();
mergeParts();
numSpills < minSpillsForCombine
查看Mapper类:
while (context.nextKeyValue()) {
map(context.getCurrentKey(), context.getCurrentValue(), context);
}
context.write(key,value);
Map环节:
输入来自哪里?
输出去向哪里?
run->runNewMapper
mapper
inputFormat::TextInputFormat
split::blocklocation
RecordReader::input
NewOutputCollector::output
MapContextImpl::mapContext
mapperContext
input.initialize(split, mapperContext);
mapper.run(mapperContext);
output.close(mapperContext);
input:
real = inputFormat.createRecordReader(split, taskContext);
new LineRecordReader(recordDelimiterBytes);
input.initialize
start = split.getStart();
end = start + split.getLength();
file = split.getPath();
fileIn.seek(start);
in = new UncompressedSplitLineReader(fileIn, job, this.recordDelimiterBytes, split.getLength());
if (start != 0) {
start += in.readLine(new Text(), 0, maxBytesToConsume(start));
}
pos = start;
nextKeyValue
key.set(pos);
in.readLine(value, maxLineLength, maxBytesToConsume(pos));
getCurrentKey()
getCurrentValue()
一个split对应一个map
split的大小在运行时改变
对于文本类输入,断行的处理机制
output
collector = createSortingCollector(job, reporter);
partitions = jobContext.getNumReduceTasks();
partitioner::0,hash,自定义
createSortingCollector
MapOutputBuffer
MapOutputBuffer::init
sortmb = job.getInt(JobContext.IO_SORT_MB, 100);
spillper = job.getFloat(JobContext.MAP_SORT_SPILL_PERCENT, (float)0.8)
sorter = ReflectionUtils.newInstance(job.getClass("map.sort.class",
QuickSort.class, IndexedSorter.class), job);
comparator = job.getOutputKeyComparator();
combinerRunner = CombinerRunner.create(job, getTaskID(),
combineInputCounter,
reporter, null);
minSpillsForCombine = job.getInt(JobContext.MAP_COMBINE_MIN_SPILLS, 3);
spillThread.start();::sortAndSpill();
追踪sort阶段QuickSort.class所使用的比较器
combiner
sortAndSpill()
sorter.sort(MapOutputBuffer.this, mstart, mend, reporter);
NewOutputCollector::write
collector.collect(key, value,
partitioner.getPartition(key, value, partitions));
output.close
collector.flush();
mergeParts();
mergeParts
kvIter = Merger.merge(job, rfs,
keyClass, valClass, codec,
segmentList, mergeFactor,
new Path(mapId.toString()),
job.getOutputKeyComparator(), reporter, sortSegments,
null, spilledRecordsCounter, sortPhase.phase(),
TaskType.MAP);
combinerRunner.combine(kvIter, combineCollector);
比较器终于出现了
combiner 可以先放一放,reduce 明白之后回来再细看
map 输出后,计算分区,进入内存缓冲区
内存缓冲区达到阈值触发溢写
溢写过程会按分区排序,分区内按 key 排序,如果配置了 combiner 还会出发合并
重复
map 结束后
合并溢写的小文件,按分区,按 key 排序
默认最小溢写数为 3,触发 combiner
1,输入
2,map>k:v
3,partitioner>k✌️p
4,compartor: 使用 key 自带的,客人认为干预
5,combiner:缩小 map 输出的数据量
6,合并小文件
combiner
7,最终生成一个独立的大文件:
按分区,同时,分区内 key 排序
**:hdfs ,block 有偏移量
**:MR,split 有偏移量
1,配置
2,提交:
1,检查路径
2,计算 split(大小,偏移量,位置信息)
那个 block 包含 split 的偏移量
取该 block 的副本位置信息
文件,offset,size,hosts
3,提交资源到 hdfs
4,提交作业
**原语:相同的key为一组,调用一次reduce方法,方法内迭代该组数据,进行计算
1,input:
seek
舍弃第一行(非第一个split)
2,map.run()
读懂数据
映射K:V
3,partitioner: 【k:v:p】
保证原语
注意数据倾斜
4,buffer
阈值
大小
5,sort:比较器
6,spill:combiner(1到2次)
7,合并小文件为一个大文件
sort
框架:ReduceTask
通过 ShuffleConsumerPlugin 插件进行洗牌的工作
rIter 迭代器是返回值
run 方法洗牌
可以设置 reduce 洗牌的线程数,并发下载数据
Fetcher 通过 http 或 https 的 get 下载 map 端的数据
上述方法真正从 map 端获取数据
openShuffleUrl 创建到 map 端 http 服务器的连接,并返回一个输入流,用于下载数据
connection 代表了一个 http 连接
洗牌结束之后,返回一个迭代器,用于遍历 reduce 端的数据集
上图是分组比较器
分组比较器是谁?
如果没有设置分组比较器,默认使用Map任务输出的Key的排序比较器:
maptask reducetask
分组比较器 用户定义
排序比较器 用户定义 用户定义
排序比较器 key自带的 key自带的
1) map&reduce 使用 key 自带的比较器,完成:排序和分组
2) map&reduce 使用用户定义的排序比较器,完成:排序和分组
3) map&reduce map 使用用户定义的排序比较器 , reduce 使用用户定义的分组比较器
4) map&reduce map 使用 key 自带的比较器 , reduce 使用用户定义的分组比较器
如果设置了,就反射生成比较器的对象返回
上图自定义分组比较器
可以使用自定义排序比较器的方式自定义分组比较器
看下一步的 runNewReducer 方法
reducer 类
默认使用 Reducer 类,如果有自定义,则使用自定义的 reducer 类
设置的时候,如何做?
设置的时候就是以相同字符串设置的
在 Reducer 的 run 方法中用到了 context 的四个方法:
context.nextKey
context.getCurrentKey
context.getValues
context.write
这四个方法是 Reducer.Context 的四个方法
看 Reducer.Context 是如何实现这四个方法的?
在创建 reducerCotnext 的时候,传的参数是 reducer(如何处理数据),rIter(从哪里读取数据),trackedRW(输出流,将处理结果输出到 HDFS), comparator (分组比较器),keyClass( Map 端输出 key 的类型),valueClass( Map 端输出 value 的类型)
首先真正的上下文对象是 ReduceContextImpl 类对象
reducerContext 是对 ReduceContextImpl 类对象的适配,代理,适配器模式
所以,reducerContext 的四个方法是 reduceContext 的四个方法
返回的 reducerContext 就是上图的 Context 对象,该对象封装了 reduceContext 对象,肯定是代理
上述四个方法都是代理的 ReduceContextImpl 的四个方法
下面看如何分组?
ReduceContextImpl 类的 nextKeyValue() 方法
上述 nextKeyIsSame 是 boolean 值,true 表示当前 key 和下一个 key 是同一组,否则不是同一组
nextKeyValue 方法:
首先取出当前的键值对
将指针向下移动一格
然后取出下一个键值对的 key
将当前 key 和下一个键值对的 key 进行比较,使用分组比较器比较
如果分组比较器返回 true ,表示当前 key 和下一个 key 是同一组数据
getValues 返回一个对象,该对象是:
内部类对象:ValueIterable 对象
当在自己的 reduce 方法中迭代这组数据进行计算的时候,需要:
调用 iterator 方法的时候,返回的是 ValueIterator 对象
自己的 reduce 中调用的是这个 ValueIterator 的 hasNext 和 next 方法
hasNext 方法何时为 true?
如果当前键值对是第一个键值对,或者当前键值对和下一个键值对是同一组数据,返回 true
在 next 方法中,如果是第一个键值对,直接返回
如果不是第一个键值对,则将指针向下移动一格,返回下一个 value设置分组比较器
如果 key 不是 null ,则传来的和返回的是同一个对象,只不过属性值不一样了
在 reduce 中反复调用 next 的时候,key 的值是否跟着变?
在 reduce 方法中,key 是一个引用对象,它就指向 key不仅 value 的值变,key 的值也要变,但是 key 是同一个对象。
在迭代同一组数据的时候,key 的值和 value 的值都变
如果在 reduce 中只取一部分值,剩下的值怎么办?
用于丢掉同一组没有处理完的数据,实际上在调用该方法的时候,nextKeyValue 方法要将指针一直向下移动到不是同一组数据,才开始新的 reduce 方法调用,新的一组数据开始处理。
总结
*,原语:相同的 key 为一组,调用一次 reduce,reduce 内部迭代该组数据进行计算
1,shuffle
copy:
2,sort
SecondarySort
group comparator
3,reduce
key,迭代器
shuffleConsumerPlugin.init(shuffleContext);
rIter = shuffleConsumerPlugin.run();
run:
rIter
comparator
框架是需要比较器的,做key的排序
Map:排序比较器的选取顺序:
1,取用户设置的排序比较器
2,取 key 自带的比较器
Reduce:RawComparator comparator = job.getOutputValueGroupingComparator();
1,取用户设置的分组比较器
2,取用户设置的排序比较器
3,取 key 自带的比较器
while (context.nextKey())
reduce(context.getCurrentKey(), context.getValues(), context);
key 是引用传递
nextKey():
nextKeyValue();
class ValueIterator
next()
nextKeyValue();
nextKeyValue():
更新 key,value
下一行 key 是否相同(分组比较器比较出来的)