1.读流程 (图1):
1.打开文件 :客户端通过通用文件系统抽象类 FileSystem.open() 打开文件。然后 DistributedFileSystem 会创建输入流 FSDataInputStream 。
2.获取数据块信息 :输入流通过 Client.getBlockLocations() 远程调用名称节点,并获取文件开始部分数据块的保存位置。同时,名称节点返回拥有该数据块的所有数据节点的地址,同时根据距离客户端远近对数据节点进行排序。然后, DistributedFileSystem 将实例化 FSDataInputStream 返回给客户端,同时返回最终数据块的数据节点地址。
3.读取请求 :获得输入流 FSDataInputStream 后,客户端调用 read() 函数读取数据。输入流根据第 2 步数据节点排序结果, 选择距离客户端最近的节点建立连接并读取数据 。如果连接失败,则尝试与下一个节点连接。
4.读取数据 :当数据块读取完毕后, FSDataInputStream 关闭第 3 步中连接的节点。
5.寻找下一数据块信息 :输入流通过 getBlockLocations() 查找下一个数据块,若之前客户端已经缓存了该数据块的位置信息,则不需要调用。
6.继续读取 :同步骤 4.
7.关闭请求 : 当客户端全部读取完毕后,调用 FSDataInputStream 的 Close() 函数关闭输入流。
图 1 HDFS 1.0 读过程
2.写流程 (图2):
1.创建文件请求 :客户端通过 FileSystem.create() 创建文件。然后 DistributedFileSystem 会创建输出流 FSDataOputStream 。
2.创建文件元数据 :与读过程不同的是, DistributedFileSystem 采用 RPC 远程调用名称节点,在文件系统的命名空间中新建一个新的文件。名称节点会执行一些检查(如文件是否已经存在、客户端是否有权限等)。然后,名称节点构造新文件,并添加文件的元数据信息。接下来, DistributedFileSystem 将实例化 DFSOutputStream 返回给客户端,运行客户端通过该输出流写入数据。
3.写入数据 :获得输出流 DFSOutputStream 后。客户端通过 write() 方法向 HDFS 中对应文件写入数据。
4.写入数据包 :输出流 DFSOutputStream 中写入的数据会被分割成一个个分包,这些分包被放入 DFSOutputStream 对象的内部队列中。然后输出流会 向名称节点申请保存文件和副本数据块的指定个数的数据节点,这些节点直接排成数据流管道 。之前输出流的分包打包成数据包,首先发送给第一个数据节点,第一个数据节点再发送给第二个节点, 类似于流水线一样复制给若干个数据节点 。
5.接收确认包 :为了保证不同机器上数据节点之间 “ 复制 “ 成功,接收到数据的数据节点需要向前发送 ” 确认包 “(TCP ACK 确认包机制 ) 。最终最底部的数据节点逆流发送给头部的客户端,当客户端收到应答时,将对应的分包从内部队列移除。反复执行 (3)~(5) 步,直到数据全部写完。
6.关闭请求 :当客户端写完时,会主动关闭输出流。
7.写操作完成 : 通过 ClientProtocol.complete() 通知名称节点关闭文件,完成一次正常的写文件操作。
图 2 HDFS 1.0 读过程
【面试:写问题】
(1)副本机制在哪里选?
准备写时会请求NameNode,然后排序成PipeLine。准备读时则请求最近的节点。
(2)上传Block时,dataNode挂了怎么办?
当 DataNode 突然挂掉了, 客户端一段时间(超时) 接收不到这个 DataNode 发送的 ack 确认 ,客户端会通知 NameNode 。然后NameNode 检查该块的副本与规定的不符, NameNode 会通知 DataNode 去复制副本,并将挂掉的 DataNode 作下线处理,不 再让它参与文件上传与下载。因此,DataNode目前的办法是手动排错重启。
(3)用户B在 block a中追加内容b,此时用户A能读取吗?(腾讯云面试问题)
- 用户B追加过程: hdfs在写数据的时候,并不是在原来的目录里面直接开写的,而是会将需要写的块(block)复制到一个写入状态的目录里面去实现数据追加,当写入终止后,再移动到原来的目录中,覆盖旧的数据块。
- 用户A读取过程: 在读取数据块过程中,datanode会检测数据块变化的,数据块是拆分为多个packet发送。发送之前都会判断一次数据块的内容是否发生变化,若读取过程中出现了块数据变化(读写竞争),也就是数据块被更新了,就会重新读取文件通道,并在packet缓冲区重新计算校验和,然后再发送。
- 用户A读取数据时变化: 若变化会形成两种不同的网络传输模式->①若数据块变化了,packet在网络传输走Java传统IO传输,②未发生变化,packet走Java NIO零拷贝,这样可以极大提升网络传输性能。
结论:因此, 读写互相不冲突,hdfs数据块主要是追加,因此数据读取的过程中即便发生变化,并不会影响到已经读取的数据。
3.HDFS 1.0常见读写出错情况
1. 名称节点出错 :
有时名称节点内容保存的元数据信息不一定正确,因此 HDFS1.0 采用两种机制来确保名称节点安全, 见图 3 。
- 将名称节点上元信息同步存储到其他文件系统(比如远程挂载的网络文件系统 NFS )中。
- 利用第二名称节点中的元数据信息进行系统恢复。
总体来说,第二名称节点只是起到了名称节点的 checkpoint (检查点)作用,并不真正做到 “ 热备份 ” 作用,因此还是有可能丢失部分信息。
一般可以将两种方式结合使用:当名称节点发生故障时,首先到远程挂载的网络文件中获取备份的元数据信息,放在第二名称节点上进行恢复, 并把第二名称代替失效的名称节点使用 。不过在其他 HDFS 版本架构还有多名称节点结构,这里就不再详细介绍。
图 3 名称节点恢复机制
2. 数据节点出错 :
当数据节点故障或者断网时,名称节点就无法收到一些来自数据节点的 “ 心跳 ” 信息,这时数据节点被标记为 “ 宕机 ” ,节点上数据被标记为 “ 不可读 ” , 见图 4 。此时,名称节点不再给它们发送任何 I/O 请求。甚至可能会导致一些数据块的副本数量小于 HDFS 预设的冗余因子。
解决方案 :名称节点会定期检查数据节点,一旦发现某个数据块的副本数量小于冗余因子,就会启动数据冗余复制,为它生成新的副本。此时,新的副本相当于是一个被调整位置后的数据块。
图 4 数据节点出错情况
3. 数据出错 :
由于网络传输和磁盘异常等因素都会造成数据错误。 HDFS 客户端在设计时会采用 MD5 和 sha1 对数据块进行校验,以确保读取正确的数据, 见图 5 。因为文件在写入时会被分为若干个独立的块,所以客户端会对每个文件块进行信息摘录,并把信息写入同一个路径的隐藏文件里面。当客户端读取文件时,会先读取该信息文件,然后利用该信息文件对每个读取的数据块进行校验。如果校验出错,客户端会立即请求其他有副本信息的数据节点读取该数据块,并且通知名称节点,最后名称节点会重新检查并复制该数据块。
图 5 数据出错恢复机制