2.4 读写数据
客户端可以通过多种不同的工具和应用程序接口(参见2.8节“访问与集成”)对HDFS进行读写操作,这些操作都遵循着同样的流程。在某些层面,客户端可能要使用到Hadoop库函数,因为只有Hadoop库函数才清楚知道HDFS的具体细节和相关语法。函数库封装了大部分与NameNode 和DataNode通信相关的细节,同时也考虑了分布式文件系统在诸多场景中的错误处理机制。
2.4.1 数据读取流程
首先,我们来看一下HDFS数据读取操作的处理逻辑。假设,HDFS中已经存储了一个文件/user/esammer/foo.txt,要读取文件,Hadoop客户端程序库(通常是Java的JAR文件)是必不可少的。同时,客户端还必须有集群配置数据的副本,因为它包含了NameNode的位置信息(参见第5章)。如图2-2所示,客户端首先要访问NameNode,并告诉它所要读取的文件,当然,这之前必须对客户的身份进行确认。客户身份确认有两种方式:一种是通过信任的客户端,由其指定用户名;第二种方式是通过诸如Kerberos(参见第6章)等强认证机制来完成。接下来还必须检查文件的所有者及其设定的访问权限。如果文件确实存在,而且用户对这个文件有访问权限,这时NameNode就会告诉客户端这个文件的第一个数据块的标号以及保存有该数据块的DataNode列表。这个列表是根据DataNode与客户端间的距离进行了排序的。客户端与DataNode之间的距离是根据Hadoop集群的机架拓扑结构计算得到的。机架拓扑结构记录了主机机架位置的配置信息(有关机架拓扑配置的更多详情,请参见第5.9节“机架拓扑”)。
在NameNode因为自身原因或网络故障无法访问时,客户端会收到超时或异常出错消息,数据读取操作也就无法继续。
有了数据块标号和DataNode的主机名,客户端便可以直接访问最合适的DataNode,读取所需要的数据块。这个过程会一直重复直到该文件的所有数据块读取完成或客户端主动关闭了文件流。
从DataNode读取数据时,可能会发生进程或主机异常结束的情况。这时,数据读操作不会停止,HDFS 程序库会自动尝试从其他有数据副本的DataNode中读取数据。如果所有数据副本都无法访问,则读取操作失败,客户端收到异常出错消息。还有一种情况,当客户端试图从DataNode中读取数据时,NameNode返回的数据块位置信息已经过期。这时如果还有其他DataNode保存有该数据块副本,客户端会尝试从那些DataNode中读取数据,否则至此读取操作就会失败。这些情况很少发生,但对Hadoop这样的大规模分布式系统而言,一旦发生,调查分析过程就会异常复杂。第9章将介绍什么情况可能导致出错以及如何诊断这类问题。
2.4.2 数据写操作流程
HDFS写数据操作比读取数据操作要相对复杂些。我们先来看个最简单的例子:客户端要在集群中创建一个新文件,当然客户端并不一定要真正实现这里介绍的逻辑,在这里只是作为一个例子来介绍Hadoop库函数是如何将数据写入到集群中的。其实应用程序开发人员可以像操作传统的本地文件一样,用他们熟悉的应用程序接口(API)打开文件、写入流,然后关闭流即可。
首先,客户端通过Hadoop文件系统相关API发送请求打开一个要写入的文件,如果该用户有足够的访问权限,这一请求就会被送到NameNode,并在NameNode上建立该文件的元数据。刚建立的新文件元数据并未将该文件和任何数据块关联,这时客户端会收到“打开文件成功”的响应,然后就可以开始写入数据了。当然在API层面会返回一个标准的Java流对象,这一实现只是针对HDFS的。当客户端将数据写入流时,数据会被自动拆分成数据包(这里,不要和TCP数据包或HDFS数据块混淆),并将数据包保存在内存队列中。客户端有一个独立的线程,它从队列中读取数据包,并同时向NameNode请求一组DataNode列表,以便写入下一个数据块的多个副本。接着,客户端直接连接到列表中的第一个DataNode,而该DataNode又连接到第二个DataNode,第二个又连接到第三个上……这样就建立了数据块的复制管道,如图2-3所示。数据包以流的方式写入第一个DataNode的磁盘,同时传入管道中的下一个DataNode并写入其磁盘,依此类推。复制管道中的每一个DataNode都会确认所收数据包已经成功写入磁盘。客户端应用程序维护着一个列表,记录哪些数据包尚未收到确认消息。每收到一个响应,客户端便知道数据已经成功地写入到管道中的一个DataNode。当数据块被写满时,客户端将重新向NameNode申请下一组DataNodes。最终,客户端将剩余数据包全部写入磁盘,关闭数据流并通知NameNode文件写操作已经完成。
然而,凡事绝非如此简单,出现问题在所难免。最常见的情况是,复制管道中的某一DataNode无法将数据写入磁盘(磁盘翘了辫子或DataNode死机)。发生这种错误时,管道会立即关闭,已发送的但尚未收到确认的数据包会被退回到队列中,以确保管道中错误节点的下游节点可以获得数据包。而在剩下的健康数据节点中,正在写入的数据块会被分配新的ID。这样,当发生故障的数据节点恢复后,冗余的数据块就好像不属于任何文件而被自动丢弃,由剩余数据节点组成的新复制管道会重新开放,写入操作得以继续。此时,雨过天晴,写操作将继续直至文件关闭。NameNode如果发现文件的某个数据块正在复制,就会异步地创建一个新的复制块,这样,即便集群的多个数据节点发生错误,客户端仍然可以从数据块的副本中恢复数据,前提是满足要求的最少数目的数据副本已经被正确写入(默认的最少数据副本是1)。