}
可以看到不断调用write1方法,保证数据发送的完整性。
那么write1方法又做了什么事情呢,write1将用户需要写入的数据流首先写到自己的BUFFER中,达到一定数量(基本是一个chunk的大小)后进行CheckSum方法调用得到一段数据的校验和,然后通过writeChecksumChunk这个方法将数据以及该部分数据的校验和,按照一定格式要求一并写入Stream。
writeChecksumChunk这个方法的主要作用就是将用户写入的数据以及该部分数据的校验和做为参数调用writeChunk()方法,这个方法是一个虚方法,真正的实现在DFSOutputStream这个类中,这也合情合理,本身FSOutputSummer这个类的作用仅仅是在输出流中增加校验和数据,至于数据是如何进行传输的是通过DFSOutputStream来实现的。
那么接下来需要说明的就是DFSOutputStream的writeChunk这个方法了。
HDFS流式数据网络传输的基本单位有哪些呢?
chunk->package->block
我们上文已经提过:
等用户写入的数据达到一定数量(基本是一个chunk的大小)后就会对这段数据取校验和。
一定数量的chunk就会组成一个package,这个package就是最终进行网络传输的基本单元,datanode收到package后,将这些package组合起来最终得到一个block。
我们接下来通过实际主要的代码了解这部分功能的实现:
currentPacket这个对象初始化的时候就是null,第一次写入数据时这个判断成立
if(currentPacket==null)
{
currentPacket=newPacket(packetSize,chunksPerPacket,
bytesCurBlock);
...
//下面开始构建package包。
//在package包中增加一个chunk,首先添加这个chunk所包含数据的checksum
currentPacket.writeChecksum(checksum,0,cklen);
currentPacket.writeData(b,offset,len);//然后添加这个chunk所包含的数据
currentPacket.numChunks++;//增加这个package所包含的chunk个数
bytesCurBlock+=len;//当前已经写入的byte个数
//Ifpacketisfull,enqueueitfortransmission
//如果这个package已经达到一定的chunk数量,准备实际的传输操作
if(currentPacket.numChunks==currentPacket.maxChunks||bytesCurBlock==blockSize)
{
......
if(bytesCurBlock==blockSize)//如果用户写入的数据,已经达到一个block缺省大小(64M)
{
//设置当前的package是某一个block的最后一个package
currentPacket.lastPacketInBlock=true;
//清除一些变量的值
bytesCurBlock=0;
lastFlushOffset=-1;
}
//这三段代码是关键的一部分代码,将已经构建完成的package写入一个dataQueue队列,由另一个线程(就是我们开始提到的:
启动一个后台线程streamer.start();这个线程的主要目的就是将上述的package队列写入SOCKET中)从该队列中不断取出package,进行实际的网络传输
dataQueue.addLast(currentPacket);//产生event,进而通知并唤醒等待线程
dataQueue.notifyAll();//这一步也很重要,设置currentPacket为空,表示这个package已经满了,需要new一个新的package继续接收用户后面进一步需要写入的数据。
currentPacket=null;
//Ifthiswasthefirstwriteafterreopeningafile,then
//theabovewritefilledupanypartialchunk.Tellthesummertogeneratefull
//crcchunksfromnowon.
if(appendChunk)
{
appendChunk=false;
resetChecksumChunk(bytesPerChecksum);
}
intpsize=Math.min((int)(blockSize-bytesCurBlock),writePacketSize);
computePacketChunkSize(psize,bytesPerChecksum);
}
computePacketChunkSize这个方法的主要作用是计算两个参数:
1.chunksPerPacket
接下来的package需要承载多少个chunk;因为最后一个package承载的chunk个数与文件大小也有关系。
2.packetSize
接下来的package的大小。
以上两个参数与判断是否需要new一个新的PACKAGE很有关系。
privatevoidcomputePacketChunkSize(intpsize,intcsize)
{
intchunkSize=csize+checksum.getChecksumSize();
intn=DataNode.PKT_HEADER_LEN+SIZE_OF_INTEGER;
chunksPerPacket=Math.max((psize-n+chunkSize-1)/chunkSize,1);
packetSize=n+chunkSize*chunksPerPacket;
if(LOG.isDebugEnabled())
}
可以看到构建的package不断添加到dataQueue这个队列,streamer.start()这个线程从中弹出package进行实际网络传输操作。
下面就涉及到比较复杂的网络传输协议部分。
我们先看一下这部分的流程:
1.上面已经讲过,开始的一步就是客户端调用create方法,在namenode上的目录树中注册一个INodeFileUnderConstruction节点,并得到一个DfSOutputStream。
2.用户得到这个outputStream后就可以进行写入操作,用户写入的数据就不断构建成package写入dataQueue这个队列。
3.streamer.start()这个线程从dataQueue队列中取出package进行实际网络传输操作。
下面的网络传输流程为关键流程:
4.streamer是一个DataStreamer的实例,这是一个线程实例。
大家知道HDFS中的文件数据会分成很多64M大小的block,所以在HDFS中保存文件数据第一步就是在namenode上申请一个特殊的blockID(当然还是通过RPC调用的方式)。
右图为文件写入流程
四、HDFS文件数据流写入传输协议
FileSystem是一个提供给用户对文件系统进行访问的抽象类,访问HDFS的具体实现类为FileSystem,用户对HDFS的所有操作就是通过这个类的具体实例完成的。
在HDFS写入文件之前需要首先在Namenode创建并注册一个INodeFileUnderConstruction,在向文件进行数据追加时,会针对这一文件逐个向系统中追加BLOCK,如果是第一次写入(或者上一个BLOCK已经写满),需要在Namenode中注册一个新的BLOCK,通过RPC调用Namenode的具体实例返回LocatedBlock对象,除了新申请的block这个LocatedBlock对象还包含多个datanodeinfo信息,表示这个block块儿需要传输的哪些datanode上进行保存。
下面我们详细看一下数据传输的具体流程以及传输协议的详细内容:
第一步将返回的多个datanode逐个建立链接形成数据传输链,如下图所示(以三份数据拷贝为例):
当流程到达第6步的时候这个管道才算建立成功,每一步都是与下一个节点建立链接,发送管道创建协议包。
管道建立协议:
4字节数据传输协议版本号(0.19.1版本的hadoop这个字段为14)
4字节操作码(追加文件时为:
OP_WRITE_BLOCK)
8字节Block的BLOCKID字段
8字节Block的时间戳字段
4字节建立的传输链中一共有多少台datanode
Boolean表示是否为恢复数据请求(recoverblock操作)
TextClient名称字串,格式为字串长度(这个采取了压缩形式)+字串内容
Boolean表示是否传输客户端信息
DatanodeInfo如果上面字段为true,才会有该字段,表示client信息
Int表示数据链中后续节点个数(每过一个节点,这个字段就减少一)
DatanodeInfo[]表示数据链中后续节点信息,上面字节是多少就读多少个
DatanodeInfo信息,传给后面的节点做下一节点的链接
ChecksumbyteChecksum的类型,可以根据该字段实例化具体checksum类
Int对多少字节进行取checksum操作
Client发出上面的请求后就等待应答,应答的协议格式如下:
TextClient名称字串,格式为字串长度(这个采取了压缩形式)+字串内容
返回就是一个字串信息,如果字串为空表示后续节点所有链路都链接成功,如果不为空该字段保存的就是出错的datanode信息,格式如下:
name:
port。
针对于一个block传输操作创建数据链路成功以后,就可以进行实际数据传输了。
后面的数据就是一个接一个的Package包。
4字节Package数据长度
4字节package在这个block中的偏移量
8字节一个序列号
Byte这个package是否是这个block中最后一个包
4字节实际的数据长度,除去checksum。
CheckSum数据缺省的CheckSum为CRC32,(缺省每个chunk的checksum占四个字节)
实际block数据有多少写多少
数据传输过程中一定需要耗时,如何知道数据传输链中的datanode都是正常工作的呢?
以及datanode成功收到package包以后是如何应答吗?
这部分协议相关字段如下:
如果收到下一个datanode返回4字节(-1),表示心跳正常
4字节opCode(-1,表示心跳正常)
如果收到下一个datanode返回4字节(不等于-1,也不等于-2)
4字节packageID(向上一级返回接受到的packageID)
4字节OP_STATUS_SUCCESS=0;OP_STATUS_ERROR=1;
0表示接受成功,1表示接受失败。
这个字节根据数据链中的位置可能会收到多个,详细见下图:
client最终可以根据返回的PackageID后的多个状态字节得知哪台机器可能出现问题,从上面的流程可以看到:
datanode3可能已经写入成功,datanode2由于没有收到datanode3的应答,故而认为datanode3接受package失败,这个状态一直透传到client。
也有可能datanode3节点的确失效,datanode2也会收不到datanode3的应答响应。
DFSClient从Namenode取得需要读取的文件对应的LocatedBlocks信息以后,就会按照block的顺序与datanode建立链接并发送读取block数据的请求。
我们看一下这部分的协议格式:
4字节
数据传输协议版本号(0.19.1版本的hadoop这个字段为14)
4字节
操作码(读取文件时为:
OP_READ_BLOCK,对应81)
8字节
Block的BLOCKID字段
8字节
Block的时间戳字段
8字节
读取在block数据文件开始的偏移量
8字节
一共读取多少字节
Text
Client名称字串,格式为字串长度(这个采取了压缩形式)+字串内容
DFSClient发送读取数据块儿请求完成以后,首先等待datanode的应答,datanode应答协议格式如下:
4字节
OP_STATUS_SUCCESS=0表示链接建立成功OP_STATUS_ERROR=1表示链接建立失败
接下来Datanode就开始进行数据传送,具体数据格式如下:
数据校验相关信息ByteChunksum类型
IntbytesPerChecksum每次checksum对应的字节数
8字节Offset(读取一个block开始的偏移量)
(这8字节有些情况没有)1.DFSClient从Datanode读取block数据时,这个字段是必须有的。
2.Datanode之间进行block互相拷贝时(balance需要,或者block
块儿没有达到副本个数要求),这个字段是不存在的。
4字节packetLen,package长度
8字节chunk的偏移量(用戶读取文件时,很可能偏移一个位置,偏移的位置很
可能不在一个chunk的结束位置,因为checksum是按照chunk来计算
的,所以hadoop会将这部分偏移量的数据多传送给client,client需
要将这部分数据丢弃)
8字节Seqno序列号
Byte是否是block数据块的最后一个package
4字节传输的block中包含的数据大小
CheckSum数据缺省的CheckSum为CRC32,(缺省每个chunk的checksum占四个字节)
实际block数据有多少写多少
五、HDFS的Java访问接口
得到filesystem的实例,有两个静态方法可以得到filesystem接口的实例
publicstaticFileSystemget(Configurationconf)throwsIOException
publicstaticFileSystemget(URIuri,Configurationco