大数据:Map终结和Spill文件合并
当Mapper没有数据输入,mapper.run中的while循环会调用context.nextKeyValue就返回false,于是便返回到runNewMapper中,在这里程序会关闭输入通道和输出通道,这里关闭输出通道并没有关闭collector,必须要先flush一下。
获取更多大数据视频资料请加QQ群:947967114 代码结构:
Maptask.runNewMapper->NewOutputCollector.close->MapOutputBuffer.flush
我们看flush帮我们做了什么事情,为什么要flush。
public void flush() throws IOException, ClassNotFoundException,
InterruptedException {
LOG.info("Starting flush of map output");
spillLock.lock();
try {
while (spillInProgress) {
reporter.progress();
spillDone.await();
//这里查看spillInProgress状态,如果有spill就等待完成,并且报告状态。
}
checkSpillException();
final int kvbend = 4 * kvend;
//kvend是元数据块的终点,元数据是向下伸展的。
//kvend是以整数计的数组下标,kvbend是以字节计的数组下标
if ((kvbend + METASIZE) % kvbuffer.length !=
equator - (equator % METASIZE)) {
//这个条件说明缓冲区中原来有数据,现在spill已经完成,需要释放空间。 获取更多大数据视频资料请加QQ群:947967114
// spill finished
//spill一次需要调整一些参数,以释放空间,这个工作通过resetSpill完成
resetSpill();
private void resetSpill() {
final int e = equator;
bufstart = bufend = e;
final int aligned = e - (e % METASIZE);
// set start/end to point to first meta record
// Cast one of the operands to long to avoid integer overflow
kvstart = kvend = (int)
(((long)aligned - METASIZE + kvbuffer.length) % kvbuffer.length) / 4;
LOG.info("(RESET) equator " + e + " kv " + kvstart + "(" +
(kvstart * 4) + ")" + " kvi " + kvindex + "(" + (kvindex * 4) + ")");
}
//这里其实就是在调整各个参数的位置。比如原点位,kvstart等。
}
if (kvindex != kvend) {
//再来判断缓冲区是否为空,如果不空表示不满足spill条件(80%),但map处理完成没有数据输入。
kvend = (kvindex + NMETA) % kvmeta.capacity();
bufend = bufmark;
LOG.info("Spilling map output");
LOG.info("bufstart = " + bufstart + "; bufend = " + bufmark +
"; bufvoid = " + bufvoid);
LOG.info("kvstart = " + kvstart + "(" + (kvstart * 4) +
"); kvend = " + kvend + "(" + (kvend * 4) +
"); length = " + (distanceTo(kvend, kvstart,
kvmeta.capacity()) + 1) + "/" + maxRec);
sortAndSpill();
//调用一次sortAndSpill过程。 获取更多大数据视频资料请加QQ群:947967114
}
} catch (InterruptedException e) {
throw new IOException("Interrupted while waiting for the writer", e);
} finally {
spillLock.unlock();
}
//至此所有数据都已经溢写出去,缓冲区已空,所有数据都spill到文件中
assert !spillLock.isHeldByCurrentThread();
// shut down spill thread and wait for it to exit. Since the preceding
// ensures that it is finished with its work (and sortAndSpill did not
// throw), we elect to use an interrupt instead of setting a flag.
// Spilling simultaneously from this thread while the spill thread
// finishes its work might be both a useful way to extend this and also
// sufficient motivation for the latter approach.
try {
spillThread.interrupt();
//让spill线程不在运行
spillThread.join();
//结束spill线程
} catch (InterruptedException e) {
throw new IOException("Spill failed", e);
}
// release sort buffer before the merge
kvbuffer = null;
mergeParts();
//合并spill文件
Path outputPath = mapOutputFile.getOutputFile();
fileOutputByteCounter.increment(rfs.getFileStatus(outputPath).getLen());
}
flush的目的,首先让缓冲区的所有KV对数据都进入spill文件,因为每次spill都会产生一个spill文件,所有spill文件可能不止一个,所以要把spill文件合并到单个文件中,分发给reduce。
所以如果有spill正在进行必须等待其完成,也可能没有spill但是缓冲区非空,需要再一次sortAndSpill,总之要把缓冲区清空为止。所有数据都spill完成后就可以进行mergeParts了
代码结构:
Maptask.runNewMapper--->NewOutputCollector.close--->MapOutputBuffer.flush--->MapOutputBuffer.mergeParts
源代码如下:
private void mergeParts() throws IOException, InterruptedException, ClassNotFoundException {
// get the approximate size of the final output/index files
long finalOutFileSize = 0;
long finalIndexFileSize = 0;
final Path[] filename = new Path[numSpills];
//每次溢写都会有一个文件,所以数组的大小是numSpills。 获取更多大数据视频资料请加QQ群:947967114
final TaskAttemptID mapId = getTaskID();
for(int i = 0; i < numSpills; i++) {
//统计所有这些文件合并之后的大小
filename[i] = mapOutputFile.getSpillFile(i);
//通过spill文件的编号获取到指定的spill文件路径
finalOutFileSize += rfs.getFileStatus(filename[i]).getLen();//获取文件大小
}
if (numSpills == 1) {
//合并输出有俩文件一个是output/file.out,一个是output/file.out.index
sameVolRename(filename[0],
mapOutputFile.getOutputFileForWriteInVolume(filename[0]));
//换个文件名,在原文件名上加个file.out
if (indexCacheList.size() == 0) {
//索引块缓存indexCacheList已空
sameVolRename(mapOutputFile.getSpillIndexFile(0), mapOutputFile.getOutputIndexFileForWriteInVolume(filename[0]));//spillIndexFile改名。
} else {
//索引块缓存indexCacheList中还有索引记录,要写到索引文件
indexCacheList.get(0).writeToFile(
//写入文件
mapOutputFile.getOutputIndexFileForWriteInVolume(filename[0]), job);
}
sortPhase.complete();
return;
//如果只有一个spill合并已经完成。 获取更多大数据视频资料请加QQ群:947967114
}
// read in paged indices
for (int i = indexCacheList.size(); i < numSpills; ++i) {
//如果spill文件不止一个,需要合并
Path indexFileName = mapOutputFile.getSpillIndexFile(i);
indexCacheList.add(new SpillRecord(indexFileName, job));
//先把所有的SpillIndexFile收集在一起。
}
//make correction in the length to include the sequence file header
//lengths for each partition
finalOutFileSize += partitions * APPROX_HEADER_LENGTH;
//每个partition都有header
finalIndexFileSize = partitions * MAP_OUTPUT_INDEX_RECORD_LENGTH;
//IndexFile,每个partition一个记录。
Path finalOutputFile =
mapOutputFile.getOutputFileForWrite(finalOutFileSize);
Path finalIndexFile =
mapOutputFile.getOutputIndexFileForWrite(finalIndexFileSize);
//The output stream for the final single output file
FSDataOutputStream finalOut = rfs.create(finalOutputFile, true, 4096);
//创建合并,最终输出。
if (numSpills == 0) {
//要是没有SipillFile生成,也创建一个空文件
//create dummy files
IndexRecord rec = new IndexRecord();
//创建索引记录
SpillRecord sr = new SpillRecord(partitions);
//创建spill记录
try {
for (int i = 0; i < partitions; i++) {
long segmentStart = finalOut.getPos();
FSDataOutputStream finalPartitionOut = CryptoUtils.wrapIfNecessary(job, finalOut);
Writer<K, V> writer =
new Writer<K, V>(job, finalPartitionOut, keyClass, valClass, codec, null);
writer.close();
//创建后马上关闭,形成空文件。
rec.startOffset = segmentStart;
rec.rawLength = writer.getRawLength() + CryptoUtils.cryptoPadding(job);
rec.partLength = writer.getCompressedLength() + CryptoUtils.cryptoPadding(job);
sr.putIndex(rec, i);
}
sr.writeToFile(finalIndexFile, job);
//所以记录写入索引文件
} finally {
finalOut.close();
}
sortPhase.complete();
return;
}
{
sortPhase.addPhases(partitions); // Divide sort phase into sub-phases
IndexRecord rec = new IndexRecord();
final SpillRecord spillRec = new SpillRecord(partitions);
for (int parts = 0; parts < partitions; parts++) {
//finalOut最终输出文件。循环分区获得所有spill文件的该分区数据,合并写入finalOut
//create the segments to be merged
List<Segment<K,V>> segmentList =
new ArrayList<Segment<K, V>>(numSpills);
//创建Segment,数据段
for(int i = 0; i < numSpills; i++) {
//准备合并所有的Spill文件
IndexRecord indexRecord = indexCacheList.get(i).getIndex(parts);
Segment<K,V> s =
new Segment<K,V>(job, rfs, filename[i], indexRecord.startOffset,
indexRecord.partLength, codec, true);
segmentList.add(i, s);
//把每个Spill文件中相同partition的区段位置收集起来。 获取更多大数据视频资料请加QQ群:947967114
if (LOG.isDebugEnabled()) {
LOG.debug("MapId=" + mapId + " Reducer=" + parts +
"Spill =" + i + "(" + indexRecord.startOffset + "," +
indexRecord.rawLength + ", " + indexRecord.partLength + ")");
}
}
int mergeFactor = job.getInt(JobContext.IO_SORT_FACTOR, 100);
//做merge操作时同时操作的stream数上限
boolean sortSegments = segmentList.size() > mergeFactor;
//对segment进行排序
@SuppressWarnings("unchecked")
RawKeyValueIterator kvIter = Merger.merge(job, rfs,
keyClass, valClass, codec,
segmentList, mergeFactor,
new Path(mapId.toString()),
job.getOutputKeyComparator(), reporter, sortSegments,
null, spilledRecordsCounter, sortPhase.phase(),
TaskType.MAP);
//合并同一partition在所有spill文件中的内容,可能还需要sort,合并后的结构是一个序列。
//write merged output to disk
long segmentStart = finalOut.getPos();
FSDataOutputStream finalPartitionOut = CryptoUtils.wrapIfNecessary(job, finalOut);
Writer<K, V> writer =
new Writer<K, V>(job, finalPartitionOut, keyClass, valClass, codec,
spilledRecordsCounter);
if (combinerRunner == null || numSpills < minSpillsForCombine) { // minSpillsForCombine在MapOutputBuffer构造函数内被初始化,numSpills 为mapTask已经溢写到磁盘spill文件数量
Merger.writeFile(kvIter, writer, reporter, job);
//将合并后的结果直接写入文件。下面看一下writeFile的源代码;
public static <K extends Object, V extends Object>
void writeFile(RawKeyValueIterator records, Writer<K, V> writer,
Progressable progressable, Configuration conf)
throws IOException {
long progressBar = conf.getLong(JobContext.RECORDS_BEFORE_PROGRESS,
10000);
long recordCtr = 0;
while(records.next()) {
writer.append(records.getKey(), records.getValue());
//追加的方式输出到writer中
if (((recordCtr++) % progressBar) == 0) {
progressable.progress();
}
}
回到主代码:
} else {
//有combiner
combineCollector.setWriter(writer);
//就插入combiner环节
combinerRunner.combine(kvIter, combineCollector);
//将合并的结果经过combiner后写入文件
}
//close
writer.close();//关闭writer通道
sortPhase.startNextPhase();
// record offsets
rec.startOffset = segmentStart;
//从当前段的起点开始
rec.rawLength = writer.getRawLength() + CryptoUtils.cryptoPadding(job);
rec.partLength = writer.getCompressedLength() + CryptoUtils.cryptoPadding(job);
spillRec.putIndex(rec, parts);
}
spillRec.writeToFile(finalIndexFile, job);
//把spillFile写入合并的indexFle
finalOut.close();
//关闭最终输出流
for(int i = 0; i < numSpills; i++) {
rfs.delete(filename[i],true);
//删除所有spill文件
}
}
}
该方法会将所有临时文件合并成一个大文件保存到output/file.out中,同时生成相应的索引文件output/file.out.index。 在进行文件合并的过程中,Map Task以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式:每轮合并io.sort.factor,默认是100,个文件,并将产生的文 件重新加入待合并列表中,对文件排序后,重复上述过程,直到只有一个文件。只生产一个文件可以避免同时打开大量的文件和同时读取大量的小文件产生的随机读 取带来的开销。最后会删除所有的spill文件。
另外需要注意的是,mergeParts()中也有combiner的操作,但是需要满足一定的条件:1、用户设置了combiner;2、spill文件的数量超过了minSpillsForCombine的值,对应配置项"min.num.spills.for.combine",可自行设置,默认是3。这俩必须同时具备才会在此启动combiner的本地聚集操作。所以在Map阶段有可能combiner会执行两次,所以有可能你的combiner执行两次之后输出数据不符合预期了。
这样Map阶段的任务就算完成了。主要是读取数据然后写入内存缓冲区,缓存区满足条件就会快排后并设置partition后,spill到本地文件和索引文件;如果有combiner,spill之前也会做一次聚集操作,待数据跑完会通过归并合并所有spill文件和索引文件,如果有combiner,合并之前在满足条件后会做一次综合的聚集操作。map阶段的结果都会存储在本地中(如果有reducer的话),非HDFS。
Mapper完成对所有输入文件的处理,并将缓冲区的数据写出到spill文件之后,spill文件的存在只有三种可能:没有spill,一个spill,多个spill。针对这三种都需要一个最终的输出文件,不管内容有没有,内容多少。这个最终文件是和单个spill文件是一样的,按照partition分成若干段,然后是排好序的KV数据,这个merge操作结合之前的spill文件进行sort。就构成了一次mergeSort,这个mergeSort只针对同一个Mapper的多个spill文件,以后在Reducer那里还会有Merge针对不同的Mapper文件。
当Maptask完成后,从runNewMapper返回,下一个操作就是done。也就是MapTask的收尾工作。MapTask的收尾涉及到怎么把生成的数据输出交给ReduceTask。MapTask和ReduceTask都是扩展自Task。但是他们都没有自己定义done函数,所以他们都调用了Task的done。
程序在这里跳出runNewMapper 获取更多大数据视频资料请加QQ群:947967114
if (useNewApi) {
runNewMapper(job, splitMetaInfo, umbilical, reporter);
} else {
runOldMapper(job, splitMetaInfo, umbilical, reporter);
}
done(umbilical, reporter);
这个done我们点进去后发现是Task.done,源码如下;
public void done(TaskUmbilicalProtocol umbilical,
TaskReporter reporter
) throws IOException, InterruptedException {
LOG.info("Task:" + taskId + " is done."
+ " And is in the process of committing");
updateCounters();
//更新容器
boolean commitRequired = isCommitRequired();
if (commitRequired) {
int retries = MAX_RETRIES;
setState(TaskStatus.State.COMMIT_PENDING);
// say the task tracker that task is commit pending
while (true) {
try {
umbilical.commitPending(taskId, taskStatus);
break;
//如果commitPending没有发生异常,就退出,否则重试。
} catch (InterruptedException ie) {
// ignore
} catch (IOException ie) {
LOG.warn("Failure sending commit pending: " +
StringUtils.stringifyException(ie));
if (--retries == 0) {
System.exit(67);
}
}
}
//wait for commit approval and commit
commit(umbilical, reporter, committer);
}
taskDone.set(true);
reporter.stopCommunicationThread();
// Make sure we send at least one set of counter increments. It's
// ok to call updateCounters() in this thread after comm thread stopped.
updateCounters();
sendLastUpdate(umbilical);
//signal the tasktracker that we are done
sendDone(umbilical);
实现sendDone的源代码:
private void sendDone(TaskUmbilicalProtocol umbilical) throws IOException {
int retries = MAX_RETRIES;
while (true) {
try {
umbilical.done(getTaskID());
//实际上这里向MRAppMaster上的TaskAttemptImpl发送TA_DONE事件
LOG.info("Task '" + taskId + "' done.");
return;
} catch (IOException ie) {
LOG.warn("Failure signalling completion: " +
StringUtils.stringifyException(ie));
if (--retries == 0) {
throw ie;
}
}
}
}
umbilical.done(getTaskID()); 获取更多大数据视频资料请加QQ群:947967114
//实际上这里向MRAppMaster上的TaskAttemptImpl发送TA_DONE事件,在TA_DONE事件的驱动下,相应的TaskAttemptImpl对象的状态机执行CleanupContainerTransition.transition,然后转入SUCCESS_CONTAINER_CLEANUP状态。注意这里有一个TaskAttemptEventType.TA_DONE事件是由具体的MapTask所在节点上发出的,但不是引起的状态机的跳变是在MRAppMaster节点上。对于Maptask,会有一个umbilical,就代表着MRAppMaster。
MPAppmaster接到CONTAINER_REMOTE_CLEANUP事件,ContainerLauncher通过RPC机制调用Maptask所在节点的ContainerManagerImpl.stopContainers.使这个MapTask的容器进入KILLED_BY_APPMASTER状态从而不在活跃。操作成功后向相应的TaskAttemptImpl发送TO_CONTAINER_CLEANED事件。如果一次TaskAttempt成功了,就意味着尝试的任务也成功了,所以TaskAttempt的状态关系到TaskImpl对象,taskImpl的扫描和善后,包括向上层的JobImpl对象发送TaskState.SUCCESSED事件。向自身TaskImpl发送的SUCCESSED事件会导致TaskImpl.handleTaskAttemptCompletion操作。
Mapper节点上产生一个过程setMapOutputServerAdress函数,把本节点的MapOutputServer地址设置成一个Web地址,意味着MapTask留下的数据输出(合并后的spill文件)可以通过HTTP连接获取。至此Mapper的所有过程完成。 获取更多大数据视频资料请加QQ群:947967114
大数据:Map终结和Spill文件合并的更多相关文章
- 入门大数据---Map/Reduce,Yarn是什么?
简单概括:Map/Reduce是分布式离线处理的一个框架. Yarn是Map/Reduce中的一个资源管理器. 一.图形说明下Map/Reduce结构: 官方示意图: 另外还可以参考这个: 流程介绍: ...
- [大数据技术]Kettle从CSV文件读取清洗后到MySQL中文乱码问题
首先要知道CSV文件的编码格式 然后在文件输入编码选择编码格式, 第二步,在每个转换或者作业的DB连接中选择选项,并添加如下内容: 中文乱码问题得到解决
- hive小文件合并设置参数
Hive的后端存储是HDFS,它对大文件的处理是非常高效的,如果合理配置文件系统的块大小,NameNode可以支持很大的数据量.但是在数据仓库中,越是上层的表其汇总程度就越高,数据量也就越小.而且这些 ...
- 大数据:Hive常用参数调优
1.limit限制调整 一般情况下,Limit语句还是需要执行整个查询语句,然后再返回部分结果. 有一个配置属性可以开启,避免这种情况---对数据源进行抽样 hive.limit.optimize.e ...
- Hive merge(小文件合并)
当Hive的输入由非常多个小文件组成时.假设不涉及文件合并的话.那么每一个小文件都会启动一个map task. 假设文件过小.以至于map任务启动和初始化的时间大于逻辑处理的时间,会造成资源浪费.甚至 ...
- 大数据技术之HBase原理与实战归纳分享-中
@ 目录 底层原理 Master架构 RegionServer架构 Region/Store/StoreFile/Hfile之间的关系 写流程 写缓存刷写 读流程 文件合并 分区 JAVA API编程 ...
- hive1.2.1实战操作电影大数据!
我采用的是网上的电影大数据,共有3个文件,movies.dat.user.dat.ratings.dat.分别有3000/6000和1百万数据,正好做实验. 下面先介绍数据结构: RATINGS FI ...
- hive优化之小文件合并
文件数目过多,会给HDFS带来压力,并且会影响处理效率,可以通过合并Map和Reduce的结果文件来消除这样的影响: set hive.merge.mapfiles = true ##在 map on ...
- 大数据:Hive - ORC 文件存储格式
一.ORC File文件结构 ORC的全称是(Optimized Row Columnar),ORC文件格式是一种Hadoop生态圈中的列式存储格式,它的产生早在2013年初,最初产生自Apache ...
随机推荐
- c#之委托事件(DelegateEvent)
前面一章学习了委托以及多播委托,接下来我们来学习下委托事件. 在学习委托事件的前提下,得知道什么是观察者模式. 首先,我们来模拟一个场景:例如,当一只狗汪汪汪叫的时候,baby被吓哭了,刚好要偷东西的 ...
- homebrew命令
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
- 纯绿色集成环境,可切换180个Mysql、700个PHP版本
测试神器又出新版!功能更强大(目测linux版本也快要出了,拭目以待吧) PHPWAMP8.8.8.8集成环境,目测大概更新如下内容(我也就是大略看了下更新内容) 1.支持自定义设置任意Mysql版本 ...
- 021.2 IO流——字节输出流
内容:流的分类,文件写入(字节输出流),异常处理,获取一个文件夹下的特定文件集合 字节流的抽象基类:InputStream,OutputStream字符流的抽象基类:Reader,Writer由这四个 ...
- 【4】【MOOC】Python游戏开发入门-北京理工大学【第三部分-游戏开发之机制(色彩与绘图)】
学习地址链接:http://www.icourse163.org/course/0809BIT021E-1001873001?utm_campaign=share&utm_medium=and ...
- 死磕salt系列-salt grains pillar 配置
grains 和 pillar 对比: Grains:存放静态数据,主要存储客户端的主机信息,重启grains会刷新. Pillar: 处理敏感数据, 处理差异性的文件. Grains数据系统 sal ...
- Hadoop学习之路(十八)MapReduce框架Combiner分区
对combiner的理解 combiner其实属于优化方案,由于带宽限制,应该尽量map和reduce之间的数据传输数量.它在Map端把同一个key的键值对合并在一起并计算,计算规则与reduce一致 ...
- 码农视角 - Angular 框架起步
开发环境 1.npm 安装最新的Nodejs,便包含此工具.类似Nuget一样的东西,不过与Nuget不同的是,这玩意完全是命令行的.然后用npm来安装开发环境,也就是下边的angular cli. ...
- Css绘制箭头
IE6不支持transparent,因此上面的代码在IE6加一点处理透明的hack,修改后的代码如下 IE6下处理transparent border-left:100px solid trans ...
- [LuoguP3668][USACO17OPEN]现代艺术2
[LuoguP3668][USACO17OPEN]Modern Art2(Link) 现在你有一块长为\(N\)的画布,每次可以选择一段连续的区间进行颜色填涂,新颜色会覆盖旧颜色.每一次填涂都要耗费一 ...