HDFS短路读详解
Posted 小米技术
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HDFS短路读详解相关的知识,希望对你有一定的参考价值。
本文介绍了HDFS的短路读演进、安全的短路读以及小米在安全短路读的优化。
上篇文章回顾:
Hadoop的一个重要思想就是移动计算,而不是移动数据。我们更愿意尽可能将计算移动到数据所在节点。因此,HDFS中经常出现客户端和数据在一个节点上,当客户端读取一个数据块时,就会出现本地读取。例如HBase场景,ResionServer写数据一般在HDFS中都会存储三备份副本并且肯定会往本地节点写一备份,当ResionServer读取该数据时也会优先选择同一节点的数据进行读取。
1、网络读
最初,HDFS中本地读取的处理方式和远程读取相同,也是通过网络读来实现。客户端通过TCP套接字连接到DataNode,并通过DataTransferProtocol协议传输数据,如下图:
这种方式简单,但有明显的问题:
DataNode必须为每个正在读取数据块的客户端保留一个线程和一个TCP套接字。内核中会有TCP协议的开销,以及DataTransferProtocol协议的开销,因此这里有很大的优化空间。
2、HDFS-2246 不安全短路读
短路读关键思想是因为客户端和数据块在同一节点上,所以DataNode不需要出现在读取数据路径中。而客户端本身可以直接从本地磁盘读取数据。这样会使读取性能得到很大的提高。在HDFS-2246中实现的短路读是DataNode将所有数据块路径的权限开放给客户端,客户端直接通过本地磁盘路径来读取数据,见下图:
但这种方式引入了很多问题:
(1)系统管理员必须更改DataNode数据目录的权限,以允许客户端打开相关文件。将能够使用短路读的用户专门列入白名单,不允许其他用户使用。通常,这些用户也必须放在特殊的Unix组中。
(2)这些权限更改会引入了一个安全漏洞,具有读取DataNode节点上数据块文件权限的用户可以任意读取路径上所有数据块,而不仅仅是他们所需访问的数据块,这好像让用户变成了超级用户,对于少数用户来说可能是可以接受的,例如HBase用户。但总的来说,它会带来很大安全隐患。
3、HDFS-347 安全短路读
HDFS-2246的主要问题是向客户端打开了DataNode的数据目录,而我们真正需要读取的只是一部分数据块文件。Unix有一种机制是可以做到这一点,称为文件描述符传递。HDFS-347使用这种机制来实现安全短路读。DataNode不是将目录传递给客户端,而是打开块文件和元数据文件,并将它们的文件描述符通过domain socket传递给客户端,如下图:
基于以下两方面,安全短路读解决了HDFS-2246存在的安全性问题。
(1)文件描述符是只读的,因此客户端无法修改传递描述符的文件。
(2)客户端无法访问数据块目录本身,所以也无法读取它不应该访问的任何其他数据块文件。
1、短路读共享内存
了解了HDFS短路读的演进,我们来看下HDFS是如何实现安全短路读的。DataNode将短路读副本的文件描述符传给DFSClient,DFSClient缓存副本文件描述符。由于副本的状态可能随时发生改变,所以需要DFSClient和DataNode实时同步副本状态。同时,DFSClient和DataNode在同一台机器上,共享内存可以通过POSIX提供的 mmap接口实现将文件映射到内存,并且映射数据是实时同步的(如下图),所以共享内存可以维护所有短路读副本的状态,使得DFSClient和DataNode通过共享内存来实时同步副本信息。
共享内存会有很多槽位,每个槽位对应一个短路读副本的信息。共享内存保存了所有槽位的二进制信息。但是映射数据中的二进制槽位信息不便于管理,所以定义了Slot对象操作映射数据中的一个槽位,如下图:
Slot槽位大小是64字节,槽位数据格式,前4字节是Slot标志位,5到8字节是锚计数位,剩余字节保留将来使用,例如统计信息等。
两个标志位:
(1)VALID_FLAG:表示槽位是否有效。
DFSClient在共享内存中分配新的槽位时设置此标志位。当与此槽位关联的副本不再有效时,DataNode将会消除此标志位。DFSClient本身也会消除此槽位,认为DataNode不再使用此槽位进行通信。
(2)ANCHORABLE_FLAG:表示槽位对应的副本是否已经缓存。
DataNode将槽位对应的副本通过POSIX提供的mlock接口缓存时会设置该标志位。当标志位已设置,DFSClient短路读取该副本时不再需要进行校验,因为副本缓存时已经做了检验操作,并且这种副本还支持零拷贝读取。DFSClient对这样的副本进行读取时,需要在对应的槽位锚计数加1,只有当槽位的锚计数为0时,DataNode才可以从缓存中删除此副本。
共享内存段的最大是8192字节,当DFSClient进行大量短路读时, DFSClient和DataNode之间可能会有多段共享内存。HDFS中DFSClient定义了DFSClientShm类抽象了DFSClient端一段共享内存,DFSClientShmManager类管理所有的DFSClientShm,而DataNode端定义了RegisteredShm类抽象DataNode端的一段共享内存,ShortCircuitRegistry类管理所有DataNode端的共享内存,如下图所示:
在安全短路读中,DFSClient和DataNode是通过domain socket来同步共享内存槽位信息的。
DFSClient申请一段共享内存保存短路读副本的状态。DataNode会创建共享内存,并将共享内存文件映射到DataNode内存中,并创建RegisteredShm管理这段共享内存,之后会将共享内存文件的文件描述符通过domain socket返回给DFSClient。
DFSClient根据文件描述符打开共享内存文件,将该文件映射到DFSClient的内存中,并创建DfsClientShm对象管理这段共享内存。
DFSClient通过domain socket向DataNode申请数据块文件以及元数据文件的文件描述符,并且同步共享内存中slot槽位的状态。DFSClient会在DfsClientShm管理的共享内存中为数据块申请一个slot槽位,之后通过domain socket向DataNode同步信息,DataNode会在RegisteredShm管理的共享内存中创建相应的slot槽位,然后获取数据块文件以及元数据文件的文件描述符,并通过domain socket发送给DFSClient,如下图:
共享内存机制
2、短路读流程
当客户端执行数据块副本短路读时,DFSClient与DataNode的交互过程如下:
短路读简化流程图
(1)DFSClient通过requestShortCircuitShm()接口向DataNode请求创建共享内存,DataNode创建共享内存文件并将共享内存文件描述符返回给DFSClient。
(2)DFSClient通过allocShmSlot()接口申请共享内存中的槽位,并通过requestShortCircuitFds()接口向DataNode请求要读取的副本文件描述符,DataNode打开副本文件并将数据块文件和元数据文件的文件描述符返回给DFSClient。
(3)DFSClient读取完副本后,异步通过releaseShortCircuitFds()接口向DataNode请求释放文件描述符及相应槽位。
1、Slot槽位释放缓慢
几次压力场景中,我们发现Hbase ResionServer多个短路读线程经常会阻塞在domain socket的读写上。从DataNode 的dump中发现大量的用于短路读的ShortCircuitShm。于是我们通过YCSB模拟线上的情况,发现短路读请求量较大时,BlockReaderLocal分配的QPS很高,并且BlockReaderLocal的分配依赖于同步读取块的ShortCircuitShm和slot的分配。
YCSB GET QPS
YCSB GET LATENCY
通过统计slot分配和释放的QPS,我们发现slot分配的QPS能达到3000+,而释放的QPS只能达到1000+,并且在YCSB测试经过约1小时,DataNode出现FULL GC。由此可看出,DataNode中积累的,来不及释放的slot,是导致GC的主要有原因。
现在的短路读实现中,每次释放slot,都会新建一个domain socket连接。而DataNode对于每个新建立的domain socket 连接,都会重新初始化一个DataXceiver去处理这个请求。通过profile DataNode发现,SlotReleaser线程花了大量的时间在建立和清理这些连接上。
于是,我们对SlotReleaser的domain socket连接进行了复用。通过复用domain socket,在同样的测试集上,slot 释放的QPS能和分配的QPS达到一致。从而消除了过期slot在DataNode中的挤压。同时,由于DataNode Young GC减少,YCSB的GET的QPS也提升了约20%左右。
2、共享内存分配效率低
在profile HBase短路读过程中,我们还发现另外一个问题,就是每隔一段时间,会有一批读会有约200ms左右的延迟而且这些延迟几乎同时出现。开始我们怀疑 Hbase ResionServer的Minor GC导致。但通过比对ResionServer的GC日志,发现时间并不完全匹配。有一部分却和DataNode Minor GC时间吻合。数据块副本的真正读取操作,是完全不通过DataNode的,如果是DataNode的影响,那问题只能出在ResionServer和DataNode建立短路读时的交互上。通过进一步在短路读过程中加trace log,我们发现这些延迟,是由于DataNode Minor GC导致ShortCircuitShm分配请求被阻塞。当分配一个ShortCircuitShm时,会导致很多slot的分配阻塞。slot的分配延迟,又会引起BlockReaderLocal的延迟,从而导致短路读的延迟。这就是之前发现有一批读,总是同时报相近的延迟。 为了解决这个问题,我们对ShortCircuitShm进行了预分配,以减轻DataNode Minor GC对短路度影响,使得延迟更为平滑。
3、短禁止正在构建块的短路读
禁止了正在构建块进行短路读,也就是最后一个块禁止短路读。这个问题由于HBase的读写模式 ,对其影响不是很大,但对基于HDFS流式服务影响很大。我们正在做的优化工作主要是通过保证短路读只发生在flush操作之后,同时在读取过程中检查块的有效性,处理读异常,如果确实失败,转成远程读的方式。
(1)文件描述符(File Descriptor)
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
Unix和Windows系统都允许在进程间传递文件描述符。一个进程打开一个文件,然后把该文件的文件描述符传递给另一个进程,另一个进程可以访问该文件。文件描述符传递对于安全性是非常有用的,因为它消除了对第二个进程需要拥有足够访问权限来打开文件限制,同时文件描述符是只读的,所以该方式还可以防止有问题的程序或者恶意的客户端损坏文件。在Unix系统上,文件描述符传递只能通过Unix domain socket完成。
(2)Unix domain socket
Unix domain socket是用于在同一台主机操作系统上执行的进程间交换数据的通信端点。有效的Unix domain socket类型是SOCK_STREAM(用于面向流的套接字)和SOCK_DGRAM(用于保留消息边界的面向数据报的套接字),与大多数Unix实现一样,Unix domain datagram socket始终可靠且不重新排序的数据报。Unix domain socket是POSIX操作系统的标准组件。
(3)共享内存(Shared Memory)
共享内存是进程间通信的方法,即在同时运行的程序之间交换数据的方法。一个进程将在RAM中创建一个其他进程可以访问的区域。由于两个进程可以像访问自身内存一样访问共享内存区域,因此是一种非常快速的通信方式。但是它的扩展性较差,例如通信必须在同一台机器上运行。而且必须要避免如果共享内存的进程在不同的CPU上运行,并且底层架构不是缓存一致的。
POSIX提供了使用共享内存的POSIX标准化API。使用sys/mman.h中的函数shm_open。POSIX进程间通信包含共享函数shmat,shmctl,shmdt和shmget。shm_open创建的共享内存是持久化的。它一直保留在系统中,直到被进程明确删除。这有一个缺点,如果进程崩溃并且无法清理共享内存,它将一直保持到系统关闭。POSIX还提供了用于将文件映射到内存的mmap API,可以共享映射,允许将文件的内容用作共享内存。
1. https://en.wikipedia.org/wiki/File_descriptor
2. https://en.wikipedia.org/wiki/Unix_domain_socket
3. https://en.wikipedia.org/wiki/Shared_memory
4. https://www.tutorialspoint.com/inter_process_communication/inter_process_communication_shared_memory.htm
5. https://blog.cloudera.com/blog/2013/08/how-improved-short-circuit-local-reads-bring-better-performance-and-security-to-hadoop/
以上是关于HDFS短路读详解的主要内容,如果未能解决你的问题,请参考以下文章