您现在的位置是:首页 >学无止境 >【MapReduce源码分析】网站首页学无止境

【MapReduce源码分析】

Al leng 2024-07-17 18:01:02
简介【MapReduce源码分析】

Client任务提交源码分析

  1. 客户端通过 hadoop jar 的命令形式来提交这个 jar 运行
    hadoop jar examples.jar WordCount /wc/input/ /wc/output/
    hadoop 这shell脚本:如果参数是jar, class=RunJar,从入口的RunJar开始查询
  • 1.1 获取到jar包中的main函数,通过反射调用真正需要执行的任务,任务最后调用job.waitForCompletion()提交任务到yarn上面
  • 1.2 首先获取到yarn的RPC服务的代理
    • 1、Job 的内部有一个 Cluster cluster 成员变量
    • 2、Cluster 的内部有一个 YARNRunner client 的成员变量
    • 3、YARNRunner 内部有一个 ResourceMgrDelegate resMgrDelegate 成员变量
    • 4、ResourceMgrDelegate 内部有一个 YarnClientImpl client 成员变量
    • 5、YarnClientImpl client 的内部有一个 ApplicationClientProtocol rmClient 的成员变量
  • 1.3 构造一个提交器,开始提交任务
    • 1.3.1 判断hdfs输出路径是否存在,调用yarn的RPC生成一个jobId,创建hdfs临时路径(/tmp/hadoop-yarn/staging/)用于提交昨夜等相关资源,将jar包等文件上传到临时目录里面
    • 1.3.2 writeSplits获取切片,切片的个数就是mapTask的个数
      • 1.3.2.1 computeSplitSize(blockSize, minSize, maxSize)获取切片的splitSize,和最大值Long.MAX_VALUE取最小值,和minSize 1 取最大值,其实就是取中间值
      • 1.3.2.2 首先定义bytesRemaining等于文件的长度,如果bytesRemaining) / splitSize > 1.1 ,那么继续遍历切分生成切片bytesRemaining -= splitSize;,否则直接生成一个切片
        • 例如300M文件 — (0-128M) — (128-256) – (256-300) 需要切分为三个切片,260M文件(128M — 132M)可以切分为两个分片,因为132/128<1.1。
      • 1.3.2.3 如果文件不可切的直接生成一个切片,如果文件大小为零也会生成一个空的切片,都加入到分片的列表中。
      • 1.3.2.4 将分片等信息提交到HDFS上,关于HDFS源码写文件流程请查看上篇HDFS源码解析https://blog.csdn.net/secret2316352792/article/details/130885271?spm=1001.2014.3001.5501
    • 1.3.3 将配置等文件写到HDFS上
    • 1.3.4 调用yarn RPC的代理ApplicationClientProtocol服务。最终由:ResourceManager 组件中的 ClientRMService实现了ApplicationClientProtocol 来执行 submitApplication 的 RPC 服务处理。
      • 1.3.4.1 首先做一些校验如判读是否重复提交,如果为指定队列则使用default队列,如果没有指定ApplicationName那么默认N/A,未设置ApplicationType那么默认使用YARN.
      • 1.3.4.2 提交到YARN上,就是初始化和注册 Application,向 ResourceManager 申请 Container 启动 MRAppMaster,然后 MRAppMaster 解析任务启动 MapTask 和 ReduceTask,这都属于 YARN 的调度源码请看后续的YARN源码解析,这块暂时不往下介绍。

MapTask源码分析

