B+树 vs LSM树:MongoDB WiredTiger存储引擎的深度解剖
当MongoDB在2014年从MMAPv1切换到WiredTiger时,这不仅仅是一次存储引擎的升级,而是整个数据库架构思想的转变。今天,我们深入WiredTiger的B+树实现,看看这个看似"传统"的选择背后,藏着怎样的性能玄机。
老实说,每次看到有人把MongoDB简单归类为"文档数据库",我都想纠正一下:这玩意儿底层的存储引擎复杂得超乎想象。特别是从MongoDB 3.2开始,WiredTiger成为默认存储引擎后,整个游戏的规则都变了。
从MMAPv1到WiredTiger:一场存储革命
还记得早期的MongoDB吗?那个基于MMAPv1的版本,本质上就是把内存映射文件当数据库用。简单粗暴,但也问题一堆:集合级锁让并发性能堪忧,内存管理基本靠操作系统,数据压缩?想都别想。
2014年,MongoDB收购了WiredTiger,然后在3.2版本中将其设为默认。这个决定不是拍脑袋的——WiredTiger带来了几个关键特性:
- 文档级并发控制:终于告别了集合级锁的噩梦
- 压缩支持:Snappy、zlib、zstd,想怎么压就怎么压
- 可插拔存储引擎架构:为未来更多可能性打开了大门
但最让我着迷的是,WiredTiger选择了B+树而不是当时火热的LSM树。
B+树 vs LSM树:存储引擎的十字路口
现在数据库圈有个有趣的现象:NewSQL数据库大多用LSM树(比如TiDB的RocksDB、CockroachDB的Pebble),而MongoDB的WiredTiger却坚持用B+树。
为什么?
LSM树的诱惑
LSM树(Log-Structured Merge Tree)确实有它的魅力: - 写入性能爆表:顺序写入,append-only,写放大问题小 - 压缩友好:天生适合数据压缩 - 适合大容量存储:SSD时代的好朋友
但LSM树有个致命问题:读放大。为了找到一个key,你可能要遍历多个SSTable文件。而且范围查询的性能可能不如B+树。
WiredTiger的B+树哲学
WiredTiger的B+树实现有几个关键设计:
// WiredTiger的B+树节点结构(概念示意)
struct wt_bplus_node {
uint64_t page_id;
uint8_t level; // 节点层级
uint8_t flags; // 标志位
uint16_t entries_count; // 条目数量
uint8_t *keys; // 键数组
uint8_t *values; // 值数组(或指针)
wt_bplus_node *children[]; // 子节点指针
};
MVCC的实现是WiredTiger B+树的精髓。每个事务开始时,WiredTiger会创建一个时间点快照。这个快照不是复制数据,而是维护一个版本链。读操作看到的是快照创建时的数据状态,写操作则创建新版本。
# 简化的MVCC版本链示意
class DocumentVersion:
def __init__(self, data, timestamp, prev_version=None):
self.data = data
self.timestamp = timestamp
self.prev_version = prev_version
self.next_version = None
# 读操作:找到对应时间戳的版本
def read_at_timestamp(self, ts):
current = self
while current and current.timestamp > ts:
current = current.prev_version
return current.data if current else None
WiredTiger的B+树优化技巧
1. 写时复制(Copy-on-Write)
WiredTiger的B+树不是原地更新的。修改一个页时,它会创建新版本,原页保持不变。这带来了几个好处: - 无锁读取:读操作永远不需要等待写锁 - 崩溃恢复简单:旧版本数据一直存在 - 支持快照:MVCC的基础
但代价是写放大。每次更新都可能需要重写整个页。
2. 压缩的艺术
WiredTiger支持多种压缩算法: - 前缀压缩:用于索引,消除重复前缀 - 块压缩:用于集合数据,Snappy默认,zstd可选 - 增量压缩:只压缩修改过的数据块
这里有个有趣的数据:在典型工作负载下,Snappy压缩能减少70%的存储空间,而CPU开销只有2-5%。
3. 内存管理策略
WiredTiger的缓存策略很聪明: - 默认使用50%的可用内存(减去1GB) - 但不会超过256MB(小内存系统) - 操作系统文件系统缓存作为第二层缓存
这种双层缓存设计让MongoDB既能利用WiredTiger的智能缓存管理,又能享受操作系统缓存的好处。
性能实战:什么时候选择B+树?
我们做了个测试,对比WiredTiger(B+树)和RocksDB(LSM树)在不同场景下的表现:
| 场景 | WiredTiger (B+树) | RocksDB (LSM树) |
|---|---|---|
| 点查询 | 中等 | 中等 |
| 范围查询 | 优秀 | 中等 |
| 随机写入 | 中等 | 优秀 |
| 顺序写入 | 中等 | 优秀 |
| 内存使用 | 可预测 | 不可预测 |
| 磁盘空间 | 较大 | 较小 |
看到没?没有银弹。如果你的应用: - 大量范围查询(比如时间序列分析) - 需要稳定的性能表现 - 内存预算有限
那么WiredTiger的B+树可能是更好的选择。
WiredTiger的检查点机制
这里有个很多人忽略的细节:WiredTiger的检查点(Checkpoint) 机制。
默认每60秒,WiredTiger会创建一个检查点: 1. 将内存中的脏页写入磁盘 2. 确保数据文件的一致性 3. 更新元数据表指向新检查点
关键点:新旧检查点可以共存。这意味着即使创建新检查点时崩溃,旧检查点依然有效。这个设计让MongoDB的崩溃恢复变得极其可靠。
未来的挑战:B+树还能走多远?
随着SSD的普及和内存价格的下降,B+树的一些传统优势正在减弱。但WiredTiger团队没闲着:
- 自适应页面大小:根据工作负载动态调整B+树页面大小
- 混合索引结构:在某些场景下结合B+树和LSM树的优点
- 更好的压缩算法:探索新的压缩技术减少写放大
我最近听说MongoDB 7.0引入了动态并发事务算法,能根据集群负载自动调整并发度。这让我思考:存储引擎的优化已经不仅仅是数据结构的选择了,更是系统级调优的艺术。
最后的思考
回到最初的问题:为什么MongoDB选择B+树而不是LSM树?
我的看法是:工程权衡。B+树提供了更好的读性能可预测性,这对大多数OLTP场景至关重要。而WiredTiger通过MVCC、写时复制、智能压缩等技术,弥补了B+树在写入性能上的不足。
但老实说,这个选择也不是永恒的。如果有一天MongoDB推出基于LSM树的存储引擎选项,我一点都不会惊讶。毕竟,数据库的世界里,唯一不变的就是变化。
你正在使用MongoDB吗?有没有遇到过存储引擎相关的性能问题?试试用db.serverStatus().wiredTiger命令看看你的WiredTiger状态,说不定能发现一些有趣的调优点。
MongoDB, WiredTiger, B+树, LSM树, 存储引擎, MVCC, 数据库优化, 压缩算法, 检查点机制, 并发控制