增强RocksDB的速度和规模
Mikhail Bautin 【hudson译】
创始人&软件架构师 2019年2月20日
*这篇博文由Mikhail Bautin和Kannan Muthukkaruppan*共同撰写
正如我们在上一篇文章“我们如何在RocksDB上构建高性能文档存储?”文中所述,YugabyteDB的分布式文档存储(DocDB)使用RocksDB作为其每节点存储引擎。在将RocksDB嵌入DocDB的文档存储层的过程中,我们对其进行了多项性能和数据密度相关的增强 (下图)。这些增强功能作为YugabyteDB开源项目的一部分发布。这篇文章的目标是深入探讨这些增强功能,以帮助有兴趣利用RocksDB的工程团队超越其最初的设计意图,即快速单体键值存储。
DocDB的存储层
抗扫描缓存
我们增强了RocksDB的块缓存以变得抗扫描。这防止了长时间运行扫描等操作(例如,由于偶尔的大型查询或后台Spark作业)用低质量数据污染整个缓存并淘汰有用/热数据。新的块缓存使用类似MySQL/HBase中间点插入策略,将LRU分为两个部分,并且在将块提升到高速缓存的多点触摸/热部分之前需要多次命中。
基于块的布隆过滤器/索引数据拆分
RocksDB的SSTable文件包含数据和元数据,如索引和布隆过滤器。SSTable文件的数据部分已分块(默认为32KB)并按需分页。 然而,布隆过滤器和索引部分是单体的,要么全部读入内存,要么根本不读入内存。对于大型数据集,这给内存需求带来了不必要的压力,同时也造成了碎片。 我们增强了RocksDB索引和布隆过滤器,这些元数据现在基于多层次的块结构组织,可以像数据块一样按需分页到块缓存中。这使得YugabyteDB能够高效利用RAM,友好分配内存,来支持非常大的数据集。
单节点多RocksDB实例
DocDB自动将表分割成多个tablet。它为每个tablet单独分配一个RocksDB实例,而不是在一个节点上多个tablet共享一个RocksDB实例。
设计的好处
-
节点故障或节点添加时的集群重新平衡变得非常高效,因为正在重新平衡的tablet的SSTable文件可以直接(以压缩形式)从tablet领导者处复制。与多个tablet共享一个RocksDB实例的方案不同,不需要对SSTable文件进行逻辑扫描或拆分。而且无需等待压缩从分片之前所在的节点回收存储!
-
删除表就像删除相关的RocksDB实例一样简单。
-
允许基于表的存储策略
- 磁盘压缩-开/关?哪种压缩算法?等
- 内存增量编码方案——例如前缀压缩或差异编码
-
允许基于表的布隆过滤器策略。对于具有复合主键的表(例如,
(<userid>,<message id>)),键的哪些部分被添加到布隆过滤器取决于要优化的访问模式。如果应用程序查询通常只提供复合键的一部分(例如,“<用户id>”),则将整个复合键添加到布隆过滤器中是无用的,纯粹是的开销。 -
允许DocDB跟踪表主键中每个聚簇列的最小/最大值(RocksDB的另一个增强功能),并将其作为元数据存储在相应的SSTable文件中。这使YugabyteDB能够优化范围谓词查询,如通过减少需要查找的SSTable文件的数量:
SELECT metric_val
FROM metrics
WHERE device=? AND ts < ? AND ts > ?
RocksDB已经允许在单个进程中运行多个实例。然而,在实践中,以这种形式使用RocksDB需要精心设计和增强。我们在这里列出其中一些重要的部分。
服务器全局块缓存
DocDB使用RocksDB服务器上跨越所有实例的共享块缓存,避免了每个tablet 的缓存孤岛,并提高了内存的有效利用率。
服务器全局Memstore限制
RocksDB允许配置每个memstore刷新大小。这在实践中是不够的,因为当用户创建新表时,memstore的数量可能会随着时间而变化,或者由于负载平衡,表中的tablet在服务器之间移动。为每个memstore刷新大小选择非常小的值会导致过早刷新并增加写入放大。另一方面,对于具有很多tablet的节点,选择非常大的memstore刷新大小会增加系统的内存需求和恢复时间(在服务器重启的情况下)。 为避免这些问题,我们增强了RocksDB以强制执行全局memstore限制。当所有memstore使用的内存达到此限制时,将刷新具有最早记录(使用混合时间戳确定)的memstore。
分离大型和小型压缩队列
RocksDB支持具有多个压缩线程的单个压缩队列。但这会导致一些大型压缩(根据读取/写入的数据量)在所有这些线程上调度, 而小型压缩得不到调度。由此导致存储文件过多、写入暂停和高延迟的读取。 我们增强了RocksDB以允许多个队列,队列个数基于总输入数据文件大小,由此可基于大小对压缩进行优先级排序。队列由可配置数量的线程提供服务,保留其一定数量子集用于小型压缩,因此对于任何tablet,SSTable文件的数量不会增长太快。
跨多个磁盘的智能负载平衡
DocDB支持多个SSD的单组磁盘(JBOD)设置,不需要硬件或软件RAID。不同tablet的RocksDB实例在所有可用的SSD上统一平衡,以确保每个SSD具有来自每个表的类似数量的tablet,并接受统一类型的负载。DocDB中的其他类型的负载平衡也基于单个表,可以是:
- 跨节点平衡tablet副本
- 跨节点平衡Raft组的领导者/跟随者
- 节点上跨SSD的Raft日志平衡
其他优化
正如我们上一篇文章所指出的那样,DocDB在整个群集级别(而不是每个节点存储引擎级别)管理事务处理、复制、并发控制、数据生存时间(TTL)和恢复/故障切换机制。因此,RocksDB提供的一些等效功能变得不必要,并被删除。
避免预写日志(WAL)双重写
DocDB使用Raft共识协议进行复制。分布式系统的更改,如行更新,已经作为Raft日志的一部分进行记录并形成日志。RocksDB中的额外WAL机制是不必要的,只会增加开销。 DocDB通过禁用RocksDB WAL避免了这种双重日志记录,并以Raft日志作为事实来源。它跟踪Raft“序列ID”,此ID前数据已从RocksDB memtables刷新到SSTable文件。这确保了我们可以正确地对Raft日志进行垃圾回收,以及在服务器崩溃或重启时重放Raft WAL日志中的最小数量的记录。
多版本并发控制(MVCC)
DocDB在整个集群级别上管理RocksDB之上的MVCC。系统中记录的改变使用混合时间戳进行版本控制。因此,在普通RocksDB中,通过序列ID实现MVCC的概念增加了开销。因此,DocDB不使用RocksDB的序列ID,而是使用混合时间戳作为键编码的一部分来实现MVCC。
混合逻辑时钟(HLC)是一种混合时间戳赋值算法,用于在分布式系统中分配时间戳,使得每对“因果连接”事件都会导致时间戳值的增加。更多细节请参考这些报告#1或#2
总结
DocDB是强力支持YugabyteDB的分布式文档存储。RocksDB是一种广泛使用的快速嵌入式存储引擎,其日志结构合并树(LSM)设计和C++实现,是选择其作为DocDB的单节点存储引擎的关键因素。YugabyteDB管理的每一行(数据)都存储为DocDB中的一个文档,在内部映射到RocksDB中的多个键值对。
当我们开始在DocDB中构建文档存储层时,我们意识到需要对RocksDB进行重大增强。这是因为每个RocksDB实例不再独立运行。它需要与同一节点上的其他RocksDB实例共享资源。此外,我们需要确保每个DocDB的tablet(映射到其自己专有RocksDB实例)可以增长到任意大,而不会影响其他tablet(以及相关的RocksDB实例)的性能。我们希望本文中揭示的增强功能能够帮助其他工程团队在RocksDB之上解决类似的性能和可伸缩性挑战。