整个Map 阶段的流程大体:
简单概述: inputFile 通过split 被逻辑切分成多个 split 文件,通过 Record 按行读取内容给map (用户自己实现的)进行处理,数据被map 处理结束后,交给OutputCollector 收集器,对其结果 key 进行分区(默认使用 hash 分区),然后写入buffer ,每个 map Task 都有一个内存缓存区,存储着 map 的输出结果,当缓冲区快满的时候需要将缓冲区的数据以临时文件的方式存储到磁盘,当整个map task 结束之后再对磁盘中这个map task 产生的所有临时文件做合并,生成最终的正式输出文件,然后等待reduce task 来拉数据;
详细步骤:
- 读取数据组件 InputFormat (默认 TextInputFormat ) 会通过 getSplits 方法对输入目录中文件进行逻辑切片规划得到 block ,有多少 block 就对应启动多少个 MapTask.
- 将输入文件切分为 block 之后,由 RecordReader 对象( 默认是 LinkRecordReader) 进行读取,以 \n 作为分隔符,读取一行数据,返回< key ,value>. Key 表示每行首字母偏移值,value 表示这一行的文本内容.
- 读取 block 返回 <key,value> ,进入用户自己继承的 Mapper 类中,执行用户重写的map 函数,RecordReader 读取一行这里调用一次.
- Mapper 逻辑结束之后,将Mapper 的每条结果通过 context.write 进行collect 数据收集,在 collect 中,会先对其进行分区处理,默认使用HashParttioner
MapReduce 提供 Partitioner 接口,它的作用就是根据 key 或 value 及 Reducer 的数量来决定当前的针对输出数据最终应该由哪个 Reducetask 处理,默认对 key Hash 后再以 Reducer 数量取模,默认的取模方式只是为了 平均 Reducer 的处理能力,如果用户自己对 Partitioner 有需求,可以定制并设置到 Job 上.
- 接下来,会将数据写入到内存,内存中的这片区域叫做环形缓冲区,缓冲区的作用就是批量收集 Mapper 的结果,减少磁盘 IO 的影响,我们的 KEY/Value 对以及 Partition 的结果都会被写入到缓冲区,当然,写入之前,key 与 Value 值都会被序列化成字节数组.
- 环形缓冲区其实是一个数组,数组中存放着 Key,Value 的序列化数据 和 Key,Value 的元数据信息, 包括 Partition, Key 的起始位置,Value 的起始位置 以及 Value的长度,环形结构是一个抽象概念.
- 缓冲区是有大小限制的,默认是 100MB ,当 Mapper 的输出结果过多的时候,就可能会撑爆缓存,所以需要在一定的条件下将缓冲区中的数据临时写入到磁盘,然后重新利用这块缓冲区,这个从内存往磁盘写数据的过程叫做Spill ,中文可以译为溢写,这个溢写是由单独线程来完成的,不影响往缓冲区写Mapper 结果的线程,溢写线程启动时不应该阻止 Mapper 的结果输出,所以真个缓冲区有个溢写的比列
- 这个比例默认是百分之80 ,也就是 80M ,也就是当缓冲区的数据已经到达阈值,溢写线程启动,锁定这 80 M 的内存,执行溢写过程, Mapper 的输出结果还可以往剩下的 20 M 内存中写,互相不影响结果.
- 当溢写线程启动后,需要对这 80 M ,空间内的 Key 做排序(Sort ),排序是 MapReduce 模型的默认行为,这里的排序也是对序列化的字节做排序.
- 如果 Job 设置过 Combiner,那么现在就是使用 Combiner 的时候了,将有相同的Key 的 Key/Value 对的 Value 加起来,减少溢写到磁盘的数据量,Combiner 会优化MapReduce 的中间结果,所以它在真个模型中会多次使用.
- 哪些场景需要使用Combiner 呢,从这里分析,Combiner 的输出是Reducer 的输入,Combiner 绝对不能改变最终的计算结果,Combiner