CMU 15445
Storage Manager
DBMS 底层是如何存储数据的?
BusTub 中有基于内存的存储后端(可以设定读写延时)和基于文件系统的存储后端 DiskManager。DiskManager 支持 Page 粒度的读写, 使用链表或字典组织页面(BusTub 中没有 Page 回收的逻辑,使用数组)。
Page Header 包含以下信息:
- Page Size
- Checksum(校验数据存储的正确性)
- DBMS Version
- Transaction Visibility(事务可见性)
- Compression Information
Page Data 部分的布局,通常采用 Slotted Pages 或 LSM,Slot 从页的 Data 区域的最前面往后存,Tuple 是从 Data 区域的最后面往前存,Tuple 之间可以是乱序的,Slot 的数据可以动态更新,简化了 Tuple 存储的方式。
BusTub 中没有使用 Direct I/O 的原因与影响?
课上提到,大部分数据库管理系统使用 Direct I/O 绕过内核的 Page Cache,但 BusTub 的 DiskManager 并没有使用 Direct I/O,在维护自身缓冲池的同时,也使用了 Page Cache。
- 原因:Direct I/O 要求 4K 对齐和文件系统支持,实现较繁琐。BusTub 目前没有考虑 Recovery,所以没有用到
fsync()
。 - 影响:应该不影响 Buffer Pool Manager,也不影响性能,取决于 Page Cache 容量。正确性上没有区别。有一些场景需要 Page Cache + Buffer Pool Manager,有一些场景 Direct I/O 更合适。如读无法 4K 对齐,其实用 Page Cache 也问题不大;还有一些 page compression 的场景,如果能让 Page Cache 缓存压缩后的 page,也可以省一点 I/O。正确性上,如果有 ARIES,正确性由 redo/undo log 保证;redo undo log 做了
fsync()
就没有正确性问题。
如果使用 O_DIRECT
选项,磁盘读写从文件系统层直接访问最底层的存储设备,不走操作系统层的 Page Cache。但除了文件数据本身,文件的元数据(如文件大小)也会影响数据完整性。Direct I/O 只对文件数据本身有效,元数据读写还是会经过内核缓存。因此即使使用了 O_DIRECT
选项,在写入后,还需要调用 fsync()
保证数据真正落到磁盘。
fsync
卡顿怎么处理?
参考 PostgreSQL 9.6 平滑 fsync, write 原理浅析-阿里云开发者社区 一文,当数据库产生尖锐(堆积)的 I/O 需求时,就可能造成 fsync()
的卡顿。解决方式为:
- 定期检测 os dirty page 的占比,超过水位线则异步刷脏
- 对属于同一个存储块的脏页排序后再写入,降低离散 I/O
将数据刷入磁盘时宕机了怎么办?
参考 基于 Redo Log 和 Undo Log 的 MySQL 崩溃恢复流程 - 知乎 一文,可以使用 Redo Log 和 Undo Log 以保证数据的持久化。MySQL 使用 2PC (two-phase commit protocol) 以保证 Redo Log 和 Binlog 在事务提交时的数据一致性。
- Redo Log 记录了此次事务「完成后 」的数据状态,记录的是更新之「后」的值
- Undo Log 记录了此次事务「开始前」的数据状态,记录的是更新之「前」的值
Buffer Pool
LRU-K 比 LRU 替换算法好在哪里?
在 LRU-K 算法的论文中,列举了以下情况:
- 内部事务 (Intra-Transaction):事务进行更新时,先读取某页,再更新该页
- 事务重试 (Transaction-Retry):事务访问页后中止,重试时再次访问该页
- 内部流程 (Intra-Process): 具有相同流程的事务接连访问某页,如连续更新 100 条记录
- 巧合流程 (Inter-Process): 两个完全独立的事务刚好访问相同的页
前 3 个例子被称作关联访问 (correlated reference-pair type),LRU 算法无法处理这些情况,无法避免因为周期性模式而淘汰了需要长时间保存在缓存中的数据;原始的 LRU-K 算法在保留前 K 次的访问情况的同时,会将关联访问缩点,从而提高缓存的命中率和性能表现。
在 BusTub 的测试中,为 Scan 线程和 Get 线程提供了不同的标签,可以用比较方便的方式区分关联访问和非关联访问。