前提
Hadoop版本:hadoop-0.20.2
概述
现在已经知道datanode是通过DataXceiver来处理客户端和其它datanode的请求,在分析DataXceiver时已经对除数据块的读与写之外的操作进行了说明,本文主要分析比较复杂而且非常重要的两个操作:读与写。对于用户而言,HDFS用得最多的两个操作就是写和读文件,而且在大部分情况下,是一次写入,多次读取,满足高吞吐量需求而非低延迟,除去客户端与namenode的协商,剩下的部分主要是客户端直接与datanode通信(数据流的头部在上篇文章中已介绍),发送或接收数据,而这些数据在datanode如何接收并写入磁盘、如何从磁盘读出并发送出去就是本文所要介绍的内容。
DataChecksum
无论是读数据还是写数据,都会涉及到checksum,我们先来看看DataChecksum的结构,该类位于org.apache.hadoop.util这个包下,有以下几个主要属性:
- type:checksum的类型,有CHECKSUM_NULL和CHECKSUM_CRC32两种
- size:checksum的大小(字节),CHECKSUM_NULL的大小是0,CHECKSUM_CRC32的大小是4
- summer:真正用来做checksum的对象,CHECKSUM_NULL使用的是ChecksumNull这个自定义的不干实事的类,CHECKSUM_CRC32使用的是java提供的CRC32
- bytesPerChecksum:用来做checksum的数据片的大小(字节),即HDFS会把块文件(block)分成多个分片(chunk),对每个分片做checksum,那么读或写数据的最小单位是分片
- inSum:已经做checksum的总字节数
DataChecksum的header有5个字节,其中type占1个字节,bytesPerChecksum占4个字节。
DataChecksum有如下几类方法:
- newDataChecksum:创建一个新的DataChecksum
- writeHeader:将checksum header写到输出流
- getHeader:将checksum header存到一个byte数组中,并返回
- writeValue:将checksum计算结果写到输出流或缓冲区中
- getValue:返回checksum计算结果
- reset:重置checksum
- update:更新checksum
BlockMetadataHeader
一直在说一个块有数据文件和元数据文件,有了上边对checksum的分析,下面我们来揭开datanode上管理数据块元数据的BlockMetadataHeader的面纱,数据块无数据的最大部分是块的CRC,这部分与namenode与块相关的功能无关。
有两个属性:
- version:元数据版本(2个字节)
- checksum:数据校验和(header占5个字节)
那么元数据文件的header总共有7个字节,元数据文件的结构大概如下所示:
|
|
BlockMetadataHeader提供以下几类方法:
- readHeader:从文件或输入流读取header
- writeHeader:将header写到输出流
- getHeaderSize:得到header的大小,该版本是7
BlockSender
从BlockSender这个名字我们就能够知道它的作用是用于发送块文件,首先,我们来看看其重要的属性:
- block:读取的数据块
- blockIn:本地磁盘的块文件
- blockInPosition:是否使用transferTo()
- checksumIn:本地磁盘的块元数据文件
- checksum:checksum stream
- offset:读取的数据在块中的起始位置
- endOffset:结束位置
- blockLength:块的长度
- bytesPerChecksum:chunk大小
- checksumSize:checksum大小
- corruptChecksumOk:是否需要验证checksum是否损坏
- chunkOffsetOK:是否需要发送chunk的开始位置
- seqno:packet编号
- transferToAllowed:是否允许transferTo
- blockReadFully:如果整个块被读取,设置为true
- verifyChecksum:在读数据时,是否检查checksum
下面,我们来看看其构造方法,其定义如下:
|
|
参数有很多:
- block:要读取的块
- startOffset:读取数据的开始位置
- length:读取数据的长度
- corruptChecksumOk:是否需要验证checksum是否损坏
- chunkOffsetOK:是否需要发送chunk的开始位置
- verifyChecksum:在读数据时,是否检查checksum
- datanode:当前所在的datanode
- clientTraceFmt:client trace log message的格式
初始化的过程如下:
- 读取元数据,加载checksum
- 计算bytesPerChecksum和checksumSize,
- 检查并调整开始位置和结束位置,使开始位置和结束位置与验证块的边界对齐
- 将checksum数据定位到正确的位置
- 将块数据文件输入流定位到正确的位置
首先,我们来看看sendBlock方法,其定义如下:
|
|
其中:
- out:是块数据要写出的流
- baseStream:如果不为null,那么out是该流的包装器,即out封装了baseStream
- throttler:用于控制流量
sendBlock的处理流程是这样的:
- 将数据的header(checksum header,如果需要发送块的开始位置还需要再加一offset)写到out
- 检查是或允许transferTo(verifyChecksum为false,baseStream是SocketOutputStream,blockIn是FileInputStream),这种方式使用FileChannel来传输数据,而不是先将数据读取到缓冲区
- 计算每个packet数据(checksum和数据)的大小
- 将所有packet写到out
- 将一整数(int)0写到out,标记块的结束
到此我们知道发送的块数据如下所示:
|
|
接下来,我们来看看sendChunks的处理流程,sendChunks的功能是发送一个packet,具体发送的chunk数由参数maxChunks指定:
- 计算真实的chunk数numChunks和packet的大小packetLen
- 将packet header(packet大小packetLen、数据在block中的位置offset、packet编号seqno、是否是最后一个packet、真实数据的大小len)写到out
- 读取checksum数据到缓冲区,如果corruptChecksumOk为真,那么在出错时修复数据
- 如果不允许transferTo,读取真实数据到缓冲区,如果verifyChecksum为真,那么检查checksum,最后将checksum和真实数据写到out
- 如果允许transferTo,调用SocketOutputStream的transferToFully方法传输数据
- 做流量控制
由以上分析,我们可知packet的结构如下:
|
|
其中x是根据以下表达式计算出来的:
|
|
BlockReceiver
BlockReceiver主要作用是接收块文件,首先,我们来看看其重要的属性:
- block:接收的块
- in:接收数据的流
- out:本地磁盘的块文件
- checksum:计算checksum
- checksumOut:本地磁盘的元数据文件
- bytesPerChecksum:用来做checksum的数据片的大小
- checksumSize:checksum的大小
- buf:存接收的数据,一个完整的packet
- bufRead:接收的合法的数据的大小
- offsetInBlock:接收的数据在块中的位置
- mirrorAddr:pipeline中下一个datanode的地址
- mirrorOut:用于将数据发送到pipeline中下一个datanode
- responder:用于应答的线程
- isRecovery:是否是恢复操作(覆盖或追加)
- inAddr:数据发送方的地址
- myAddr:本地的地址
下面,我们来看看其构造方法,其定义如下:
|
|
参数也有不少:
- block:接收的块
- in:接收数据的流
- inAddr:数据发送方的地址
- myAddr:本地的地址
- isRecovery:是否是恢复操作,即原来文件已经存在
- clientName:客户端名字
- srcDataNode:发送数据的datanode
- datanode:本datanode
初始化的过程如下:
- 读取checksum信息
- 打开本地的块文件和元数据文件,并检查块是否正确
- 如果是恢复操作,将块从blockScanner中删除
BlockReceiver这个类比较复杂,有一千行左右代码,我们以客户端写文件为例来说明其处理过程,如下图所示:
从上图可以看出数据被分成64KB的packet从客户端沿着pipeline逐一发送到所有的datanode,到达最后一个datanode后,应答信息ACK从最后一个datanode沿着pipeline送回客户端,客户端收到ACK就能够知道数据是否发送成功。对于每个datanode,其职责是接收数据包并将数据包发送到其下游datanode,收到ACK后,对ACK进行加工后发送给上游的datanode或client。如果是拷贝块数据操作,是不需要发送应答包的,过程比上图要简单,只需要把数据从一个datanode发送到另一个datanode。
那么,可以将下面的内容分成接收数据和发送应答包两部分,首先,我们来看看接收数据的入口receiveBlock方法:
|
|
先来分析其参数:
- mirrOut:到下游datanode的输出流,用以发送数据包
- mirrIn:来自下游datanode的输入流,用以接收应答包
- replyOut:到上流datanode的输出流,用以发送应答包
- mirrAddr:下游datanode的地址
- throttlerArg:节流器,用于控制流量
- numTargets:下游datanode的数量,用于确认应答包是否正确
处理的过程如下面的流程图所示:
在这个过程中,需要注意的是setBlockPosition这个方法,如果块文件之前已经finalize了,并且isRecovery为false或者offsetInBlock超过块的大小,那么会抛异常。前边已经讲到每个块文件会被分割成多个chunk,然后对每个chunk做checksum,在这里,如果offsetInBlock不与chunk的边界对齐,那么需要先读出offsetInBlock所位于chunk在offsetInBlock之前数据的checksum,再更新接收到的数据,这样才能确保checksum的正确性。
下面就来看看发送应答包是怎么回事,相关的类有PacketResponder、Packet和PipelineAck,PipelineAck是接口org.apache.hadoop.hdfs.protocol.DataTransferProtocol的内部静态类。先来看看简单的Packet,纯粹就是一个数据结构,有两个属性:
- seqno:packet的编号
- lastPacketInBlock:是否是最后一个包
PipelineAck封装了应答的内容,我们来看看其属性:
- seqno:packet的编号
- replies:一个数组,下游datanode及其自己的答应,数组中每个元素的取值是上一篇文章中操作的状态
- HEART_BEAT:心跳应答对象,seqno为-1,replies中只有一个值OP_STATUS_SUCCESS
一个ACK的内容如下所示:
|
|
如何判断一个ACK是否是成功呢?很简单,只要replies中有值不为OP_STATUS_SUCCESS,那么就不成功。
好了,只剩下一个PacketResponder了,先看其属性:
- ackQueue:等待应答的packet队列
- running:PacketResponder是否在运行
- block:数据块
- mirrorIn:来自下游datanode的输入流,用以接收应答包
- replyOut:到上流datanode的输出流,用以发送应答包
- numTargets:下游datanode的数量,用于确认应答包是否正确
- receiver:PacketResponder的所有者
PacketResponder的处理有两种不同的方式:numTargets=0,说明这是pipeline的最后一个datanode;有下游datanode。
先来看看最后一个datanode是如何处理每个packet的:
- 如果ackQueue中没有元素,先等待一段时间,如果距上次发送心跳的时间间距超过某阈值,发送心跳给上游的datanode,重复以上操作直到ackQueue不为空
- 如果当前packet是最后一个,finalize数据块,并通知datanode接收完数据块
- 发送ACK给上游的datanode
如果不是最后一个datanode又是如何处理的:
- 接收下游datanode的ACK
- 如果是心跳ACK,直接发送给上游datanode,接着处理下个packet
- 如果非心跳ACK,先检查接收到的ACK的packet编号和当前队列中第一个元素的packet编号是否一致
- 如果当前packet是最后一个,finalize数据块,并通知datanode接收完数据块
- 构造ACK消息,replies的第一个元素是自己的状态,值为OP_STATUS_SUCCESS,如果没有收到下游datanode的ACK,其它元素的值为OP_STATUS_ERROR,否则其它元素的值为接收到的原值
- 将ACK消息发送给上游datanode
- 如果ACK有错,中止PacketResponder的运行