HDFS深度历险 之 从客户端逻辑看HDFS写入机制
2019-01-04 16:47 猴子顶呱呱 阅读(535) 评论(0) 编辑 收藏 举报说明
除了标注之外,本文纯属原创,转载请注明出处:https://www.jianshu.com/p/ea6ef5f5b868, https://www.cnblogs.com/monkeyteng/p/10220395.html
HDFS架构简介
Hadoop的框架最核心的设计就是:HDFS和MapReduce。HDFS为海量的数据提供了存储,则MapReduce为海量的数据提供了计算。本文基于Hadoop 2.7.3源码,分析本地文件推送(新建/追加)到的HDFS客户端逻辑。
- HDFS架构主要包含两种类型的节点:NameNode和DataNode。
- NameNode,其实就是名字节点,其功能类似于我们常用的磁盘文件系统中的inode。对于HDFS而言,NameNode相当于“目录管理器”和“inode表”。
- NameNode保存两类关键的映射表:
- 名字空间表:从文件名到数据块(DataBlock)的映射,这部分数据保存在NameNode服务器的磁盘。
- inode表:从数据块(DataBlock)到机器的映射,包括每一个数据块保存在哪一个或者哪几个机器上。这部分数据在每次重启NameNode的时候都会和DataNode通讯并重建。
- 对于Hadoop 2.7.3而言,一个DataBlock默认是128MB,所以一个文件可能需要N个DataBlock来存储,那么名字空间表很可能是一个文件名映射到一个DataBlock的数组。
- 关于这两张表如何协作定位文件:
- 当使用文件名访问文件时,NameNode会查询名字空间表,根据这个文件名获取它所有内容对应的DataBlock列表(是不是很类似于单机磁盘的数据访问)。此时inode表会查询每一个DataBlock的信息,包括它所在的位置(DataNode的IP+端口)、DataBlock的ID和时间戳以及里面数据的长度(<=128MB)等。
- 这个DataBlock列表返回到客户端,客户端根据每个DataBlock上的信息(线索),分别连接到每个DataNode上,获取上面存储的数据。
- 客户端与NameNode、NameNode与DataNode的连接,全部都是通过ProtoBuf的RPC调用来实现的。关于ProtoBuf可以参考这里。例如,下面就是追加文件的append请求的RPC协议:
//摘自hadoop-hdfs-project/hadoop-hdfs/src/main/proto/ClientNamenodeProtocol.proto
//RPC请求
rpc append(AppendRequestProto) returns(AppendResponseProto);
//请求报文
message AppendRequestProto {
required string src = 1;
required string clientName = 2;
optional uint32 flag = 3; // bits set using CreateFlag
}
//应答报文
message AppendResponseProto {
optional LocatedBlockProto block = 1;
optional HdfsFileStatusProto stat = 2;
}
HDFS写文件Pipeline机制
HDFS在对文件的写入方面,只允许数据追加到文件末尾,而不允许在文件中间修改文件。因为在文件中间修改文件,需要涉及文件锁、数据块之类的比较复杂的逻辑。
Hadoop的文件按照DataBlock分块,并以DataBlock为单位做冗余(负载均衡)。HDFS可以指定一个复制因子(replication),默认是保存3份,根据dfs.replication
配置项配置。
下面分析HDFS写文件的Pipeline流程(蓝色线表示用于通讯,红色线表示数据的传输路线):
- ①客户端发送请求到NameNode,请求写文件/新建数据块。
- NameNode收到请求后,会给客户端分配一个数据块,其ID是
blk_123456
,并指明DataBlock各个拷贝所在的各个DataNode的IP和端口(图中是分别存在于三个DataNode中)。 - 这一系列的DataNode称为Pipeline,也就是数据传输的管道,也就是【DataNode_1:50010, DataNode_2:50010, DataNode_3:50010】。
- ②客户端收到数据块的信息,开始对DataNode发起写的请求,请求报文包括要写的数据块,要写的数据大小等等。请求成功后,发送数据到第一个DataNode,也就是图中的DataNode_1,在该请求中包含DataBlock各个拷贝的地址(包含DataNode2和DataNode3的地址):【DataNode_1:50010, DataNode_2:50010, DataNode_3:50010】,发送完成之后等待DataNode_1返回的ACK报文。
- ③DataNode_1收到数据后,保存数据,并把数据发送到DataNode_2,Pipeline修改为【DataNode_2:50010, DataNode_3:50010】,发送完成之后等待DataNode_2返回的ACK报文。
- ④DataNode_2收到数据后,把数据发送到DataNode_3,Pipeline修改为【DataNode_3:50010】,发送完成之后等待DataNode_2返回的ACK报文。
- ⑤DataNode_3发现Pipeline中只有自己,不再有下游的DataNode节点,于是处理完成之后只需要返回ACK到Pipeline的上游节点,即DataNode_2。
- ⑥DataNode_2收到DataNode_3的ACK,于是把ACK发送到Pipeline的上游节点,即DataNode_1。
- ⑦DataNode_1收到DataNode_2的ACK,把ACK发送到Pipeline的上游节点,即客户端。
数据发送至此完成。
HDFS文件推送客户端
要把本地文件推送到HDFS,可以通过以下两个命令实现:
hadoop fs -appendToFile <localsrc> ... <dst>
hadoop fs -put [-f] [-p] [-l] <localsrc> ... <dst>
跟踪调用堆栈发现,这两个命令最终是调用DFSOutputStream.java
中的代码实现文件的拷贝。
辅助发送的相关类和数据结构
这份代码里面包含了一些用于辅助发送的类:
DFSOutputStream
:实现了发送数据的主流程,最主要是继承自FSOutputSummer
这个虚拟类的接口方法writeChunk
。DataStreamer
:继承自Daemon
的后台线程,主要实现数据的流式发送。ResponseProcessor
:同样继承自Daemon
的后台线程,主要实现对已发送数据包的ACK报文的接收。
还有一些保存发送数据相关信息的数据结构:
DFSPacket
:表示发送出去的一个数据包,包含相应的请求头部以及相关标志位。LinkedList<DFSPacket> dataQueue
:用于保存待发送的数据包。它是主线程*DFSOutputStream
和发送线程DataStreamer
之间生产者-消费者关系的共享数据结构。LinkedList<DFSPacket> ackQueue
:用于保存已经发送的数据包。发出去的数据包还要等待DataNode返回ACK才可以被认为是发送成功。它是发送线程DataStreamer
与ACK接收线程ResponseProcessor
之间生产者-消费者关系的共享数据结构。BlockConstructionStage stage
:这是一个状态变量,整个发送流程就相当于一个状态机。
看完上面的数据结构,整个数据发送流程就很明显了:
DFSOutputStream
把数据组装成DFSPacket
对象,放入dataQueue
;然后等待发送线程DataStreamer
发送到DataNode;DataStreamer
发送之后,把DFSPacket
对象移动到ackQueue
,等待ACK线程ResponseProcessor
在收到对应的ACK之后把该DFSPacket
从队列移除。
下面主要分析DFSOutputStream.java
这个客户端代码的执行流程。
数据发送的主要流程
newStreamForCreate/newStreamForAppend
这两个静态函数用于创建DFSOutputStream
对象。一个是用于新建文件,一个用于追加到现有的文件。两个函数主要差别在于,前者需要新建一个文件(发送create的RPC请求到NameNode),后者直接通过发送append的RPC请求到NameNode,在返回报文中获取文件最后的一个数据块并开始写入。newStreamForCreate/newStreamForAppend
这两个函数返回一个DFSOutputStream
的对象,然后被org.apache.hadoop.io.IOUtils.copyBytes()
调用DFSOutputStream
的writeChunk
接口函数,把本地的数据块发送出去。下面主要看writeChunk
函数。这个函数的参数主要包括数据的缓冲区、要发送的数据在DataBlock中的offset、还有数据的校验等。