- WAL
- 每一次读取时, 扫描整个 wal
问题:
- wal 无序, 只能顺序扫描
- 数据存在多个版本, 所以必须扫描完整个 wal 文件
- 数据多版本, 浪费空间
- 无法利用局部性原理
启动时加载全部 key 到 cache, 写时维护 key cache
问题:
问题:
考虑内存占用问题, 内存中只保留 key 的摘要(crc32, hash). 因为摘要有可能冲突, 所以采用开放链式结构.
问题:
按 range 拆分 key wal, 否则每一次都 compact 全量 key, 成本太高.
问题:
如果分裂操作是同步的, 将阻塞正常请求. 在分裂过程,
因为在写流程中做 compact 会拖慢写操作, 所以, 异步地进行 compaction.
问题:
在并发写的场景下, 是可以用写缓冲模式的, 主动阻塞每一个写线程, 收集到多个写请求之后, 批量提交. 但如果是单线程写, 只能要求请求者自己缓冲多个请求(也即 Batch 接口).
在将 log(raft log, binlog) 分离之后, 数据库不依赖使用场景即可做缓冲, 因为丢失的数据可以从 log 中回补.
给操作分配序列号, 多个目标(资源)单独确认自己的持久化进度, 引入单点确认整体的持久化进度.
因为某种原因, 在操作目标前, 先通过一个代理, 对代理的操作, 即认为是对目标的操作. 例如, 目标当前正在做 compaction, 无法写入, 因此先将数据写入代理. 等目标就绪后, 代理再将之前缓冲的操作, 依次应用到目标身上.
读取时, 要么全部通过代理, 要么先获取目标的写锁之后(禁止目标变更状态), 判断目标的状态, 然后决定是否去读代理.
compaction 生成新文件时, 采用 buffer io 模式, 最后再调用 fsync().
这是一个总的原则, 然后粒度不断地细化.
数据库最初是序列化操作的, 每一个读和写操作排队.
读和写虽然排队, 但, 读和读之间可以并发进行. 性能有了进一步的提高.
读可以并发, 写虽然不可以并发, 但读和写之间可以并发. 只需要它们操作不同的版本, 然后通过一个 Commit Point 来切换.
打开同一个文件两次, 获得两个 fd, 一个用于写, 一个用于读. 记录写的偏移, 确保读不超过该偏移, 这样读就不会读到不完整的记录.
好处:
系统的 fsync 资源是唯一的, 即使用户多线程/多线程调用 fsync, 依然会被内核排队处理.
所以, 对于数据库系统来说, 提高写性能的唯一路径就是做写 Batch.
例如 cache, 除了限制占用内存的大小, 还可能需要限制数据条数.
在目标进行变更的过程中, 新的数据暂时存在 Buffer 中. 当目标变更完毕后, 需要将 Buffer 转存到目标中. 如果转存过程是一个原子操作, 那么就要加锁, 锁住 Buffer.
使用 tombstone 技术的系统, 即使 tombstone 保存在内存中, 当出现大量的连续的 tombstone 时(例如10w级别), 扫描一次可能也要数毫秒, 而且空耗大量的 CPU.
把数据和删除标记, 分开存储. 当删除标记达到一定程度时, 必须真正地将数据清除.
写缓冲
缓冲多次写操作, 合并同一个 key 多次操作, 便可减少写 IO.
问题: