一、数据输入

1.1、切片与 MapTask 并行度决定机制

1、问题引出
MapTask 的并行度决定 Map 阶段的任务处理并发度,进而影响到整个 Job 的处理速度。
思考:1G 的数据,启动 8 个 MapTask,可以提高集群的并发处理能力。那么 1K 的数 据,也启动 8 个 MapTask,会提高集群性能吗?MapTask 并行任务是否越多越好呢?哪些因
素影响了 MapTask 并行度?

2、MapTask 并行度决定机制
数据块:Block 是 HDFS 物理上把数据分成一块一块。
数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。
image.png

1.2、Job 提交流程

图解:
image.png
源码:

  1. job.waitForCompletion()
  2. submit();
  3. // 1、建立连接
  4. connect();
  5. // 创建提交Job的代理
  6. new Cluster(getConfiguration());
  7. // 判断是本地 yarn 还是远程
  8. initialize(jobTrackAddr, conf);
  9. // LocalClientProtocolProvider、YarnClientProtocolProvider
  10. clientProtocol = provider.create(conf);
  11. // 2、提交job
  12. submitter.submitJobInternal(Job.this, cluster)
  13. // 创建给集群提交数据的Stag路径
  14. Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
  15. // 获取 jobid ,并创建 Job 路径
  16. JobID jobId = submitClient.getNewJobID();
  17. // 拷贝jar包到集群
  18. copyAndConfigureFiles(job, submitJobDir);
  19. rUploader.uploadFiles(job, jobSubmitDir);
  20. // 计算切片,生成切片规划文件
  21. writeSplits(job, submitJobDir);
  22. maps = writeNewSplits(job, jobSubmitDir); input.getSplits(job);
  23. // 得到切片
  24. List<InputSplit> splits = input.getSplits(job);
  25. // 向Stag路径写XML配置文件
  26. writeConf(conf, submitJobFile);
  27. conf.writeXml(out);
  28. // 提交Job,返回提交状态
  29. status = submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());
  30. // 删除staging目录
  31. finally {
  32. jtFs.delete(submitJobDir, true);
  33. }

1.3、切片源码解析(input.getSplits(job))

步骤:

  • 程序先找到你数据存储的目录。
  • 开始遍历处理(规划切片)目录下的每一个文件

  • 遍历第一个文件ss.txt

    • 获取文件大小fs.sizeOf(ss.txt)

    • 计算切片大小 computeSplitSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M

    • 默认情况下,切片大小=blocksize

    • 开始切,形成第1个切片: ss.txt—0:128M,第2个切片: ss.txt—128:256M,第3个切片: ss.txt—256M:300M (每次切片时,都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍不进行划分)
    • 将切片信息写到一个切片规划文件中

    • 整个切片的核心过程在getSplit()方法中完成

    • InputSplit只记录了切片的元数据信息,比如起始位置、长度以及所在的节点列表等。

  • 提交切片规划文件到YARN上,YARN上的MrAppMaster就可以根据切片规划文件计算开启MapTask个数。

源码: 以 FileInputFormat 为例

  1. // 计算切片,生成切片规划文件
  2. writeSplits(job, submitJobDir);
  3. maps = writeNewSplits(job, jobSubmitDir); input.getSplits(job);
  4. // 得到切片
  5. List<InputSplit> splits = input.getSplits(job);
  6. // 以FileInputFormat为例
  7. long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job)); // 默认值为1
  8. long maxSize = getMaxSplitSize(job); // 默认值为 Long.MAX_VALUE
  9. List<FileStatus> files = listStatus(job);
  10. for (FileStatus file: files) {
  11. if (isSplitable(job, path)) {
  12. // 得到文件的实际存储的块大小
  13. blockSize = file.getBlockSize();
  14. // 计算逻辑分片的大小
  15. splitSize = computeSplitSize(blockSize, minSize, maxSize);
  16. Math.max(minSize, Math.min(maxSize, blockSize));
  17. // 文件切分后剩余大小
  18. bytesRemaining = length;
  19. // SPLIT_SLOP 为 1.1
  20. while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
  21. int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
  22. splits.add(makeSplit(path, length-bytesRemaining, splitSize,
  23. blkLocations[blkIndex].getHosts(),
  24. blkLocations[blkIndex].getCachedHosts()));
  25. bytesRemaining -= splitSize;
  26. }
  27. }
  28. }
  29. // 返回计算后的切片
  30. return splits

