HDFS 的文件访问机制为流式访问机制:即通过 API 打开文件的某个数据块之后,可以顺序读取或者写入某个文件.
由于 HDFS 中存在多个角色,且对应的应用场景主要为一次写入、多次读取的场景,因此其读和写的方式有较大不同.
读、写操作都由客户端 Client 发起,并且由客户端进行整个流程的控制,NameNode 和 DataNode 都是被动式响应.
读取流程
- 客户端发起读取请求时,首先与 NameNode 进行连接;
- 建立连接完成后,客户端会请求读取某个文件的某一个数据块;
- NameNode 在内存中进行检索,查看是否有对应的文件及文件块;
若没有则通知客户端对应文件或数据块不存在,
若有则通知客户端对应的数据块存在哪些DataNode上; - 客户端接收到信息之后,首先选择离它最近的一个DataNode 进行连接–[就近原则]
- 客户端与最近的 DataNode 连接后,开始进行数据传输–[客户端直接与存储数据的DataNode进行通信]
读取文件的具体过程如下图所示:
读取流程原理
- 客户端调用 DistributedFileSystem(分布式文件系统)的 Open() 方法打开文件;
- DistributedFileSystem 用 RPC 协议连接到 NameNode,请求获取文件的数据块的信息;
NameNode 返回文件的部分或者全部数据块列表;
对于每个数据块,NameNode 都会返回该数据块副本的所有 DataNode 地址;
DistributedFileSystem 返回 FSDataInputStream(文件数据输入流)给客户端,用来读取数据. - 客户端调用 FSDataInputStream 的 Read() 方法开始读取数据;
- FSInputStream 连接保存此文件第一个数据块的最近的 DataNode,并以数据流的形式读取数据;
客户端多次调用 Read(),直到到达数据块结束位置. - FSInputStream 连接保存此文件下一个数据块的最近的 DataNode,并读取数据;
- 当客户端读取完所有数据块的数据后,调用 FSDataInputStream 的 Close() 方法.
在读取数据的过程中,如果客户端在与数据结点通信时出现错误,则尝试连接包含此数据块的下一个数据结点;失败的数据结点将被记录,并且以后不再连接.
写入流程
- 客户端向NameNode发出写文件请求.
- 检查是否已存在文件、检查权限;若通过检查,直接先将操作写入EditLog,并返回输出流对象.
[WAL-write ahead log:先写Log,再写内存;
因为EditLog记录的是最新的HDFS客户端执行所有的写操作,如果后续真实写操作失败了,
由于在真实写操作之前,操作就被写入EditLog中了,故EditLog中仍会有记录,我们不用担心后续client读不到相应的数据块,
因为在第5步中DataNode收到块后会有一返回确认信息,若没写成功,发送端没收到确认信息,会一直重试,直到成功] - Client端按128MB的块切分文件.
- Client将NameNode返回的分配的可写的DataNode列表和Data数据一同发送给最近的第一个DataNode节点,
此后Client端和NameNode分配的多个DataNode构成pipeline管道,Client端向输出流对象中写数据;
Client每向第一个DataNode写入一个packet,这个packet便会直接在pipeline里传给第二个、第三个…DataNode.
[注:并不是写好一个块或一整个文件后才向后分发] - 每个DataNode写完一个块后,会返回确认信息.
[注:并不是每写完一个packet后就返回确认信息,因为packet中的每个chunk都携带校验信息,没必要每写一个就汇报一下,
这样效率太慢;正确的做法是写完一个block块后,对校验信息进行汇总分析,就能得出是否有块写错的情况发生] - 写完数据,关闭输输出流.
- 发送完成信号给NameNode.
[注:发送完成信号的时机取决于集群是强一致性还是最终一致性,强一致性则需要所有DataNode写完后才向NameNode汇报;
最终一致性则其中任意一个DataNode写完后就能单独向NameNode汇报, HDFS一般情况下都是强调强一致性]
写入文件的过程比读取较为复杂,在不发生任何异常情况下,客户端向 HDFS 写入数据的流程如下图所示:
写入流程原理
- 客户端调用 DistribuedFileSystem 的 Create() 方法来创建文件.
- DistributedFileSystem 用 RPC 协议连接 NameNode,请求在文件系统的命名空间中创建一个新的文件;
NameNode 首先确定文件原来不存在,并且客户端有创建文件的权限,然后创建新文件;
DistributedFileSystem 返回 FSOutputStream 给客户端用于写数据. - 客户端调用 FSOutputStream 的 Write() 函数,向对应的文件写入数据.
- 当客户端开始写入文件时,FSOutputStream 会将文件切分成多个分包(Packet),并写入其內部的数据队列;
FSOutputStream 向 NameNode 申请用来保存文件和副本数据块的若干个 DataNode,这些 DataNode 形成一个数据流管道;
队列中的分包被打包成数据包,发往数据流管道中的第一个 DataNode;
第一个 DataNode 将数据包发送给第二个 DataNode,第二个 DataNode 将数据包发送到第三个 DataNode(复制过程)
这样,数据包会流经管道上的各个 DataNode。 - 为了保证所有 DataNode 的数据都是准确的,接收到数据的 DataNode 要向发送者发送确认包(ACK Packet).
确认包沿着数据流管道反向而上,从数据流管道依次经过各个 DataNode,并最终发往客户端;
当客户端收到应答时,它将对应的分包从内部队列中移除。 - 不断执行第 (3)~(5)步,直到数据全部写完。
- 调用 FSOutputStream 的 Close() 方法,将所有的数据块写入数据流管道中的数据结点,并等待确认返回成功;
最后通过 NameNode 完成写入.
/**
Question:客户端是怎么将数据传输给DataNode的呢?
通过NIO!
向HDFS上传文件时,主要是通过DistributedFileSystem里的DFSOutputStream实现。
DFSOutputStream 中有个 DataStreamer streamer(负责开启线程,通过nio的方式将Packet传输至hdfs) 对象
DataStreamer 里有个 DataOutputStream blockStream (里面实现了SocketOutputStream接口,负责发送Packet)
文件数据的传输单位为Packet,一个Packet默认大小为64K,一个Packet中含有多个chunk,chunk为校验数据的单位,默认大小512byte。
*/
源码
void waitForIO(int ops) throws IOException {
if(selector.select(channel,ops,timeout) == 0){
throw new SocketTimeoutException(timeoutExceptionString(channel,timeout,ops));
}
}
RPC 协议
RPC[Remote Procedure Call]—远程过程调用协议:是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议.
RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。
RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
RPC采用客户机/服务器模式:请求程序就是一个客户机,而服务提供程序就是一个服务器。
过程:
- 首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。
- 在服务器端,进程保持睡眠状态直到调用信息到达为止。
当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息。 - 最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续进行。
对比HTTP协议:
RPC的核心并不在于使用什么协议。
RPC的目的是让你在本地调用远程的方法,而对你来说这个调用是透明的,你并不知道这个调用的方法是部署哪里。
通过RPC能解耦服务,这才是使用RPC的真正目的;
RPC主要用在内部服务间的通信。
因为Hadoop服务器之间已经建立了互信(SSH)所以使用 RPC 比 HTTP 的请求与响应更快!