2021-02-01 大数据课程笔记 day12

时间煮雨
@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 方法在这里插入图片描述

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 上。

总结:在这里插入图片描述在这里插入图片描述
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 是否相同(分组比较器比较出来的)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值