1.3.1、FileInputFormat切片机制

1、切片机制

  • 简单地按照文件的内容长度进行切片

  • 切片大小,默认等于Block大小

  • 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片

2、案例分析
输入数据有两个文件:

  1. file1.txt 300M
  2. file2.txt 10M

经过FileInputFormat的切片机制运算,形成的切片信息如下:

  1. file1.txt.split1 0~128M
  2. file1.txt.split2 128~256M
  3. file1.txt.split3 256~300M
  4. file2.txt.split1 10M

3、源码中计算切片大小的公式

  1. Math.max(minSize, Math.min(maxSize, blockSize));
  2. mapreduce.input.fileinputformat.split.minsize=1; 默认值为1
  3. mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue; 默认值Long.MAXValue

因此,默认情况下,切片大小=blocksize

4、切片大小设置
maxsize(切片最大值):参数如果调得比blockSize小,则会让切片变小,而且就等于配置的这个参数的值。 minsize(切片最小值):参数调的比blockSize大,则可以让切片变得比blockSize还大。

5、获取切片信息API

  1. // 根据文件类型获取切片信息
  2. FileSplit inputSplit = (FileSplit) context.getInputSplit();
  3. // 获取切片的文件名称
  4. String name = inputSplit.getPath().getName();

1.3.2、CombineTextInputFormat 切片机制


框架默认的 TextInputFormat 切片机制是对任务按文件规划切片,不管文件多小,都会 是一个单独的切片,都会交给一个 MapTask,这样如果有大量小文件,就会产生大量的 MapTask,处理效率极其低下。

1、应用场景:
CombineTextInputFormat 用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个 MapTask 处理。

  1. <br />2、虚拟存储切片最大值设置
  1. CombineTextInputFormat.setMaxInputSplitSize(job, 4194304); // 4m

注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值

  1. <br />3、切片机制<br />生成切片过程包括: 虚拟存储过程和切片过程 两部分
  • 虚拟存储过程
    • 将输入目录下所有文件大小,依次和设置的 setMaxInputSplitSize 值比较,如果不大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍, 那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值 2 倍,此时 将文件均分成 2 个虚拟存储块(防止出现太小切片)。
      1. 例如:
      2. setMaxInputSplitSize 值为 4M,输入文件大小为 8.02M,则先逻辑上分成一个
      3. 4M。剩余的大小为 4.02M,如果按照 4M 逻辑划分,就会出现 0.02M 的小的虚拟存储
      4. 文件,所以将剩余的 4.02M 文件切分成(2.01M 2.01M)两个文件。
  • 切片过程:

    • 判断虚拟存储的文件大小是否大于 setMaxInputSplitSize 值,大于等于则单独 形成一个切片。

    • 如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。

测试举例:
image.png

1.4、FileInputFormat实现类

思考:在运行MapReduce程序时,输入的文件格式包括:基于行的日志文件、 二进制格式文件、数据库表等。那么,针对不同的数据类型,MapReduce是如何读取这些数据的呢?

FileInputFormat 常 见 的 接 口 实 现 类 包 括 : TextInputFormat 、 KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat和自定义 InputFormat 等。

1.4.1、TextInputFormat

TextInputFormat是默认的FileInputFormat实现类。按行读取每条记录。键是存储该行在整个文件中的 起始字节偏移量, LongWritable类型。值是这行的内容,不包括任何行终止符(换行符和回车符), Text类型。
以下是一个示例,比如,一个分片包含了如下4条文本记录:

  1. Rich learning form
  2. Intelligent learning engine
  3. Learning more convenient
  4. From the real demand for more close to the enterprise

每条记录表示为以下键/值对:

  1. (0,Rich learning form)
  2. (19,Intelligent learning engine)
  3. (47,Learning more convenient)
  4. (72,From the real demand for more close to the enterprise)

实际案例:

https://github.com/Wells-Lee/mapreduce-demo/tree/master/src/main/java/com/wells/demo/inputformat/textinput

1.4.2、KeyValueTextInputFormat

每 一 行 均 为 一 条 记 录 , 被 分 隔 符 分 割 为 key , value 。 可 以 通 过 在 驱 动 类 中 设 置

  1. conf.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR, "\t");
  2. job.setInputFormatClass(KeyValueTextInputFormat.class);

