Hadoop-MapReduce源码分析

575 阅读6分钟

源码分析

分布式计算追求:

  • 计算向数据移动
  • 并行度、分而治之
  • 数据本地化读取

Client

没有计算发生。

支撑了计算向数据移动,和计算的并行度。

做的最主要的是:存储与计算解耦,就是对所要进行切片的数据进行split切片,(split == map并行度)。

Split默认是与Block块数量一致,目的是为了计算向数据移动,几个Block块分布在几个地方,就起几个Map,这样就不需要让大量数据进行移动,而是只需要将Jar包分发到各个Block所在的结点进行执行即可。

用户也可以对Split进行自定义:

当Split大于Block时,将会有部分数据向计算移动,但从原来的单机Map,会将所有数据拉取到一个结点上,考虑来说,这其实也是计算向数据移动。

当Split小于Block时,一般用于CPU密集型计算,也会有部分数据向计算移动。

MapTask

如何解决HDFS,Block刚好把一个单词切割开的问题?

即蓝色部分为Block01内容,红色部分为Block02内容。

Hello World!!!

Hello World!!!

Hello World!!!

其中第二行中的World单词被切割了,但是在进行MapReduce-WordCount计算时,Hadoop框架仍然能返回一个正确的值给我们,Hadoop框架是如何实现的呢?

阅读原码可以发现。MapReduce在使用LineRecordReader时,会判断,如果当前开始的文件偏移量不为0时(即不是第一个切片文件时),会读一行空行,即将偏移量移至Block的第二行开头。即示例中,偏移到全文的第三行开头处,然后在Map执行时,框架会将第二个Block中没有被处理的数据移动向第一个Block所处的位置进行计算,这样就可以解决单词被Block切分开的问题了。


Input -> map -> output

  • Input(split+format):来自于我们的输入格式化类给我们实际返回的记录读取器对象。

    TextInputFormat -> LineRecordReader

    Split:file、offset、length

    init():

    • in = fs.open(file).seek(offset)
    • 除了第一个Map之外,之后的Map都会让出第一行,从Split的第二行读起。换言之,前一个Map会多读取一行,来弥补HDFS把数据切割问题。

    nextKeyValue():

    1. 读取数据中的一条Key,Value数据
    2. 返回Boolean值

    getCurrentKey()

    getCurrentValue()

  • output

    • 判断是否需要Reduce?

      • 需要,则再次判断Reduce需要几个分区(Partition)?若为1个,则分区器都指定为0,若分区数大于1个,则默认使用Hash分区器进行分区,也可以通过自定义分区器进行。
      • 不需要,则直接写出。
    • 写出是通过writer写出(K, V, P)的格式到缓存区(MapOutPutBuffer)中。

    • MapOutPutBuffer:

      • *:

        • map()方法执行完毕之后,会进行计算对应的partion值,再通过writer通过三元组(K、V、P)的方式写到缓冲区(可以理解为二位数组)中。

        • 缓冲区:

          1. 缓冲区存储的数据为对应的K、V数据,及其元数据(索引,大小恒定为16byte,存储的是P值、KeyStart、ValueStart、ValueLength),这样我们就可以通过整齐排序的索引数据,随机取出对应数据的KEY。

          2. 环形缓冲区,其实是逻辑上为环形,其实际内存结构仍为线性。

            原理:

            通过在缓冲区中划分一个界线,在界线的两边分别存储,KV数据与索引数据,当缓冲区占用率达到80%时,锁住已经有数据的缓冲区,进行快速排序并且溢写;与此同时,在没有数据的那一边的环形缓冲区,再次划分一个界线,再次在界线两边存储数据。若在缓冲区还没有充满的同时,溢写数据结束了,那则可以不影响到写的数据的线程进行。

            优点:更好地利用了内存空间,提高了效率。

          3. 溢写数据前先进行快速排序,先按分区值进行排序,再按Key值进行排序;排序进行的交换数据,是交换索引数据而不是KV数据,原因是:KV数据大小参差不齐,大小一旦不一致,就不能进行数据交换,而KV数据是恒定16byte的,可以进行交换。写出时候,根据索引取出对应的元数据,再通过指向的内存位置,卸下对应的KV数据即可完成排序后溢写。

          4. Combiner优化(减少IO),Combiner相当于在Map中提前进行合并,发生在排序之后,在溢写之前。在多个小文件通过归并排序合并成一个大文件的时候,若小文件的个数大于等于minSpillsForCombine(默认 = 3)值时,也会进行一次合并。

            (Tips:为什么每一个Map的数据需要由多个小文件合并成一个大文件?磁盘IO是MapReduce的性能瓶颈,合并文件可以避免磁盘的随机读取,加快IO速度。)

          5. Combine须注意:合并必须遵循幂等性,即合并数据的过程不应该使最终结果发生错误。

      • Init() :

        spiller: 0.8 缓冲区溢写百分比,默认为80%

        sortmb:100 缓冲区大小

        sorter:QuickSort 排序算法

        comparator:job.getOutputKeyComparator()

        • 优先取用户自定义比较器
        • 默认取这个Key类型自身的比较器
    • Combiner: 用于提前合并相同的Key的K-V数据。

      minSpillsForCombine = 3

    • SpillThread

      sortAndSpill() 主要用于做排序并溢血。

      并判断是否需要合并,若需要合并,则进行。

ReduceTask

input -> reduce -> output

rIter - input : 会到所有的Map端产生的文件,拉取对应的数据到Reduce端,并经过sort归并排序成一个大文件,并将其封装成一个迭代器。

而在reduce()方法运行时,也是通过传递一个“假”迭代器values,来将同一个key的数据加载进行的。

values迭代器通过hasNext()方法判断是否还有相同Key的数据,其实就是通过nextKeyIsSame值(下一个值的Key是否与当前Key相等)来判断,当前同一个Key为一组的数据是否还有下一条数据。

而通过next()方法则可以间接调用rIter的nextKeyValue()拉取出一条数据,通过再多读一条数据,判断下一个数据是否是与当前数据的Key相同,更新nextKeyIsSame值。

分组排序器:将大文件中K-V数据,分成一组进行Reduce操作,若用户没有指定,默认使用Key分组进行。

为什么使用迭代器?

避免OOM:大数据文件很大,不能全部加载到内存中。

减少IO成本:同一组中可能会有多个不同的Key,这些不同的Key需要进入不同的reduce()方法中,只需要真迭代器从头开始读,而每次当需要处理新的一组Key数据时,只需要通过假迭代器配合真迭代器去读出数据即可完成ReduceTask操作,整个ReduceTask只需要真迭代器进行的一次I/O,假迭代器进行间接调用。