yarnChild启动类

  1. MRAppMaster 的服务器节点,发送shell命令给某一个 nodeManager,让 NM 启动 JVM, 其实就是初始化一个 Container,运行一个 Task,进而启动YarnChild的main方法。
  2. 初始化task的信息,判断如果ReduceTask的个数为零,那么MapTask的进度就是100%,如果有ReduceTask,那么Mapper的占比是2/3, Sorter 阶段是 1/3。初始化TextOutputFormat用于输出
  3. 启动MapTask
  • 3.1 初始化各种组件
    • 3.1.1 读入数据组件(TextInputFormat);初始化该MapTask需要执行的逻辑切片;初始化LineRecordReader用于读取数据,被NewTrackingRecordReader包装,LineRecordReader是NewTrackingRecordReader成员变量
    • 3.1.2 判断是否有ReduceTask,如果没有初始化NewDirectOutputCollector,如果有ReduceTask,初始化NewOutputCollector,内部有个成员变量MapOutputCollector,实现类是MapOutputBuffer,这个组件的内部管理了一个 100M 大小的 kvBuffer,就是所谓的环形缓冲区。
    • 3.1.3 获取reduce的个数,reduce的个数决定了分区的个数,如果分区的个数=1,那么就一个分区,否则就初始化一个分区器,默认是HashPartition。分区规则是key%(reduceTask的个数==分区的个数)。
  • 3.2 MapOutputBuffer环形缓冲区初始化
    • 3.2.1 初始化本地文件系统,mapper的输出结果写入到本地文件系统中。
    • 3.2.2 初始化环形缓冲区的大小默认100MB,默认的溢写的标准是内存大小的百分之八十。
    • 3.2.3 初始化溢写数据索引内存缓存的大小默认1MB,当溢出的文件数达到3个时,就要合并
    • 3.2.4 初始化环形缓冲区数据溢写到磁盘之前使用的算法是快速排序
    • 3.2.5 启动一个写线程,加锁保证只有一个线程在运行,当达到溢写的条件后spillReady.sinal通知线程开始执行溢写。
    • 3.2.6 使用快速排序对环形缓冲区的数据(按照分区和key)进行排序
    • 3.2.7 遍历分区如果设置了combiner则将数据局部聚合后写入到本地磁盘,否则直接将key和vaule数据写入到磁盘中。Reducer阶段有多少个Task,就证明有多少分区,那么这个溢写的数据文件,就分成几段。
    • 3.2.8 每个分区溢写完成后会生成索引记录,每个分区会生成一条索引,包含分区的起始偏移量和长度,溢写完成后判断索引的总大小超过了索引内存缓存的大小(1MB)那么就将数据写入到文件系统中,否则写入到内存中,溢写的次数加1。reducer阶段有多少个Task,就证明有多少分区,那么这个溢写的数据文件,就分成几段,某个reduceTask 过来拉取数据,必须要知道它拉取的是这个 数据文件中 那一段数据,最高效的方式,就是给每一个 分区 保存一些必要的信息(起始偏移量,长度)。
  • 3.3 input.initialize方法调用,input是NewTrackingRecordReader的实例,会调用LineRecordReader的成员变量,获取文件切片的开始位置和结束位置,并创建输入流用于输入。
    • 3.3.1 获取到该切片的起始和结束的偏移量,获取该文件的路径,打开文件的的输入流,定位到该文件的起始偏移量,根据该文件是否是压缩文件还是非压缩文件,创建不同的SplitLineReader读取器。注意这个时候还没有开始读取文件。
    • 3.3.2 如果判断出起始的偏移量不是0,那么说明不是第一个块,那么就跳过该文件块的第一行数据,因为执行上一块的读取的时候已经执行了next()会多读取下个块的第一行数据。目的是为了解决一行数据框了两个InputSplit放在两个块里面的问题。
  1. 运行MapTask任务mapper.run(mapperContext)
  • 4.1 context.nextKeyValue()遍历读取数据最终调用LineRecordReader的nextKeyValue一行一行读取数据,并将数据存放到上下文中,读取一行就调用map处理一行,key用于存储读取到的value在当前文件中的offset,value用于存储读取到的一行数据
    • 4.1.1 mapper其实是真正的要运行的类最终调用map(context.getCurrentKey(), context.getCurrentValue(), context),以WordCount这个MR程序为例,会调用Mapper下的map方法
    • 4.1.2 MR程序的map方法最终会拿到一行value数据,将value数据进行分隔,然后遍历读取打上特征。
    • 4.1.3 context.write(word, one)写出数据最终会调用NewOutputCollector来进行数据的写出写到环形缓冲区中
    • 4.1.4 每次写入到环形缓冲区中都需要先判断剩余的缓冲区,如果不满足溢写的条件就将数据写入到环形缓冲区中,如果满足溢写的条件,那么就调用spillReady.signal启动溢写的线程,将数据写入到磁盘中 详情见3.2.5-3.2.8。

ReduceTask源码分析

yarnChild启动类

  1. 初始化一些操作包括启动线程查看reduceTask的进度,初始化TextOutputFormat、jobContext。
  2. Reduce消费端拉取数据
  • 2.1 启动ShuffleConsumerPlugin这个类,默认实现是Shuffle。如果是本地的map端文件,那么就启动一个Fetcher线程拉取,如果是远程的默认启动五个Fetcher线程去拉取map端数据
  • 2.2 获取和map端的远程连接,获取到远程的输入流。
  • 2.3 从远程的输入流中获取到流的长度,如果长度大于最大的系统运行内存的memory0.70.25,则通过OnDiskMapOutput写入到磁盘中,否则InMemoryMapOutput写入到内存中
  • 2.4 reduce端进行合并
    • 2.4.1 如果拉取放到内存中的数据大于0,且磁盘中的文件块的个数小域100个,那么将内存中的数据先合并,然后写入到磁盘中,再加入到磁盘块的列表中,随后和磁盘一起再进行归并合并。
    • 2.4.2 如果磁盘中的文件大于100或者内存数据<0,那么就将内存中的数据加入到最终合并的列表中,合并多个磁盘文件也加入到最终的合并列表中,最后执行最终的合并,将内存和磁盘中的文件最终归并合并。
  • 2.5 最后关闭map端的远程连接。
  1. 运行ReduceTask
  • 3.1 初始化TaskContext和RecordWriter和reduceContext
  • 3.2 通过反射获取到真正要运行的Reduce方法,调用run方法,走到父类的run方法中。
  • 3.3 调用nextKey方法不断地将同一个key的的value写入到BackupStore这个里面,这个类实现了迭代器,通过getValues就能获取到这个key下面所有的values值。
  • 3.4 从RawKeyValueIterator获取key和value,写入到BackupStore,在获取下一个key和value,如果是同一个key就不停读取,把所有相同的key的数据都读取到BackupStore中,当读取完一个key的数据之后开始往下处理
  • 3.5 调用reduce(context.getCurrentKey(), context.getValues(), context),这个方法因为是子类继承了Reducer实现的方法,所以先走到子类,也即是这边的WordCount的程序的reduce方法,将同一个key下面所有的value根据业务逻辑来进行处理,最终将key和计算后的value值输出。写出规则是如果key不为空写key,都不为空写出key和value的分隔符,value不为空写出value,最后写换行符。
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。