HDFS 基于 Unix Socket 的短路读与中心化缓存的实现演进

在 SSD 集群的 HBase 中, 硬盘 I/O 可能可以已经可以支撑很高的 QPS,但基于 HDFS 短路读的 HBase 在实际测试中可能并不如预期,这是因为短路读在共享内存的分配与回收上,其 QPS 也是非常高且消耗资源的 。所以 ——

短路读为什么依赖共享内存?直接传递文件描述符不可以吗?

在展开后文之前,需要说明,基于 UNIX Socket 在多进程间传递文件描述符,传递行为需要使用 sendmsg() 与 recvmsg() 函数,并在发送时设置 cmsg_type = SCM_RIGHTS,这样内核才会执行文件描述符的复制,准确来说,是创建新的文件描述符编号,并且在进程表项中的打开文件表项中创建文件描述符编号与文件表的映射,把文件表的 vnode 指针指向源进程所发送的文件描述符对应的 vnode 指针,即本质上是复制 vnode 指针(Linux 系统由于省略了 vnode,所以复制的直接就是 inode 指针)。

再看回共享内存问题,我们可以在演进过程中见微知著:

13年1月,2.0.3-alpha 确实是通过 UNIX Socket 直接传递文件描述符实现短路读(HDFS-4356),并实现 FileInputStreamCache 客户端文件描述符缓存类(新版为 ShortCircuitCache 类),意在于打开的减少文件描述符的数量。

但与此同时,也埋入了一个 BUG。

14年1月,2.3.0 发布,集成中心化缓存完成(HDFS-4949),使用的是 mlock() 系统调用。同时为短路读实现了 Zero Copy Read,需要依赖上层 Java 应用是把数据读出到 Java 的堆外内存。

mlock(), mlock2(), and mlockall() lock part or all of the calling process’s virtual address space into RAM, preventing that memory from being paged to the swap area.

https://man7.org/linux/man-pages/man2/mlock.2.html

另外,在 mlock() 之前是需要 mmap(),把文件映射到内存虚拟地址,这样才能使用 mlock 的锁定虚拟地址到 RAM。

mmap() creates a new mapping in the virtual address space of the calling process. The starting address for the new mapping is specified in addr. The length argument specifies the length of the mapping (which must be greater than 0).

https://man7.org/linux/man-pages/man2/mmap.2.html

14年4月,2.4.0 发布,实时缓存的中心化缓存(HDFS-5182) 完成,这个大任务主要有两个子任务:实时通知客户端的实现(HDFS-5746),以及顺便重构客户端里的 mmap 及短路读文件描述符缓存管理统一为 ShortCircuitCache 类(HDFS-5810)。

值得一提的是,实时通知客户端的实现是由共享内存实现,共享内存本质就是一个文件,只是这个文件是保存于内存的,然后两个线程可以去同时读写这个文件,也能即时看到对方修改的内容。

通常,为短路读创建的共享内存,其文件名如,/dev/shm/HadoopShortCircuitShm_DFSClient_NONMAPREDUCE_xx,默认是创建 8192 字节,然后将分为单位 64 字节的 Slot,因此最大可分配 128 个 Slot,Slot 与 Block 一对一关联关系,Slot 记录 Slot Flags 以及锚计数器(其实就是中心化缓存启用后的 Read Counter,不启用不会修改),共 8 字节。

也就是说,假设现在有 20T 数据的 SSD 机器,HDFS 块大小在 128M,那么短路读打开这些文件需要创建 1280 个共享内存文件。而每一次读 Block 的时候,都得去文件中询问对应的 Slot 是否可以 ShortCircuitReplica#addNoChecksumAnchor,也就是锚计数器能不能加一,尽管没有开启中心化缓存(目的是为了当开启中心化缓存后,锚计数器能立即加一,以此知道该副本数据能否在以后驱逐出内存)。

14年7月,2.6.0发布,解决了文件描述符有效性缺陷(HDFS-6750),这个问题说明了短路读仅仅传递文件描述符是不足够的,因为客户端缓存了文件描述符,而一旦 DataNode 对该描述符标记删除后,客户端是无法感知的,还以为文件依旧在,出现了严重的一致性问题。解决方法是,借用了之前中心化缓存所实现的实时通知功能,也就是共享内存的做法,其实也很简单,删除时标记一下 Invalid 就可以了,而本来设计的 ShortCircuitCache#fetch 就会过滤掉 Invalid 的。

所以,短路读缓存了文件描述符的时候,就不得不依赖共享内存等的通知机制,以确保删除、追加的操作可见;而一旦短路读不缓存文件描述符,就会出现文件描述符分配与回收性能问题。

最后,由于我们只是之前在性能测试 SSD 集群的 HBase 才遇到这个问题,可以尝试:

  1. 小米提出的,在 Release 时复用 UNIX Socket(HDFS-13639,为了并发准确性,每次都创建新的 Unix Socket,事实上它很可能是线程安全的Are parallel calls to send/recv on the same socket valid?)。
  2. 直接取消共享内存,在 HBase 集群中,除了 RegionServer 进程读取 HFile,也就是只有一个客户端,因此即便没有共享内存的通知机制(根本没有其他人可以通知),也应该可以,但此方法并未经过长期测试。
  3. 直接减少 Slot 的单位大小(实际有效仅 8 字节,但访问一个 Block 根据预留的需求需要占用 64 字节)。
  4. 选择 BlockReaderLocalLegacy(但是 pread 是用 read + skip,应该凉凉)。

参考资料

  1. 《Hadoop 2.X HDFS源码剖析》,徐鹏
  2. https://blog.csdn.net/pengzhouzhou/article/details/90776682
  3. https://switch-router.gitee.io/blog/scm_rights/

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注