Hadoop I/O管道剖析

本文翻译自:http://developer.yahoo.com/blogs/hadoop/posts/2009/08/the_anatomy_of_hadoop_io_pipel/

介绍 

        在一个典型的Hadoop MapReduce任务中,从HDFS中读入输入文件。数据一般已经压缩过以便减少文件大小。在解压缩后,序列化后的字节流会被转换成Java对象,然后传入用户定义好的map函数。相反的,输出记录会经过序列化、压缩,最后被存到HDFS中。这些看起来很简单,基于种种原因这个双向的过程实际上会复杂的多:

a 压缩和解压缩一般由本地的库代码来处理

b 端对端的CRC32校验码在读取和写入的时候都会进行校验或者计算

c 缓冲控制由于种种接口限制比较复杂

在这篇博客中,我尝试着详细的剖析Hadoop I/O管道机制,并且提出一些可能的优化。为了讨论的更具体些,我用了一些从gzip压缩的文本记录中读取/写入行记录的常用例子。我不会深入讨论DataNode端的管道,取而代之主要讨论用户端(map/reduce任务流程)。最后,所有这些文字都是基于Hadoop 0.21主干版本,这意味着你需要根据Hadoop的新旧版本来看这篇文章。


读取输入

 

图1说明了I/O管道如何用TextInputFormat从gzip压缩的文本记录中读取行记录。这个图用分割线切开。左半部分是DataNode流程,有半部分是应用流程(也就是Map任务)。从下至上的看,这里根据操作/控制缓冲的位置分成了三个部分:内核空间、本地代码空间、和JVM空间。对于应用流程,从左至右的看,有一个数据块需要便利的软件层。不同颜色的盒子代表了不同种类的缓冲。盒子之间的箭头代表了数据转移/拷贝的方向。箭头的粗细代表了数据转移量的大小。每个盒子中的表现说明了缓冲的大体位置(不是引用缓冲的变量就是分配缓冲的模块)。如果可用,缓冲的大小用方括号括起来。如果缓冲大小可配,那么显示可配属性和默认大小。我把每个需要数据转移的地方都用数字标记起来:

       

1.      从DataNode到MapTask的数据转移。DBLK是文件数据块,CBLK是文件校验块。文件数据通过Java Nio(即UNIX传送文件系统调用)转移到客户端。首先把校验数据拿到DataNode的JVM缓冲中,然后推送到客户端(为展示细节)。文件数据和校验数据都被捆绑在HDFS的包中(一般64KB),格式:{包头:校验字节:数据字节}

2.      从Socket接收来的数据被缓存在BufferedInputStream,应该是为了减少系统内核调用的。这实际上设计两个缓冲拷贝过程:1. 数据从内核缓冲拷贝到一个JDK代码的临时直接缓冲里2. 数据从临时直接缓冲力拷贝到由BufferedInputStream控制的字节缓冲byte[] buffer里。在BufferedInputStream内的byte[]有可配属性”io.file.buffer.size”来控制,默认大小4K。在我们的产品中,这个参数被配置为128K

3.      通过BufferedInputStream,校验字节保存在内部的ByteBuffer中(大小一般(包大小/512*4)或者512字节),并且文件字节(压缩后数据)被放置在由解压缩层提供的byte[] buffer内。由于校验计算要求一个512字节块,而用户的请求可能和块边际不对其,在拷贝部分数据块到用户定义的byte[] buffer前,一个512字节的byte[] buffer被用来对齐输入。同时注意数据以512字节的块来拷贝(由FSInputChecker API要求)。最后,所有的检验字节都被拷贝到4字节向量中以便校验数据。总的来说,这个步骤设计额外的缓冲拷贝。

4.      解压缩层使用了一个byte[] buffer来接收从DFSClient层发来的数据。解压缩流从byte[] buffer内拷贝到一个大小为64K的直接缓冲中,调用了本地库代码来解压缩数据,并且使用另一个大小64K的直接缓冲来存储解压缩后的数据。这个步骤设计两个缓冲拷贝。

 

优化输入管道

 

综合以上来说,为了解压缩进行的拷贝,当整个读取管道设计为了给一个MapTask的map()函数发送一个记录,因为数据在进程中的内核缓冲中读取出来,而涉及到了七个缓冲拷贝。在上述步骤中有很多地方可以改进:

