mapreduce
Mapreduce是什么?
是一个分布式计算框架,支持少量代码实现海量数据并发处理程序。多个服务器同时进行计算,极大的提高了计算效率。
Mapreduce优点:
良好的扩展性:可以通过简单的增加机器扩展计算能力 容错性:其中一台机器挂掉了,上面的计算任务自动转移到另一个节点运行
Mapreduce缺点:
MapReduce无法在毫秒或者秒级时间内返回结果。 后一个应用程序的输入为前一个的输出,MapReduce的输入输出都会写入到磁盘,会造成大量的磁盘IO,会导致性能底下。
一、map过程
一个mapper节点可能有多个文件
- 文件会被切分为三个block,(hadoop2.0以后默认每个datanode的block大小为128M),每个split与maptask是一一对应的关系。split的数量由
InputSplitFormat类的getSplits决定
1.1 启动map
//默认使用新版mapper
public void run{
···········//省略,此处进行选择新旧版api,以新版api为例
if (useNewApi) {
runNewMapper(job, splitMetaInfo, umbilical, reporter);
} else {
runOldMapper(job, splitMetaInfo, umbilical, reporter);
}
}
运行一个新的mapper时候,mapTask会做什么?
void runNewMapper(final JobConf job,final TaskSplitIndex splitIndex,final TaskUmbilicalProtocol umbilical,
TaskReporter reporter) throws IOException, ClassNotFoundException,InterruptedException {
// 创建taskContext 用于从conf配置文件中获取classes
// 使用反射设置mapper.class
ReflectionUtils.newInstance(taskContext.getMapperClass(), job); 设置mapper
// 设置InputFormat.class
ReflectionUtils.newInstance(taskContext.getInputFormatClass(), job);
// 设置inputsplit.class
split = getSplitDetails(new Path(splitIndex.getSplitLocation()),splitIndex.getStartOffset());
// 将输入转换为需要的 <k,v>结构,作为mapper的输入
org.apache.hadoop.mapreduce.RecordReader<INKEY,INVALUE> input =
new NewTrackingRecordReader<INKEY,INVALUE>(split, inputFormat, reporter, taskContext);
// 判断reduce是否为0,为0则将map输出落hdfs,否则将输出暂存本地磁盘进一步处理传给reducer
if (job.getNumReduceTasks() == 0) {
output =
new NewDirectOutputCollector(taskContext, job, umbilical, reporter);
} else {
output = new NewOutputCollector(taskContext, job, umbilical, reporter);
}
·········
// 运行mapper函数
//依次执行 setup->map->cleanup
mapper.run(mapperContext);
我们来看看单个mapper运行时分哪些步骤?
public void run(Context context) throws IOException, InterruptedException {
//setup主要进行资源的集中创建,根据实际解析需要重写
setup(context);
try {
//通过调用RecordReader 不断获取下一行数据并设置为CurrentKey/CurrentValue,作为map的<KeyIn,ValueIn>调用至map处理
while (context.nextKeyValue()) {
// 我们需要重写处理逻辑解析函数map
map(context.getCurrentKey(), context.getCurrentValue(), context);
}} finally {
//cleanup做资源的清理工作
cleanup(context);
}}}
//每行数据都会调用一次map函数,我们最终需要把处理完的结果写入到map中,作为KEYOUT/VALUEOUT
protected void map(KEYIN key, VALUEIN value,
Context context) throws IOException, InterruptedException {
//这里写我们的处理逻辑,最终要写到context中
context.write((KEYOUT) key, (VALUEOUT) value);
}
2. shuffle
2.1 partition:
-
要求:将具有同一key的数据放置于同一节点上(超级key?)
-
使用哈希进行分区来实现同key同区(partition的数目等于reducer) 但是不同key也可在一partition
2.2 spill
- 环形缓冲区写满默认的80%数据后,触发一次溢写过程,此时同时存在两个过程:
- 此80%的数据由内存写入磁盘
- 剩余的20%空间继续写入数据到内存
- 当剩余的20%空间也写满&&磁盘数据未写完时,只能等待剩余数据写入磁盘,不可在进行写入操作。
- 当数据全部落入磁盘,本轮spill完成,开始新一轮的写入,溢出。 环形缓冲区结构:
/ 输出结果,进入collect函数,获取分区信息,将数据写入内存缓冲区,写满后进行flush到磁盘
// 环形缓冲区
// start spill if the thread is not running and the soft limit has been
// 当写达到缓冲区阈值后进行写磁盘->临时文件 ,此时进行分partition排序 ,快排序先比较partition,在比较值
// 排序完成后,生成一堆小文件
sortAndSpill();
//此时在按着堆排序和多路归并,生成一个大文件,大文件中按partition排列 ,每个partition区域内有序。
2.3 merge
每个maptask可能产生多个溢写文件 ->多路归并(小顶堆维护最小值) -->mapout
- sort :
- 按key排序,排序方式可自定义
- combine:
- 可以看为一次小的reduce,避免过多过大的文件参与到reduce过程中,先使用小计算进行合并
- 非默认过程,需要手动设置
job.setCombinerClass(myCombine.class - 计算规则与reduce一致,但是不是每种作业都可以做combine操作的,比如wordcount可以,(可以理解为计算顺序不影响结果的计算就可以进行combine?)
- 要求reduce的输入输出类型都一样,因为combine本质上就是reduce操作,不能影响后续的reduce输入输出
- combine操作后不会影响计算结果,像求和就不会影响
进行merge的准备工作:mergeParts,包装segments
// 索引列表:记录每条记录的开始位置,长度,所属的partition,后面的merge通过这个来找到属于每个partition的记录
public class IndexRecord {
public long startOffset;
public long rawLength;
public long partLength;
······
}
// 将多个文件进行合并的函数
MapOutputCollector.mergeParts()
{
// 在partition次(多路归并+堆排序)中将内部有序的多个小文件合并为一个按partition划分生成一个具有起始值,长度,以及索引的大文件,与此同时原文件将被删除
for (int parts = 0; parts < partitions; parts++) {
// 1. 获取该partition的小文件们
for(int i = 0; i < numSpills; i++) {
filename[i] = mapOutputFile.getSpillFile(i);
finalOutFileSize += rfs.getFileStatus(filename[i]).getLen();
}
// 2. 将索引到的属于此partition的记录包装为segment对象,每个segment均为关于某个partition的有序记录,获取到的多个segment为所有文件关于此partition的记录
for(int i = 0; i < numSpills; i++) {
//从索引列表中查询这个partition对应的所有索引信息
IndexRecord indexRecord = indexCacheList.get(i).getIndex(parts);
//将此条记录包装为segment对象,后续操作均为segment
Segment<K,V> s =
new Segment<K,V>(job, rfs, filename[i], indexRecord.startOffset,
indexRecord.partLength, codec, true);
segmentList.add(i, s);
if (LOG.isDebugEnabled()) {
LOG.debug("MapId=" + mapId + " Reducer=" + parts +
"Spill =" + i + "(" + indexRecord.startOffset + "," +
indexRecord.rawLength + ", " + indexRecord.partLength + ")");
}
}
// 3. 进行merge操作 kvIter 为可迭代对象,不断移动堆进行排序,每个可迭代对象都是一个个segment(内部有序记录),需要用最小堆维护最小值(不断获取多个segment的的最小一条记录),进行多路归并排序
RawKeyValueIterator kvIter = Merger.merge(job, rfs·······segmentList, mergeFactor)
// 4. 将排好序的文件写入到磁盘
long segmentStart = finalOut.getPos();
Writer<K, V> writer = new Writer<K, V>(job, finalOut, keyClass, valClass, codec,
spilledRecordsCounter);
if (combinerRunner == null || numSpills < minSpillsForCombine) {
Merger.writeFile(kvIter, writer, reporter, job);
} else { // 有combine则进行
combineCollector.setWriter(writer);
combinerRunner.combine(kvIter, combineCollector);
}
// 5.记录此次partition归并排序后写入文件的位置,数据长度,压缩后的数据长度,多个partition在一个文件中据此区分起始
rec.startOffset = segmentStart;
rec.rawLength = writer.getRawLength();
rec.partLength = writer.getCompressedLength();
spillRec.putIndex(rec, parts);
}
spillRec.writeToFile(finalIndexFile, job);
finalOut.close();
for(int i = 0; i < numSpills; i++) { //删除小文件
rfs.delete(filename[i],true);
}
我们接下来看看merge操作主要过程
RawKeyValueIterator merge(Class<K> keyClass, Class<V> valueClass,
int factor, int inMem, Path tmpDir,
Counters.Counter readsCounter,
Counters.Counter writesCounter,
Progress mergePhase)
// 每个map排序生成一堆小文件,每个文件按partition 划分出一个个segment (此处应有图)
// 我们需要将这些有序segment进行用多路归并排序。
// 多路归并排序使用多个文件的最小值,使用小顶堆方式构建最小值
do {
//每次merge 默认100个segment
initialize(segmentsToMerge.size());
clear();
for (Segment<K, V> segment : segmentsToMerge) {
//将segment加入小顶堆中,维护最小segment队列
put(segment);
}
Writer<K, V> writer =
new Writer<K, V>(conf, fs, outputFile, keyClass, valueClass, codec,
writesCounter);
// 将排好序的segment,使用writer进行写文件
writeFile(this, writer, reporter, conf);
//排序好的segments
Segment<K, V> tempSegment = new Segment<K, V>(conf, fs, outputFile, codec, false);
// Insert new merged segment into the sorted list
// 将新排好序的segment 插入到排序好的segment list中 二分查找
int pos = Collections.binarySearch(segments, tempSegment,segmentComparator);
}while(true)
3.reduce
3.1 copy
主要是去每个map的输出文件中copy自己partition部分的数据
- 当map任务完成超过一定百分比后,触发了reduce进程,通过http请求map端数据到内存中处理,与map端内存缓冲区类似,溢写文件过程类似
//将map输出文件从每个map中复制自己的分片数据,保存到内存或磁盘
public RawKeyValueIterator run() throws IOException, InterruptedException {
// Scale the maximum events we fetch per RPC call to mitigate OOM issues
// on the ApplicationMaster when a thundering herd of reducers fetch events
int eventsPerReducer = Math.max(MIN_EVENTS_TO_FETCH,
MAX_RPC_OUTSTANDING_EVENTS / jobConf.getNumReduceTasks());
int maxEventsToFetch = Math.min(MAX_EVENTS_TO_FETCH, eventsPerReducer);
// 启动获取map完成情况的线程以及获取map输出的线程
// Start the map-completion events fetcher thread
final EventFetcher<K,V> eventFetcher =
new EventFetcher<K,V>(reduceId, umbilical, scheduler, this,
maxEventsToFetch);
eventFetcher.start();
// Start the map-output fetcher threads
final int numFetchers = jobConf.getInt(MRJobConfig.SHUFFLE_PARALLEL_COPIES, 5);
Fetcher<K,V>[] fetchers = new Fetcher[numFetchers];
for (int i=0; i < numFetchers; ++i) {
fetchers[i] = new Fetcher<K,V>(jobConf, reduceId, scheduler, merger,
reporter, metrics, this,
reduceTask.getShuffleSecret());
fetchers[i].start();
}
//等待线程完成
3.2 merge
- reduce的merge输入是每个map的输出分片的一个合并过的溢出文件
- map的merge输入的是每个spill操作输出的spill_file
- 对多个map输出的分片文件在进行多路归并过程。
有如下三种merge方式,按顺序分别为 1)内存到磁盘2)内存到内存 3)磁盘到磁盘 默认第二种不开启,三种merge均为归并排序,与map阶段的merge相同,迭代器维护小顶堆,同上
3.3 3.reduce
随着merge不断移动小顶堆输出迭代对象,输入到reduce函数中进行处理。这里写我们的reduce逻辑
细节
- 环形缓冲区的好处:
- 不需要申请新的内存空间,环形指针,循环利用
- 不存在gc问题
2.整个过程都有哪些排序查找算法
- 快排,在spill阶段
- 堆排序和多路归并排序,这两个放在一起是因为总是一起使用,在map中的merge和reduce中的merge,需要将多个小文件进行多路归并,这个过程使用小顶堆来维护最小值,不断移动小顶堆来保证最小值归并:
- 堆排序,构建segment的最小值
- 多路归并排序,在进行合并多个的有序小文件为一个文件的过程,对segment的排序
- 二分查找,将segment加入有序的segmentlist,使用二分查找