这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
正文
读
①客户端通过调用 FileSystem 对象的 open() 方法来打开希望读取的文件,对于HDFS来说,这个对象是 DistributedFileSystem 的一个实例。
②DistributedFileSystem 通过使用远程过程调用(RPC)来调用 NameNode,以确定文件起始块的位置。
③对于每一个块, NameNode 返回存有该块副本的 DataNode 地址。此外,这些 DataNode 根据它们与客户端的距离来排序(根据集群的网络拓扑)。
如果该客户端本身就是一个 DataNode (比如在一个 MapReduce 任务中),那么该客户端将会从保存有相应数据块副本的本地 DataNode 读取数据。
④DistributedFileSystem 类返回一个 FSDataInputStream 对象(一个支持文件定位的输入流)给客户端以便读取数据。
FSDataInputStream 类转而封装 DFSInputStream 对象,该对象管理着 DataNode 和 NameNode 的I/O 。
⑤接着,客户端对这个输人流调用 read() 方法。
存储着文件起始几个块的 datanode 地址的 DFSInputStream 随即连接距离最近的文件中第一个块所在的 DataNode。
通过对数据流反复调用 read() 方法,可以将数据从 DataNode 传输到客户端。
⑥到达块的末端时, DFSInputStream 关闭与该 DataNode 的连接, 然后寻找下一个块的最佳 DataNode。
所有这些对于客户端都是透明的,在客户看来它一直在读取一个连续的流客户端从流中读取数据时,块是按照打开 DFSInputStream 与 DataNode 新建连接的顺序读取的。
它也会根据需要询问 NameNode 来检索下一批数据块的 DataNode 的位置。
⑦一旦客户端完成读取,就对 FSDataInputStream 调用 close() 方法。
异常处理
在读取数据的时候,如果 DFSInputStream 在与 DataNode 通信时遇到错误,会尝试从这个块的另外一个最邻近 DataNode 读取数据。
它也记住那个故障 DataNode,以保证以后不会反复读取该节点上后续的块。
DFSInputStream 也会通过校验和确认从 DataNode 发来的数据是否完整。
如果发现有损坏的块, DFSInputStream 会试图从其他 DataNode 读取其副本,也会将被损坏的块通知给 NameNode。
这个设计的一个重点是,客户端可以直接连接到 DataNode 检索数据, 且 NameNode 告知客户端每个块所在的最佳 DataNode。
由于数据流分散在集群中的所有 DataNode,所以这种设计能使 HDFS 扩展到大量的并发客户端。
同时,DataNode 只需要响应块位置的请求(这些信息存储在内存中,因而非常高效),无须响应数据请求,否则随着客户端数量的增长, NameNode 会很快成为瓶颈。
写
我们要考虑的情况是如何新建一个文件,把数据写入该文件,最后关闭该文件。
① 客户端通过对 DistributedFileSystem 对象调用 create() 来新建文件。
② DistributedFileSystem 对 NameNode 创建一个RPC调用,在文件系统的命名空间中新建一个文件,此时该文件中还没有相应的数据块。
③ NameNode 执行各种不同的检查以确保这个文件不存在以及客户端有新建该文件的权限。
如果这些检查均通过, NameNode 就会为创建新文件记录一条记录;否则文件创建失败并向客户端抛一个 IOException 异常。
④ DistributedFileSystem 向客户端返回一个 FSDataOutputStream 对象,由此客户端可以开始写入数据。
就像读取事件一样, FSDataOutputStream 封装一个 DFSOutputStream 对象, 该对象负责处理 DataNode 和 NameNode 之间的通信。
⑤ 在客户端写人数据时, DFSOutputStream 将它分成一个个的数据包,并写入内部队列,称为“数据队列”(data queue)。
DataStreamer 处理数据队列,它的责任是挑选出适合存储数据副本的一组 DataNode ,并据此来要求 NameNode 分配新的数据块。
⑥ 这一组 DataNode 构成一个管线(Pipeline)一一我们假设副本数为3,所以管线中有3个节点。
⑦ DataStreamer 将数据包流式传输到管线中第1个 DataNode ,该 DataNode 存储数据包并将它发送到管线中的第2个 DataNode 。
⑧ 同样,第2个 DataNode 存储该数据包并且发送给管线中的第3个(也是最后一个) DataNode。
⑨ DFSOutputStream 也维护着一个内部数据包队列来等待 DataNode 的收到确认回执,称为“确认队列”(ack queue)。
收到管道中所有 DataNode 确认信息后,该数据包才会从确认队列删除。
⑩ 客户端完成数据的写入后,对数据流调用 close() 方法。
该操作将剩余的所有数据包写入 DataNode 管线,并在联系到 NameNode 告知其文件写入完成之前,等待确认。
NameNode 已经知道文件由哪些块组成(因为 DataStreamer 请求分配数据块),所以它在返回成功前只需要等待数据块进行最小量的复制。
复制代码
异常处理
单 DataNode 故障
如果任何 DataNode 在数据写入期间发生故障,则执行以下操作(对写入数据的客户端是透明的)。
- 首先关闭管线,确认把队列中的所有数据包都添加回数据队列的最前端,以确保故障节点下游的 DataNode 不会漏掉任何一个数据包。
- 为存储在另一正常 DataNode 的当前数据块指定一个新的标识,并将该标识传送给 NameNode ,以便故障 DataNode 在恢复后可以删除存储的部分数据块。
- 从管线中删除故障 DataNode ,基于两个正常 DataNode 构建一条新管线。余下的数据块写入管线中正常的 DataNode。
- NameNode 注意到块副本数量不足时, 会在另一个节点上创建一个新的副本。后续的数据块继续正常接受处理。
多 DataNode 故障
在一个块被写入期间可能会有多个 DataNode 同时发生故障,但非常少见。
只要写入了 dfs.NameNode.replication.min 的副本数(默认为1),写操作就会成功。
并且这个块可以在集群中异步复制,直到达到其目标副本数(dfs.replication的默认值为3)。