本文介绍了HDFS的短路读演进、安全的短路读以及小米在安全短路读的优化。
上篇文章回顾: HDFS Decommission问题分析
Hadoop的一个重要思想就是移动计算,而不是移动数据。咱们更愿意尽量将计算移动到数据所在节点。所以,HDFS中常常出现客户端和数据在一个节点上,当客户端读取一个数据块时,就会出现本地读取。例如HBase场景,ResionServer写数据通常在HDFS中都会存储三备份副本而且确定会往本地节点写一备份,当ResionServer读取该数据时也会优先选择同一节点的数据进行读取。node
最初,HDFS中本地读取的处理方式和远程读取相同,也是经过网络读来实现。客户端经过TCP套接字链接到DataNode,并经过DataTransferProtocol协议传输数据(以下图):缓存
这种方式简单,但有明显的问题:安全
DataNode必须为每一个正在读取数据块的客户端保留一个线程和一个TCP套接字。内核中会有TCP协议的开销,以及DataTransferProtocol协议的开销,所以这里有很大的优化空间。网络
短路读关键思想是由于客户端和数据块在同一节点上,因此DataNode不须要出如今读取数据路径中。而客户端自己能够直接从本地磁盘读取数据。这样会使读取性能获得很大的提升。在HDFS-2246中实现的短路读是DataNode将全部数据块路径的权限开放给客户端,客户端直接经过本地磁盘路径来读取数据,见下图:架构
但这种方式引入了不少问题:dom
(1)系统管理员必须更改DataNode数据目录的权限,以容许客户端打开相关文件。将可以使用短路读的用户专门列入白名单,不容许其余用户使用。一般,这些用户也必须放在特殊的Unix组中。异步
(2)这些权限更改会引入了一个安全漏洞,具备读取DataNode节点上数据块文件权限的用户能够任意读取路径上全部数据块,而不只仅是他们所需访问的数据块,这好像让用户变成了超级用户,对于少数用户来讲多是能够接受的,例如HBase用户。但总的来讲,它会带来很大安全隐患。socket
HDFS-2246的主要问题是向客户端打开了DataNode的数据目录,而咱们真正须要读取的只是一部分数据块文件。Unix有一种机制是能够作到这一点,称为文件描述符传递。HDFS-347使用这种机制来实现安全短路读。DataNode不是将目录传递给客户端,而是打开块文件和元数据文件,并将它们的文件描述符经过domain socket传递给客户端(如图3):函数
基于如下两方面,安全短路读解决了HDFS-2246存在的安全性问题。oop
(1)文件描述符是只读的,所以客户端没法修改传递描述符的文件。
(2)客户端没法访问数据块目录自己,因此也没法读取它不该该访问的任何其余数据块文件。
了解了HDFS短路读的演进,咱们来看下HDFS是如何实现安全短路读的。DataNode将短路读副本的文件描述符传给DFSClient,DFSClient缓存副本文件描述符。因为副本的状态可能随时发生改变,因此须要DFSClient和DataNode实时同步副本状态。同时,DFSClient和DataNode在同一台机器上,共享内存能够经过POSIX提供的 mmap接口实现将文件映射到内存,而且映射数据是实时同步的(如图4),因此共享内存能够维护全部短路读副本的状态,使得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,见下图:
当客户端执行数据块副本短路读时,DFSClient与DataNode的交互过程以下:
(1)DFSClient经过requestShortCircuitShm()接口向DataNode请求建立共享内存,DataNode建立共享内存文件并将共享内存文件描述符返回给DFSClient。
(2)DFSClient经过allocShmSlot()接口申请共享内存中的槽位,并经过requestShortCircuitFds()接口向DataNode请求要读取的副本文件描述符,DataNode打开副本文件并将数据块文件和元数据文件的文件描述符返回给DFSClient。
(3)DFSClient读取完副本后,异步经过releaseShortCircuitFds()接口向DataNode请求释放文件描述符及相应槽位。
几回压力场景中,咱们发现Hbase ResionServer多个短路读线程常常会阻塞在domain socket的读写上。从DataNode 的dump中发现大量的用于短路读的ShortCircuitShm。因而咱们经过YCSB模拟线上的状况,发现短路读请求量较大时,BlockReaderLocal分配的QPS很高,而且BlockReaderLocal的分配依赖于同步读取块的ShortCircuitShm和slot的分配。
经过统计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%左右。
在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对短路度影响,使得延迟更为平滑。
禁止了正在构建块进行短路读,也就是最后一个块禁止短路读。这个问题因为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操做系统的标准组件。
Unix domain socket的API相似于网络socket,可是不使用底层网络协议,全部通讯都彻底在操做系统内核中进行。Unix domain socket使用文件系统做为其地址名称空间。进程引用Unix domain socket做为文件系统inode,所以两个进程能够经过打开相同的socket进行通讯。 除了发送数据外,进程还可使用sendmsg()和recvmsg()系统调用在Unix domain socket链接上发送文件描述符。而且只有发送进程受权给接收进程,接收进程才能够访问文件描述符的权限。
(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/