1.主要概念
1.1 NameNode(NN):
HDFS系统核心组件,负责分布式文件系统的名字空间管理、INode表的文件映射管理。如果不开启备份/故障恢复/Federation模式,一般的HDFS系统就只有1个NameNode,当然这样是存在单点故障隐患的。
NN管理两个核心的表:文件到块序列的映射、块到机器序列的映射。
第一个表存储在磁盘中,第二表在NN每次启动后重建。
1.2 NameNodeServer(NNS):
负责NN和其它组件的通信接口的开放(IPC、http)等。
NN通过客户端协议(ClientProtocol)和客户端通信,通过数据节点协议(DataNodeProtocol)和DN通信。
1.3 FSNameSystem:
管理文件系统相关,承担了NN的主要职责。
1.4 DataNode(DN):
分布式文件系统中存放实际数据的节点,存储了一系列的文件块,一个DFS部署中通常有许多DN。
DN和NN,DN和DN,DN和客户端都通过不同的IPC协议进行交互。
通常,DN接受来自NN的指令,比如拷贝、删除文件块。
客户端在通过NN获取了文件块的位置信息后,就可以和DN直接交互,比如读取块、写入块数据等。
DN节点只管理一个核心表:文件块到字节流的映射。
在DN的生命周期中,不断地和NN通信,报告自己所存储的文件块的状态,NN不直接向DN通信,而是应答DN的请求,比如在DN的心跳请求后,回复一些关于复制、删除、恢复文件块的命令(comands)。
DN和外界通信的接口会报告给NN,想和此DN交互的客户端或其它DN可以通过和NN通信来获取这一信息。
1.5 Block
文件块,hadoop文件系统的原语,hadoop分布式文件系统中存储的最小单位。一个hadoop文件就是由一系列分散在不同的DataNode上的block组成。
1.6 BlockLocation
文件块在分布式网络中的位置,也包括一些块的元数据,比如块是否损坏、块的大小、块在文件中的偏移等。
1.7 DFSClient
分布式文件系统的客户端,用户可以获取一个客户端实例和NameNode及DataNode交互,DFSClient通过客户端协议和hadoop文件系统交互。
1.8 Lease
租约,当客户端创建或打开一个文件并准备进行写操作,NameNode会维护一个文件租约,以标记谁正在对此文件进行写操作。客户端需要定时更新租约,否则当租约过期,NN会关闭文件或者将文件的租约交给其它客户端。
1.9 LeaseRenewer
续约管控线程,当一个DFSClient调用申请租约后,如果此线程尚未启动,则启动,并定期向NameNode续约。
2.创建文件流程分析
创建一个名为tmpfile1.dat的文件,主要流程如下:
2.1 发送创建文件的请求(CreateFile)
客户端向NN发起请求,获取文件信息,NN会在缓存中查找是否存在请求创建的文件项(file entry),如果没找到,就在NameSystem中创建一个新的文件项:
块管理器(BlockManager)检查复制因子是否在范围内,如果复制因子过小或过大就会异常。
同时会进行权限验证、加密、安全模式检测(如果在安全模式不能创建文件)等,并记录操作日志和事件日志,然后向客户端返回文件状态。
2.2 申请文件租用权(beginFileLease)
客户端取得文件状态后,对文件申请租用(lease),如果租用过期,客户端将无法再继续对文件进行访问,除非进行续租。
2.3 数据流管控线程启动(DataStreamer & ResponseProcessor)
DataStreamer线程负责数据的实际发送:
当数据队列(Data Queue)为空时,会睡眠,并定期苏醒以检测数据队列是否有新的数据需要发送、Socket套接字是否超时、是否继续睡眠等状态。
ResponseProcessor负责接收和处理pipeline下游传回的数据接收确认信息pipelineACK。
2.4 发送添加块申请并初始化数据管道(AddBlock & Setup Pipeline)
当有新的数据需要发送,并且块创建阶段处于PIPELINE_SETUP_CREATE,DataStreamer会和NameNode通信,调用AddBlock方法,通知NN创建、分配新的块及位置,NN返回后,初始化Pipeline和发送流。
2.5 DataNode数据接收服务线程启动(DataXceiverServer & DataXceiver)
当DataNode启动后,其内部的DataXceiverServer组件启动,此线程管理向其所属的DN发送数据的连接建立工作,新连接来时,DataXceiverServer会启动一个DataXceiver线程,此线程负责流向DN的数据接收工作。
2.6 在Pipeline中处理数据的发送和接收
客户端在获取了NameNode分配的文件块的网络位置之后,就可以和存放此块的DataNode交互。
客户端通过SASL加密方式和DN建立连接,并通过pipeline来发送数据。
2.6.1 从pipeline接收数据
pipeline由数据源节点、多个数据目的节点组成,请参考上面的流程图。位于pipeline中的第一个DataNode会接收到来自客户端的数据流,其内部DataXceiver组件,通过读取操作类型(OP),来区分进行何种操作,如下所示:
protected final void processOp(Op op) throws IOException {
switch(op) {
case READ_BLOCK:
opReadBlock();
break;
//本例中将会使用WRITE_BLOCK指令
case WRITE_BLOCK:
opWriteBlock(in);
break;
//略...
default:
throw new IOException("Unknown op " + op + " in data stream");
}
}
如果OP是WRITE_BLOCK,调用写数据块的方法,此方法会根据数据源是客户端还是其他DataNode、块创建的阶段等条件进行不同的逻辑。
2.6.2 数据在pipeline中流动
在本例中,第一个收到数据的DN会再启动一个blockReceiver线程,以接收实际的块数据,在本地保存了块数据后,其负责向pipeline中的后续DN继续发送块数据。每次向下游DN节点发送数据,标志着数据目的节点的targets数组都会排除自身,这样,就控制了pipeline的长度。
下游收到块数据的DN会向上游DN或者客户端报告数据接收状态。
这种链式或者序列化的数据转移方式,就像数据在管道中从上游流向下游,所以这种方式称作pipeline。2.6.3 pipeline的生命周期
在本例中:
DataStreamer线程启动后,pipeline进入PIPELINE_SETUP_CREATE阶段;
数据流初始化后,pipeline进入DATA_STREAMING阶段;
数据发送完毕后,pipeline进入PIPELINE_CLOSE阶段。客户端在DataStreamer线程启动后,同时启动了一个ResponseProcessor线程,此线程用于接收pipeline中来自下游节点的数据接收状态报告pipelineACK,同时此线程和DataStreamer线程协调管理pipeline状态。
当DataStreamer向pipeline发送数据时,会将发送的数据包(packet)从数据队列(Data Queue)中移除,并加入数据确认队列(Ack Queue):
//DataStreamer发送数据后,将dataQueue的第一个元素出队,并加入ackQueue
one = dataQueue.getFirst();
dataQueue.removeFirst();
ackQueue.addLast(one);
而当ResponseProcessor收到下游的pipelineAck后,据此确认信息来判断pipeline状态,是否需要重置和重新调整。如果确认信息是下游节点数据接收成功了,就将确认队列(AckQueue)的第一个数据包删除。//ResponseProcessor收到成功的Ack,就将ackQueue的第一个包移除
lastAckedSeqno = seqno;
ackQueue.removeFirst();
dataQueue.notifyAll();
通过这样的方式,DataStreamer可以确认数据包是否发送成功,也可以确认全部的数据包是否已经发送完毕。显然,当AckQueue空了,并且已经发送的数据包是块里的最后一个包,数据就发送完毕了。
发送完毕的判断如下所示:
if (one.lastPacketInBlock) {
// wait for all data packets have been successfully acked
synchronized (dataQueue) {
while (!streamerClosed && !hasError &&
ackQueue.size() != 0 && dfsClient.clientRunning) {
try {
// wait for acks to arrive from datanodes
dataQueue.wait(1000);
} catch (InterruptedException e) {
DFSClient.LOG.warn("Caught exception ", e);
}
}
}
if (streamerClosed || hasError || !dfsClient.clientRunning) {
continue;
}
//在没有错误的情况下,AckQueue为空,并且包one是block的最后一个包,数据就发送完了
stage = BlockConstructionStage.PIPELINE_CLOSE;
}
2.7 发送文件操作完成请求(completeFile)
客户端向NameNode发送completeFile请求:
NN收到请求后,验证块的BlockPoolId是否正确,接着对操作权限、文件写锁(write lock)、安全模式、租约、INode是否存在、INode类型等等进行验证,最后记录操作日志并返回给客户端。
2.8 停止文件租约(endFileLease)
客户端在完成文件写操作后,调用leaseRenewer(LR)实例,从LR管理的续约文件表中删除此文件,表明不再更新租约,一段时间后,租约在NN端自然失效。
总结:
在向DataNode写数据的时候,Client需要知道它需要知道自身的数据要写往何处,在茫茫Cluster 中,DataNode成百上千,写到DataNode的那个Block块下,是Client需要清楚的。在通过create创建一个空文件时,输出流会向 NameNode申请Block的位置信息,在拿到新的Block位置信息和版本号后,输出流就可以联系DataNode节点,通过写数据流建立数据流管 道,输出流中的数据被分成一个个文件包,并最终打包成数据包发往数据流管道,流经管道上的各个DataNode节点,并持久化。
Client在写数据的文件副本默认是3份,换言之,在HDFS集群上,共有3个DataNode节点会保存这份数据的3个副本,客户端在发送 数据时,不是同时发往3个DataNode节点上写数据,而是将数据先发送到第一个DateNode节点,然后,第一个DataNode节点在本地保存数 据,同时推送数据到第二个DataNode节点,依此类推,直到管道的最后一个DataNode节点,数据确认包由最后一个DataNode产生,并逆向 回送给Client端,在沿途的DataNode节点在确认本地写入成功后,才会往自己的上游传递应答信息包。这样做的好处总结如下:
分摊写数据的流量:由每个DataNode节点分摊写数据过程的网络流量。
降低功耗:减小Client同时发送多份数据到DataNode节点造成的网络冲击。
另外,在写完一个Block后,DataNode节点会通过心跳上报自己的Block信息,并提交Block信息到NameNode保存。当 Client端完成数据的写入之后,会调用close()方法关闭输出流,在关闭之后,Client端不会在往流中写数据,因而,在输出流都收到应答包 后,就可以通知NameNode节点关闭文件,完成一次正常的写入流程。
在写数据的过程当中,也是有可能出现节点异常。然而这些异常信息对于Client端来说是透明的,Client端不会关心写数据失败后 DataNode会采取哪些措施,但是,我们需要清楚它的处理细节。首先,在发生写数据异常后,数据流管道会被关闭,在已经发送到管道中的数据,但是还没 有收到确认应答包文件,该部分数据被重新添加到数据流,这样保证了无论数据流管道的哪个节点发生异常,都不会造成数据丢失。而当前正常工作的 DateNode节点会被赋予新的版本号,并通知NameNode。即使,在故障节点恢复后,上面只有部分数据的Block会因为Blcok的版本号与 NameNode保存的版本号不一致而被删除。之后,在重新建立新的管道,并继续写数据到正常工作的DataNode节点,在文件关闭 后,NameNode节点会检测Block的副本数是否达标,在未达标的情况下,会选择一个新的DataNode节点并复制其中的Block,创建新的副 本。这里需要注意的是,DataNode节点出现异常,只会影响一个Block的写操作,后续的Block写入不会收到影响
参照博客链接:
(1)http://www.cnblogs.com/foreach-break/p/hadoop_hdfs_pipeline.html#_2
(2)https://yq.aliyun.com/articles/34086