推荐跳表:其无锁并发、内存友好、契合“用完即弃”语义等优势显著优于红黑树,更适配go中lsm tree的高吞吐与安全性需求。

MemTable 选跳表还是红黑树?
Go 实现 LSM Tree 时,MemTable 的底层有序结构直接影响写入吞吐与并发安全。跳表(Skiplist)和红黑树(RedBlackTree)都能提供 O(log n) 查找,但工程表现差异明显:
- 跳表天然支持无锁并发读写——多个 goroutine 可同时在不同层级插入/查找,
sync.RWMutex开销小;红黑树标准实现(如gods/trees/redblacktree)通常需全局锁,高并发下易成瓶颈 - 跳表内存布局更友好:节点是链表+指针数组,GC 压力低于红黑树的嵌套结构体;实测在 10K+ key/s 写入场景下,跳表 GC pause 低约 40%
- 跳表删除后不缩容,但 LSM 场景中
MemTable是“用完即弃”——达到阈值就 flush 成 SSTable,无需担心内存残留;红黑树则需显式清理,稍增逻辑复杂度
结论:Skiplist 更贴近 LSM 的“一次性写入、批量落盘”语义,推荐作为 MemTable 默认实现。
WAL 日志必须 fsync 吗?
是的,WAL 文件每次写入后必须调用 file.Sync(),否则断电或 crash 会导致未 flush 的 MemTable 数据永久丢失。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 仅
file.Write()不够:数据可能滞留在 OS page cache 中,Sync()才确保刷到磁盘物理介质 - 但频繁
Sync()会拖慢写入——可聚合多条记录为 batch,再统一Sync();Clawstore 就采用此策略,延迟控制在 1ms 内 - 注意:SSD/NVMe 对
Sync()延迟敏感,某些廉价 NVMe 驱动存在Sync()hang 问题,建议在初始化时做fsync健康检测
SSTable 的 block size 设多少才合理?
SSTable 切分 block 的大小直接影响读放大和内存索引开销,不是越大越好,也不是越小越好。
- 默认设为
4KB是平衡点:匹配大多数 SSD 的页大小,减少读取时的物理 IO 次数;小于 2KB 会显著增加 block header 和 restart point 元数据占比 - 若应用以大对象(如图片、日志块)为主,可调至
16KB或32KB,但需同步增大布隆过滤器的bitsPerKey,否则假阳性率飙升 - Clawstore 使用动态 block size:根据 key/value 平均长度自动选择 4KB/8KB/16KB,避免小 key 大 block 浪费空间
Compaction 时为什么不能直接删旧 SSTable?
因为 compaction 是异步的,而查询可能正在读取被标记为“待删除”的旧文件——直接 os.Remove() 会导致 EBADF 错误或静默数据丢失。
- 正确做法:compaction 完成后,先将新 SSTable 的元数据原子写入
MANIFEST文件,再通过引用计数或 epoch 标记旧文件为“可回收”,最后由后台 goroutine 安全清理 - LevelDB 用
VersionSet管理活跃文件集合,Clawstore 则用FileRegistry+atomic.Int64记录每个文件的 reader 引用数 - 容易忽略的坑:Windows 下文件句柄未完全释放前
Remove()会失败,需加重试和超时,否则磁盘空间持续增长
LSM 的真正复杂性不在单点算法,而在各组件间的状态协同——MemTable 切换时机、WAL 截断边界、SSTable 生命周期管理,三者必须严格对齐,差一个原子操作就可能引发数据不一致。










