大数据系列2:Hdfs的读写操作
在前文大数据系列1:一文初识Hdfs中,我们对Hdfs
有了简单的认识。
在本文中,我们将会简单的介绍一下Hdfs
文件的读写流程,为后续追踪读写流程的源码做准备。
Hdfs 架构
首先来个Hdfs
的架构图,图中中包含了Hdfs
的组成与一些操作。
对于一个客户端而言,对于Hdfs的操作不外乎也就读写两个操作,接下来就去看看整个流程是怎么走的。
下面我们由浅及深,氛围简单流程,详细流程分别介绍读写过程
简单流程
读请求流程
客户端需要读取数据的时候,流程大致如下:
-
Client
向NameNod
e发起读请求 -
NameNode
收到读请求后,会返回元数据,包括请求文件的数据块在DataNode
的具体位置。 -
Client
根据返回的元数据信息,找到对应的DataNode
发起读请求 -
DataNode
收到读请求后,会返回对应的Block
数据给Client
。
写请求流程
客户端需要写入数据的时候,流程大致如下:
-
Client
向NameNode
发起写请求,其中包含写入的文件名,大小等。 -
NameNode
接收到信息,NameNode
会将文件的信息存储到本地,同时判断客户端的权限、以及文件是否存在等信息,验证通过后NameNode
返回数据块可以存储的DataNode
信息。 - 客户端会切割文件为多个
Block
,将每个Block
写入DataNode
,在DataNode
之间通过管道,对Block
做数据备份。
详细流程
读请求流程
客户端需要读取数据的时候,流程大致如下:
- 客户端通过调用
FileSystem
的open()
方法来读取文件。、 - 这个对象是
DistributedFileSystem
的一个实例,通过远程调用(RPC)与NameNode
沟通,会向NameNode
请求需要读写文件文件的Block
位置信息。 -
NameNode
会进行合法性校验,然后返回Block
位置信息,每一个Block
都回返回存有该副本的DataNode
地址,并且会根据DtaNode与Client的距离进行排序(这里的距离是指集群网络拓扑的距离,也是尽可能满足数据本地性的要求) -
DistributedFileSystem
会返回一个支持文件定位的输入流FSDataInputStream
给客户端,其中封装着DFSInputStream
对象,该对象管理者DataNode
和NameNode
之间的I/O
-
Client
对这个输入流调用read()
方法 -
DFSInputStream
存储了文件中前几个块的DataNode
地址,然后在文件第一个Block
所在的DataNode
中连接最近的一个DtaNode
。通过对数据流反复调用read()
,可以将数据传输到客户端。 - 当到到
Block
的终点的时候,DFSInputStream
会关闭与DataNode
的链接。然后搜寻下一个Block
的DataNode
重复6、7步骤。在Client
看来,整个过程就是一个连续读取过程。 - 当完成所有
Block
的读取后,Client
会对FSDataInputStream
调用close()
Client
读取数据流的时候,Block
是按照DFSInputStream
与DataNode
打开新的连接的顺序读取的。
并且在有需要的时候,还会请求NameNode
返回下一个批次Blocks
的DataNode
信息
在DFSInputStream
与DataNode
交互的时候出现错误,它会尝试选择这个Block
另一个最近的DataNode
,并且标记之前的DataNode
避免后续的Block
继续在该DataNode
上面出错。
DFSInputStream
也会对来自DataNode
数据进行校验,一旦发现校验错误,也会从其他DataNode
读取该Bclock
的副本,并且向NamaNode
上报Block
错误信息。
整个流程下来,我们可以发现Client直接连接到DataNode检索数据并且通过NameNode知道每个Block的最佳DataNode。
这样设计有一个好处就是:
因为数据流量分布在集群中的所有DataNode
上,所以允许Hdfs扩展到大量并发Client.
与此同时,NamaNode
只需要响应Block
的位置请求(这些请求存储在内存中,非常高效),
而不需要提供数据。
否则随着客户端数量的快速增加,NameNode会成为成为性能的瓶颈。
读请求流程
客户端需要写入数据的时候,流程大致如下:
-
Client
通过create()
方法调用DistributedFileSystem
的create()
-
DistributedFileSystem
通过RPC
向NameNode
请求建立在文件系统的明明空间中新建一个文件,此时只是建立了一个空的文件加,并没有Block
。 -
NameNode
接收到crete
请求后,会进行合法性校验,比如是否已存在想通文件,Client
是否有相关权限。如果校验通过,NameNode
会为新文件创建一个记录,并返回一些可用的DataNode
。否则客户端抛出一个IOException
-
DistributedFileSystem
会返回一个FSDataOutputStream个Client
,与读取数据类似,FSDataOutputStream
封装了一个DFSOutputStream
,负责NameNode
与DataNode
之间交互。 -
Client
调用write()
-
DFSOutputStream
会将数据切分为一个一个packets
,并且将之放入一个内部队列(data queue
),这个队列会被DataStreamer
消费,DataStreamer
通过选择一组合合适DataNodes
来写入副本,并请求NameNode
分配新的数据块。与此同时,DFSOutputStream
还维护一个等待DataNode
确认的内部包队列(ack queue
) - 这些
DataNodes
会被组成一个管道(假设备份数量为3
) - 一旦
pipeline
建立,DataStreamer
将data queue
中存储的packet
流式传入管道的第一个DataNode
,第一个DataNode存储Packet并将之转发到管道中的第二个DataNode
,同理,从第二个DataNode
转发到管道中的第三个DataNode
。 - 当所一个
packet
已经被管道中所有的DataNode
确认后,该packet
会从ack queue
移除。 - 当
Client
完成数据写入,调用close()
,此操作将所有剩余的数据包刷新到DataNode
管道,等待NameNode
返回文件写入完成的确认信息。 -
NameNode
已经知道文件是由哪个块组成的(因为是DataStreamer
请求NameNode
分配Block
的),因此,它只需要等待Block
被最小限度地复制,最后返回成功。
如果在写入的过程中国发生了错误,会采取以下的操作:
- 关闭管道,并将所有在
ack queue
中的packets
加到data queue
的前面,避免故障节点下游的DataNode
发生数据丢失。 - 给该
Block
正常DataNode
一个新的标记,将之告知NameNode
,以便后续故障节点在恢复后能删除已写入的部分数据。 - 将故障节点从管道中移除,剩下的两个正常
DataNodes
重新组成管道,剩余的数据写入正常的DataNodes
。 - 当
NameNode
发现备份不够的时候,它会在另一个DataNode
上创建一个副本补全,随后该Blcok
将被视为正常
针对多个DataNode
出现故障的情况,我们只要设置 dfs.NameNode.replication.min
的副本数(默认为1),Block
将跨集群异步复制,直到达到其目标复制因子( dfs.replication
,默认为3)为止.
通俗易懂的理解
上面的读写过程可以做一个类比,
NameNode
可以看做是一个仓库管理员;DataNode
可以看作是仓库;
管理员负责管理商品,记录每个商品所在的仓库;
仓库负责存储商品,同时定期想管理员上报自己仓库中存储的商品;
此处的商品可以粗略的理解为我们的数据。
管理员只能有一个,而仓库可以有多个。
当我们需要出库的时候,得先去找管理员,在管理员处取得商品所在仓库的信息;
我们拿着这个信息到对应仓库提取我们需要的货物。
当我们需要入库的时候,也需要找管理员,核对权限后告诉我们那些仓库可以存储;
我们根据管理员提供的仓库信息,将商品入库到对应的仓库。
存在的问题
上面是关于Hdfs
读写流程介绍,虽然我分了简单和详细,但是实际的读写比这个过程复杂得多。
比如如何切块?
为何小于块大小的文件按照实际大小存储?
备份是如何实现的?Block
的结构等等。
这些内容会在后续的源码部分详细解答。
此外,有人也许发现了,前文大数据系列1:一文初识Hdfs中Hdfs架构的介绍和本文读写的流程的介绍中,存在一个问题。
就是NameNode
的单点故障问题。虽然之前有SecondaryNameNode
辅助NameNode
合并fsiamge
和edits
,但是这个还是无法解决NameNode
单点故障的问题。
很多人听过HA(High Availability)
即高可用,误以为高可用就是SecondaryNameNode
,其实并不是。
在下一篇文章中会介绍Hdfs
高可用的实现方式。
想了解更多内容观影关注:【兔八哥杂谈】