文章从《Hadoop权威指南》以及《Hadoop技术内幕:深入解析MapReduce架构设计与实现原理》中总结而来。
四种Map Task:
- Job-setup Task:作业运行时启动的第一个任务
- Job-cleanup Task:作业运行时启动的最后一个任务
- Task-cleanup Task:任务失败或是被杀死后用于清理已写入临时目录中数据的任务
- Map Task: 处理数据,输出结果存到本地磁盘
Maptask通过用户编写的RecordReader,从输入InputSplit中解析出一个个key、value
将解析出的key、value交给用户编写的map()函数处理,产生一系列新的key、value
在用户编写的map()函数中,当数据处理完成后,一般会调用OutputCollector.collect()输出结果。
该函数内部会调用Partitioner产生key、value分片,并写入一个环形内存缓冲区中。
当环形缓冲区满后,MapReduce会将数据写到本地磁盘上,生成一个临时文件。
数据写入磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并,压缩等操作。
对所有的临时文件进行合并,以确保最终只生成一个文件。
Collect
每个map任务都有一个环形缓冲区,用于收集Map结果,减少磁盘IO的影响。
Mapper走完业务逻辑之后,调用MapOutputCollector.collect()输出结果。
MapOutputCollector(接口类)有两个实现类:
- MapOutputBuffer,写入环形缓冲区
- DirectMapOutputCollector-没有ReduceTask时调用,直接写入HDFS
MapOutputBuffer采用二级索引结构,涉及三个环形内存缓冲区,由io.sort.mb=100M
控制
1. kvoffsets-键值对索引的偏移量(在kvindice中进行查找)
- 一对键值对只占用kvoffsets的一个int大小
- 由三个变量控制,kvstart,kvend,kvindex
- 一开始kvstart=kvend,kvindex指向待写入位置,当写入一条数据后,kvindex向后移动一位
- 当kvoffsets使用率超过
io.sort.spill.percent=80%
后,数据开始溢出(spill)到磁盘 -
io.sort.record.percent
控制索引与实际数据占用内存大小的比值
2. kvindices-分区信息,键值对索引(在kvbuffer中进行查找)
- 一个键值对占用数组kvindices的3个int大小(分别保存partition号,key开始位置,value开始位置)
- Hadoop按比例1:3将大小为
io.sort.record.percent * io.sort.mb
的内存空间分配给数组kvoffsets和kvindices。
3. kvbuffer-键值对具体的值
- kvbuffer的读写操作由指针bufstart,bufend,bufindex,bufvoid,bufmark控制,前三者与kvoffsets中的作用相同。
- bufvoid指向缓冲区中有效内存结束位置。
- bufindex会跟随key值和value值移动。
- bufmark只会跟随value值移动。(bufmark≠bufindex时代表写入了key还没写入value)
- 当kvbuffer使用率超过
io.sort.spill.percent=80%
后,数据开始溢出到磁盘。 - 最多可以使用
io.sort.mb
的95%
生产者->MapOutputBuffer.collect(),MapOutputBuffer.Buffer.write()
消费者->SpillThread
//生产者部分主要伪代码
//取得下一个可写入的位置
spillLock.lock();
if(缓冲区使用率达到阈值){
//唤醒SpillThread线程,将缓冲区数据写入磁盘
spillReady.signal();
}
if(缓冲区满){
//等待SpillThread线程结束
spillDone.wait();
}
spillLock.lock();
//将数据写入缓冲区
Spill
- 由SpillThread线程完成,是缓冲区kvbuffer的消费者。
- SpillThread调用函数sortAndSpill()将环形缓冲区kvbuffer的区间[bufstart,bufend)内数据写到磁盘上。
以kvbuffer为例,令bufend=bufindex将缓冲区[bufstart,bufend)(留了最后一个index)之间的数据写出到磁盘。
达到soft buffer limit时,将会启动spill线程,spill线程以bufstart为读指针向bufend移动对磁盘进行写入,此时Map Task可写入;
当达到hard buffer limit时,缓冲区将会阻塞直到spill线程执行完毕。完成后,MapTask才可以继续向kvbuffer写入数据。
最后bufstart到达bufend位置,等待新一轮溢写。
跨界写入的两次内存复制与三次内存复制没怎么看懂,先留着位置
Hadoop0.21对环形内存区的修改
- 不再将索引和记录分放到不同的环形缓冲区中,而是让它们共用一个环形缓冲区。
- 引入一个新指针equator,该指针界定了索引和数据的共同起始位置。从位置开始,索引和数据分别沿着相反的方向增长内存使用空间。(使得
io.sort.record.percent
可以被舍弃,减少溢写次数)
MapTask端会产生一次溢写过程,Reduce端也会产生一次溢写过程。
sortAndSpill()
- 利用快速排序对缓冲区的区间内数据进行排序,先按分区编号partition排序,再按key排序(group by partition order by partition,key)
- 按照分区编号从小到大将数据写入临时文件output/spillN.out(N表示当前溢写次数),如果设置了Combiner,则写入文件之前,还会对每个分区进行一次聚集操作。
- 将分区数据的元数据写到内存索引数据结构SpillRecord中,元数据包括每个分区在spillN.out中的偏移量,压缩前后数据大小。若索引大小超过1M,则写到output/spillN.out.index中。
Combine
- 当所有数据处理完后,所有的临时文件会合并为一个大文件output/file.out同时生成索引文件output/file.out.index
- 以分区为单位合并,对于每个分区,采用多轮递归合并。
多轮递归合并
- 合并
io.sort.factor=100
个文件,产生新文件 - 将新文件加入待合并列表中
- 对文件列表重新排序
- 重复上述步骤