a. 许多缓冲拷贝仅仅是因为在直接缓冲directbuffer和字节缓冲byte[] buffer中转换

b. 校验计算可以在分散的执行而不是每个块一个

图2把缓冲拷贝从7个减少到了3个,这是优化后的视图:

1.      一个输入包分解成校验部分和数据部分,分在两个直接缓冲中:一个内部校验字节,一个由解压缩层控制的直接缓冲,控制压缩数据。FSInputChecker直接操作两个缓冲。

2.      解压缩层减少了从未压缩字节发给由LineReader控制的直接缓冲字节大大小

3.      LineReader扫描直接缓冲,找到行分隔符,构建文本对象

 

写输出

 

现在咱们转向写的一段。图三说明了ReduceTask使用TextOutputFormat向gzip文件写压缩后行记录的I/O管道。和图1相近,每次数据转移都贴上了标签:

1.      TextOutputFormat的RecordWriter没有使用缓冲。当一个用户发出一个行记录,文本对象的字节直接拷贝到有压缩层控制的一个64KB直接缓冲中。很长一段时间内,会多次想这个缓冲区拷贝,每次拷贝64KB。

2.      每次压缩层接收一行(或者一个长的行),都会调用本地压缩代码,压缩字节然后存到另一个64KB的直接缓冲中。在被推送到DFSClient层前(因为DFSClient层仅仅接收字节缓冲作为输入),数据从直接缓冲中拷贝到由压缩层控制的一个内部字节缓冲。这个大小还是由可配属性”io.file.buffer.size”来控制,这一步涉及两个缓冲拷贝。

3.      FSOutputSummer从压缩层的直接缓冲进行CRC32校验计算,并且把数据字节和校验字节存到一个包对象内。另外,校验计算必须在一个512字节的块内完成,并且一个内置的512的字节缓冲用来存部分块,这部分块可能是压缩块和块边际未对齐的部分。在拷贝到包内前,首先计算校验码然后存在一个4字节的字节缓冲内。这一个不设计一个缓冲拷贝。

4.      当一个包满的时候,这个包被推送到一个长度为80的队列中,这个包大小由可配属性”dfs.write.packet.size”来配置,默认大小64KB。这一个不不涉及缓冲拷贝

5.      一个DataStreamer线程等待着队列,只要收到一个包,就把包发送给socket。Socket被封装在BufferedOutputStream中。但是字节缓冲非常小(不大于512字节),并且经常被绕过。然而数据经常需要被拷贝到由JDK代码控制的临时直接缓冲中,这一步设计两个数据拷贝。

6.      数据从ReduceTask内核缓冲中发来,发送到DataNode内核缓冲中。在进行数据以块文件和校验文件存储之前,在DataNode端有些缓冲拷贝过程,不想有些DFS读的情景,文件数据和校验数据都不会经过内核,再进入JVM部分。这一部分属于讨论之外,并没有在图中展示。

 

优化输出管道

 

总的来说,包含了拷贝和压缩的字节,这个流程为了给ReduceTask的内核缓冲有一个行记录,需要6个缓冲拷贝。那我们如何优化写管道呢?

a 我们可以减少一些缓冲拷贝

b 本地压缩袋吗可以调用少一些,如果我们尽在输入缓冲满的时候调用它(像LZO这样的块压缩代码就可以)

c 校验计算可以大块计算而不是一次一块

从图4可以看出在几个优化之后如何,仅仅有四个必要的缓冲拷贝:

1.      用户文本对象的字节流拷贝到由TextOutputFormat层的直接缓冲中

2.      当缓冲满的时候,调用本地压缩代码,压缩数据存到有压缩层控制的直接缓冲中

3.      FSOutputSummer计算在压缩层中直接缓冲内的校验字节,并且存储数据字节和校验字节到包的直接缓冲中。

4.      一个包满后发送到一个队列中,在后台,DataStreamer线程把包以socket方式发出去,它拷贝字节后发送到内核缓冲中。

 

总结

 

这篇博客经过一下午具体针对Hadoop I/O的讨论,并在代码中验证。结果证明,在类与类中结合后,管道明显比我们想的复杂。当我们对每个部件都熟悉的时候,我们发现,做了阐述Hadoop I/O的图,我们希望其他的开发者或者用户能知道这些。优化工作将会很麻烦,这也是我们优化Hadoop的第一步


微博请加:杜龙_hmily

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值