详解DataXceiver读数据的流程和零拷贝数据传输

我们知道DataXceiverServer主要用于监听并接收流式接口请求,然后建立并启动DataXceiver对象。DataXceiver是Receiver的子类,DataTransferProtocol真正的响应操作都是在DataXceiver类中实现的。
流式接口中最重要的一个部分就是客户端从数据节点上读取数据块,DataTransferProtocol.readBlock()给出了读取操作的接口定义,操作码是81。 DataXceiver.readBlock()则实现了DataTransferProtocol.readBlock()方法。
这里写图片描述
客户端通过调用Sender.readblock()方法从指定的数据节点上读取数据块,请求通过IO流到达数据节点后,数据节点DataXceiverServer会创建一个DataXceiver对象响应流式接口请求。
DataXceiver.readblock()会首先会向客户端回复一个Blockresponse响应,指明请求已经接收成功,并通过这个响应给出Datanode当前使用的校验方式。接下来DataXceiver.readblock()方法会将数据节点上的数据块切分成若干数据包,然后依次将数据包发送给客户端。客户端会在接收到每个数据包时进行校验,如果检验和错误,客户端会切断与当前数据节点的连接,选择新的数据节点接收数据;如果数据块内的所有数据包都已经校验成功,客户端就会给这个数据节点发送一个Status.CHECKSUM.OK 响应,表明读取成功。
在readblock方法中会构造一个数据块发送器blocksender类对象,然后通过该对象发送数据。Blocksender发送的数据是以一定的结构组织的。Blocksender发送数据的格式包括两个部分:校验信息头和数据包序列。
校验信息头包括两个部分:数据校验类型和校验块大小(这里以CRC32为例,一般情况下是512字节产生一个4字节的校验和,我们把512字节的数据称为一个校验块,及校验块是HDFS中读取写入数据块操作的最小单元)。

预读取
BlockSender在读取数据块之前,会先调用manageOsCache()方法执行预读取操作以提高读取效率。预读取操作就是将数据块文件提前读取到操作系统的缓存中,这样当BlockSender到文件系统中读取数据块文件时,可以直接从操作系统的缓存中读取数据,这样比直接从磁盘上读取快得多。但是操作系统的缓存空间是有限的,所以需要调用manageOsCache()方法将不再使用的数据从缓存中丢弃,为新的数据挪出空间。

发送数据块—sendBlock()
BlockSender.sendBlock()方法用于读取数据以及校验和,并将它们发送到接收方,整个发送流程可以分为以下几步:
1.在刚开始读取文件时,触发一次预读取,预读取部分数据到操作系统的缓存中。
2.构造缓冲区,也就是能容纳一个数据包的缓冲区。这里需要注意的是缓冲区的大小,也就是能容纳一个数据包的大小。对于两种不同发送数据包的模式transferTo和ioStream,缓冲区的大小是不同的。在TransferTo模式中,数据块文件时通过拷贝方式直接传输给客户端的,并不需要将数据写入到缓冲区中,所以缓冲区只需要缓冲校验数据即可;而ioStream模式则需要将实际的数据以及校验数据都缓冲下来,所以在这两种发送数据包模式下,缓冲区的大小是完全不同的。
3.循环调用sendPacket()方法发送数据包序列,直到所有的数据包都发送完毕。
4.发送一个空的数据包,用以标识数据块的结束。
5.完成数据块的发送工作后,调用close()方法关闭数据块以及校验文件,并从操作系统的缓存中删除已读取数据。

零拷贝数据传输

Datanode最重要的功能之一就是读取数据块,这个操作看似简单,但是在操作系统层面需要4个步骤才能完成。
这里写图片描述
1.Datanode会首先将数据块从磁盘存储(也可能是SSD、内存等异构存储)读入操作系统的内核缓冲区。
2.再将数据跨内核推到用户缓冲区
3.然后会再次跨内核将数据推回内核中的套接字缓冲区
4.最后将数据写入网卡缓冲区。
我们可以看到Datanode对数据进行了两次多余的操作(步骤2和3),Datanode只是起到缓冲数据并将其传回套接字的作用,别无它用。
步骤1和4的操作发生在外设和内存之间,由DMA(Directory Memory Access,直接内存读取)引擎执行,而步骤2和3的拷贝发生在内存中,由CPU执行。

操作系统之所以引入内核缓冲区,是为了提高读写性能。在读操作中,如果应用程序所需的数据量小于内和缓冲区大小时,内核缓冲区可以预读取部分数据,从而应用程序的读效率。在写操作中,引入中间缓冲区则可以让写入过程异步完成。
而对于Datanode,由于读取的数据块文件往往比较大,引入中间缓冲区可能成为一个性能瓶颈,造成数据在磁盘、内和缓冲区和用户缓冲区中被拷贝多次。

上述读取方式除了会造成多次数据拷贝之外,还会增加内核态和用户态之间的上下文切换。
这里写图片描述
我们可以看到这个简单的读取操作,会造成4次用户态与内核态之间的上下文切换。

幸运的是,javaNIO提供了零拷贝模式来消除多余的拷贝操作,并且减少内核态与用户态之间的上下文切换。使用零拷贝的应用程序可以要求内核直接将数据从磁盘文件拷贝到网卡缓冲区,从而大大提高了程序的性能。数据拷贝次数从4次降低到了2次,上下文切换次数也从4次降低到了2次。
这里写图片描述
这里写图片描述
由于数据不再经过Datanode中转,而是直接在内核中完成了数据的读取与发送,所以大大提高了读取效率。
但是这样也带了一个问题,由于数据不经过Datanode内存,所以Datanode失去了在Datanode端读取数据块的过程中对数据校验的能力。为了解决这个问题,HDFS将数据块的读取操作放在了客户端执行,客户端就完成校验工作之后,会将校验结果发送回Datanode。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值