You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ListDB 可在不恢复缓存的情况下处理客户端查询(只是在重新填充缓存前性能较差)。通过避免重构 DRAM 缓存和索引,ListDB 的恢复性能优于同步 Checkpoint。
ScheduleUnfinishedZipperCompactionJob();
curr_table <- NULL;
while log_iter.Valid() do
iul_entry <- log_iter.GetIULEntry();
table_id <- GetTableIdByLSN(iul_entry.LSN);
if curr_table = NULL || table_id 6= curr_table.Id() then
curr_table <- MANIFEST.GetTableById(table_id);
curr_table.ResetSkipListHead();
end if
curr_table.InsertEntry(iul_entry); /* 插入跳表 */
log_iter.Next(); /* 从旧到新 */
end while
This discussion was converted from issue #3 on June 09, 2023 03:14.
Heading
Bold
Italic
Quote
Code
Link
Numbered list
Unordered list
Task list
Attach files
Mention
Reference
Menu
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
TL; DR
论文发布于 2022 USENIX OSDI,其介绍了一种为 NVMM 实现写优化的 K-V 存储系统。
首先在摘要部分交代了技术背景:
DRAM 和 NVMM 之间存在延迟,DRAM 容量也有限。基于 LSM-tree 的 K-V 存储系统写操作普遍会存在停顿。
为此 ListDB 贡献了三项技术来解决这个问题:
最终达到的效果是写吞吐量比 PACTree 高 1.6 倍,比 RocksDB(Intel Pmem based)高 25 倍。
介绍
一些 K-V 存储系统在定位检索大数据集时结合 NVMM 特征的采用高效的持久化索引结构。
比如混合 DRAM + NVMM:由 DRAM 负责检索可发挥其性能优势、避免 NVMM 短板。
但作者质疑这种方案提高性能的可行性:
ListDB 是一种写优化的 LSM-tree 存储,其实现:
研究表明 ListDB 写性能比最新的、基于 NVMM 的 K-V 存储性能更高;读性能则依赖于传统缓存技术。
背景与设计动机
混合 DRAM + NVMM K-V 存储
基于 NVMM 的索引在持久化时会使用 内存屏障 指令避免缓存污染、确保失败时的原子性和一致性,因此性能受损较大。
过往的解决思路:
但是生成 Checkpoint 相当于产生快照,会阻塞写入,有较大的高尾延迟。
LSM-tree
生成 Checkpoint 更好的方法是 异步、增量 地生成,即只处理当前 Checkpoint 与上一个 Checkpoint 之间的差异。
LSM-tree(Log-Structured Merge Tree)是一种经典的索引实现,随着时间推移不断整合 Checkpoint 数据。
多层压缩与两层压缩
传统压缩是基于数据复制的,这允许并发读取 SSTable、同时写入新的 SSTable。由于需要复制相同的对象到新 SSTable,存在较大的 写放大(研究表明可高达 40,即一个 K-V 对象被复制 40 次)。
如果使用分层压缩,当层次数量很大则会加剧写放大(level 限制了每层 SSTable 数量,且避免单层数据重叠)。
NVMM 支持字节寻址,可使用单层持久化索引代替多层:比如 SLM-DB 是一层 MemTable、一层 B+ tree,缓冲了多个 K-V 对象后插入一个更大的持久性索引。多次写操作只需遍历一次大索引,而写吞吐量更高。
这种设计的主要问题是 MemTable 归并排序并写入一个很大的持久性索引,由于 NVMM 延迟较高,数据量会影响易失性索引合并到持久化索引的性能。
从 Flush 中解耦归并排序
为了缓解上面的问题,LevelDB 和 RocksDB 引入一个中间 buffer。
利用这个 L0 buffer 可以把 flush 与归并排序分离开,MemTable 可以更快地 flush 到 NVMM,其中 flush 操作的吞吐量可与数据库大小分离。
但这种设计也存在问题:导致了更多重叠的 SSTable,降低了写性能。
K-V 对象写入存储共发生两次:WAL + MemTable 的 flush。
解决方法:
NUMA 效应
相比起 DRAM,NVMM 带宽更低、对 NUMA 效应更敏感。
由于不规则的缓存访问机制和 NUMA 效应影响下,FAST and FAIR B+ tree、CCEH 这些持久化索引的 多线程扩展性 不太好。
缓解 NUMA 效应的方法:
ListDB 设计
三层架构
即 Volatile MemTables、L0 Persistent MemTables、L1 Persistent MemTables(PMTable)。
索引统一日志(Index-Unified Logging)
索引统一日志(Index-Unified Logging, IUL)指的是以 跳表 分配和写入日志(WAL 的结构也统一为跳表)。
使用 IUL,MemTable 刷写到 NVMM 时,MemTable 中的 K-V 对象已保存在 NVMM 中的 commit log 中,无需复制 K-V 对象。
统一为以上的结构后,K-V 对象实际写入 NVMM 只发生一次。
复用日志和跳表元素
顺便给出文中的伪代码描述:
由于 K-V 对象已经被持久化到日志中,把跳表指针写入日志项时无需(执行 clflush 指令来完成)持久化,可利用 CPU 缓存替换机制实现数据的批量刷写:不仅推迟了 read-modify-write 的问题,还避免了后台压缩线程受到 read-modify-write 问题以及 NVMM 高延迟写的影响。
Checkpointing
当日志项被转换为 L0 PMTable 元素,此时还不能保证 L0 PMTable 已完成持久化。
只有当更新指针显式调用 clflush 指令时,WAL 日志空间和 PMTable 之间的边界才会移动。
虽然 Checkpoint 可以缩短崩溃恢复的时间,由于调用 clflush 开销太大,ListDB 会尽量推迟 Checkpoint。
所谓「惰性」、「分组」的操作(Lazy Group Checkpointing),就是对多个 L0 PMTable 分组处理,批量地把缓存持久化;并通过降低 Checkpoint 频率来缩短刷写时间。但随着日志变大,恢复速度也会变慢,因此这也是在 写吞吐量(压缩时间)与恢复时间之间权衡。
下文中提到基于 Zipper Compaction 来持久化指针的速度非常快,以至于可减少 L0 PMTable 的增长。(即使 IUL 没有持久化 L0 PMTable)将 L0 PMTable 合并到 L1 PMTable 时,压缩持久化指针的速度很快,而且 IUL 恢复时间比同步的 Checkpoints 短很多,因此可减少 L0 PMTable。
NUMA 效应与跳表
对于跳表而言,每层链表都是最底层链表的有序子集,而根据概率选出上层指针不会影响查询结果,因此上层链表只需要是底层链表的子集。
Braided SkipList 是一种特殊的跳表,可用于减小 NUMA 效应:
跳表每个元素的上层指针指向同一个 NUMA 节点中、更大 key 的元素。与传统实现相比,Braided SkipList 可有效减少 NUMA 节点内存访问次数(减少到 1/N)。
拉链压缩(Zipper Compaction)
通过修改指针来归并排序 L0 PMTable 和 L1 PMTable,不阻塞并发读;而本地操作则避免写放大,从而提升压缩的吞吐量。
无锁搜索(Lock-Free Search)
部分研究提出可使用无锁跳表(比如 Java ConcurrentSkipListMap)避免对多个写线程加锁。
写操作:ListDB 的写线程是压缩线程,ListDB 会协调这些线程避免写写冲突,多个压缩线程写入不相交的分片实现并行。
读操作:拉链压缩不会影响并发搜索结果的正确性,读线程不会在不获取锁的情况下错过目标元素:
在压缩线程修改跳表时,读线程会被挂起,但搜索结果仍然正确:
更新与删除
LSM-tree 的写操作会缓冲到 MemTable 中,并逐渐刷新到最后一层 PMTable,在此过程会出现相同的 key。
ListDB 不会主动删除 L1 PMTable 中的旧版本:执行压缩时,压缩线程扫描 L0 PMTable 和 L1 PMTable,将标记 L1 PMTable 中的旧版本为过期。K-V 对象也是同理,删除时是将一个 K-D(即删除标记)插入到 MemTable 中。如果 LSM-tree 从 MemTable 或 L0 PMTable 中物理删除 key 的最新版本,则旧版本的 key 将恢复使用。
压缩时会将较新的 K-V 对象或 K-D 对象放在其对应的旧对象之前,读取总是在旧对象之前访问最近的对象来返回结果。
内存碎片与垃圾收集
当一个内存块所有元素都标记为过期或删除,则释放内存块。由于 ListDB 不会重新定位跳表元素进行垃圾收集,就需要解决内存碎片的问题(压缩线程可使用 COW 的策略,待后续优化)。
使用无锁数据结构,难以判断待释放的内存空间是否仍被并发读取。ListDB 采用基于 epoch 的回收策略:等待足够长的时间,让读请求完成对待释放内存块的访问。当内存块中的所有对象都过期或删除,垃圾收集线程则定期检查并回收内存块。垃圾收集线程检查过时的对象的新版本,当该版本也足够旧、可认为没有再被访问,可从 L1 PMTable 中删除,最终释放物理内存。
线性一致性(Linearizability)
定理 1:在一写多读的场景下,拉链压缩是可线性化的。
拉链压缩是可线性化的,意味着当元素由写事务插入并已提交,无论该元素是在 L0 PMTable 还是 L1 PMTable 中,读取操作始终都能找到该元素。
证明过程见原文。
查找缓存(Look-up Cache)
在 ListDB 的实现中,读请求至少要访问 Mutable MemTable 和 L1 PMTable 这两个索引,读吞吐量低于 B+tree。引入查找缓存:
崩溃恢复(Recovery)
在 L0 PMTable 和 L1 PMTable 被压缩合并时系统可能会崩溃。
因此压缩线程会 记录日志(参考 Redo Log)跟踪被合并到 L1 PMTable 的 L0 PMTable。系统重新启动时 ListDB 检查压缩日志、重做未完成的压缩。在重做压缩时,由于很多 L0 PMTable 尾部的条目可与 L1 PMTable 共享,因此压缩时会检查重复的条目。
与传统 LSM-tree 相似,在崩溃恢复时,恢复线程定位(由压缩线程记录在日志中的) WAL 边界,对日志条目排序并恢复 L0 PMTable。此时系统正常执行并处理查询请求,L0 PMTable 和 L1 PMTable 之间的压缩在后台继续进行。
ListDB 可在不恢复缓存的情况下处理客户端查询(只是在重新填充缓存前性能较差)。通过避免重构 DRAM 缓存和索引,ListDB 的恢复性能优于同步 Checkpoint。
评估对比
结论
本次论述了 ListDB 设计与实现的过程:这是一种利用字节寻址特性,通过本地重构数据和 NVMM 的高性能来避免数据复制,以减少写放大和写停顿的 K-V 存储系统。
ListDB 通过 异步增量 Checkpoint 和 本地复制 显著提高写性能。基于三层架构,ListDB 在写吞吐量方面表现得比目前最先进的持久性索引和基于 NVMM 的 K-V 存储系统更好;而通过查找缓存缓解了多层结构存在的问题。
作者提到在接下来的工作中将会探索搜索性能提高的方法:如通过引入 L2 PMTable,适时重排 L1 PMTable 元素以满足空间局部性和垃圾收集。
参考
Beta Was this translation helpful? Give feedback.
All reactions