来设定分隔符。默认分隔符是tab(\t)。
以下是一个示例,输入是一个包含4条记录的分片。其中 ——> 表示一个(水平方向的)制表符:

  1. line1 ——>Rich learning form
  2. line2 ——>Intelligent learning engine
  3. line3 ——>Learning more convenient
  4. line4 ——>From the real demand for more close to the enterprise

每条记录表示为以下键/值对:

  1. (line1,Rich learning form)
  2. (line2,Intelligent learning engine)
  3. (line3,Learning more convenient)
  4. (line4,From the real demand for more close to the enterprise)

此时的键是每行排在制表符之前的Text序列。

实际案例:

https://github.com/Wells-Lee/mapreduce-demo/tree/master/src/main/java/com/wells/demo/inputformat/kvinput

1.4.3、NLineInputFormat

如果使用NlineInputFormat,代表每个map进程处理的InputSplit不再按切片块去划分,而是按
NlineInputFormat指定的行数N来划分。

  1. job.setInputFormatClass(NLineInputFormat.class);
  2. NLineInputFormat.setNumLinesPerSplit(job, 10);

即 输入文件的总行数/N=切片数,如果不整除,切片数=商+1。
以下是一个示例,仍然以上面的4行输入为例:

  1. Rich learning form
  2. Intelligent learning engine
  3. Learning more convenient
  4. From the real demand for more close to the enterprise

例如,如果N是2,则每个输入分片包含两行。开启2个MapTask:

  1. (0,Rich learning form)
  2. (19,Intelligent learning engine)

另一个 mapper 则收到后两行:

  1. (47,Learning more convenient)
  2. (72,From the real demand for more close to the enterprise)

这里的键和值与TextInputFormat生成的一样。

实际案例:

https://github.com/Wells-Lee/mapreduce-demo/tree/master/src/main/java/com/wells/demo/inputformat/nlineinput

1.5、自定义InputFormat

在企业开发中,Hadoop框架自带的InputFormat类型不能满足所有应用场景,需要自定义InputFormat来解决实际问题。
自定义InputFormat步骤如下( 以输出的key为文件名,value为文件内容为例 ):

  • 自定义一个类继承FileInputFormat。

  • 改写RecordReader,实现一次读取一个完整文件封装为KV。

  • 在输出时使用SequenceFileOutPutFormat输出合并文件:是因为value为文件内容,采用的事BytesWritable。

1.5.1、案例

无论 HDFS 还是 MapReduce,在处理小文件时效率都非常低,但又难免面临处理大量小文件的场景,此时,就需要有相应解决方案。可以自定义 InputFormat 实现输出结果小文件的合并

需求

将多个小文件合并成一个 SequenceFile 文件(SequenceFile 文件是 Hadoop 用来存储二 进制形式的 key-value 对的文件格式),SequenceFile 里面存储着多个文件,存储的形式为文件路径+名称为 key,文件内容为 value。

分区数等于设置的numReduceTasks,默认为1

  1. partitions = jobContext.getNumReduceTasks();
  • 输入数据
    one.txt、two.txt、three.txt
  • 期望输出文件格式:part-r-00000

实际案例:

https://github.com/Wells-Lee/mapreduce-demo/tree/master/src/main/java/com/wells/demo/inputformat/custom

二、详细工作流程

流程图

Map工作流程:

image.png

Reduce工作流程:
image.png

流程详解:

上面的流程是整个 MapReduce 最全工作流程,但是 Shuffle 过程只是从第 7 步开始到第16 步结束,具体 Shuffle 过程详解,如下:

  • MapTask 收集我们的 map()方法输出的 kv 对,放到内存缓冲区中

  • 从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件( 分区内有序:快速排序 )

  • 多个溢出文件会被合并成大的溢出文件
  • 在溢出过程及合并的过程中,都要调用 Partitioner 进行分区和针对 key 进行排序

  • ReduceTask 根据自己的分区号,去各个 MapTask 机器上取相应的结果分区数据

  • ReduceTask 会取到同一个分区的来自不同 MapTask 的结果文件,ReduceTask 会将这些文件再进行合并(归并排序)

  • 合并成大文件后,Shuffle 的过程也就结束了,后面进入 ReduceTask 的逻辑运算过程 (从文件中取出一个一个的键值对 Group,调用用户自定义的 reduce()方法)

注意:
Shuffle 中的缓冲区大小会影响到 MapReduce 程序的执行效率,原则上说,缓冲区越大, 磁盘 io 的次数越少,执行速度就越快。
缓冲区的大小可以通过参数调整,参数:io.sort.mb 默认 100M