44.5 存储引擎:读写放大与空间放大

一句话

存储引擎的好坏最终都落到三个放大系数上:写放大(写 1 字节实际写了几字节)、读放大(读 1 行实际读了几行/几页)、空间放大(1GB 数据实际占几 GB)——优化引擎,本质是把这三个数往 1 压,而它们互相牵制。

三个放大系数的来源

放大 定义 主要来源 B+ 树 LSM
写放大 WA 实际写字节 / 逻辑写字节 页分裂、compaction 重写、日志 低(1–2×) 高(10–30×)
读放大 RA 实际读的行/页 / 需要的数据 多层查找、页内多余列、块过大 低 中高(查多层)
空间放大 SA 实际占用 / 逻辑数据 多版本、tombstone、页内空隙 低 中(待合并文件)

实测:LSM 的写放大与布隆过滤器的读收益

模拟 5 万条记录、内存表 1000 条、fanout=4:

指标 结果
写放大 约 3.0×(每层合并都重写数据)
生成文件数 62 个 SSTable
5000 次点读(无布隆) 读 7462 个文件
5000 次点读(有布隆) 读 5000 个文件,跳过 2521 次(33%)

注意这个 WA=3× 是 “小规模、少层” 的乐观结果:真实 LSM 每层都要重写,层数越多 WA 越高(常见 10–30×)。层数 = log_fanout(数据量/内存表),所以 “fanout 越大、层越少、WA 越低,但单次合并越贵”。

LSM 写放大 ≈ fanout × 层数
例:fanout=10、数据 1TB、memtable 256MB → 层数 ≈ 4 → WA ≈ 40×(量级估算)

实测:B+ 树的写路径

同一份 5 万条记录,sqlite3(B+ 树)写入 86ms、文件 5.4MB:写放大接近 1(就地改页),但代价是随机写与页分裂——机械盘上会明显变慢。

反模式

反模式 后果
只看 “写入快”,不看 WA SSD 寿命与 IO 被 compaction 吃光
用大 fanout 压 WA 单次 compaction 搬运巨大,延迟尖刺
LSM 不设布隆过滤器 RA 高,点查慢
B+ 树频繁随机更新大表 页分裂 + 随机 IO,写放大上升
不监控 SA 磁盘 “莫名” 涨到告警,其实是旧版本堆积
用 “平均延迟” 评估 compaction 影响 尖峰被平均掩盖,用户感知不到

三条铁律

🔑 三个放大互相牵制:压 WA 常常抬 SA,压 RA 常常抬内存。

🔑 LSM 的 WA 随层数线性增长;层数由 “数据量/内存表 × fanout” 决定。

🔑 放大系数要监控,而不是等磁盘满/IO 饱和才发现。

自检

Q1 为什么 LSM 的写放大比 B+ 树高这么多?

▸ 答案:因为 LSM 的数据会被反复重写:从 memtable 刷到 L0,再逐层合并到 L1、L2…每合并一次就重写一遍。B+ 树只在页分裂时搬少量数据。所以 LSM 用 “多写几倍” 换 “顺序写 + 高吞吐”。

Q2 增加 fanout 为什么能降低写放大?

▸ 答案:因为 fanout 越大、层数越少,每条记录被重写的次数越少(WA ≈ fanout × 层数)。但合并时的单次工作量更大(一次合并 fanout 个文件),会造成更大的 IO 尖峰与内存占用——这是 “平均 WA 下降,尾部延迟上升” 的典型权衡。

Q3 空间放大在 LSM 里具体是什么?

▸ 答案:三部分:多版本文件(同一 key 在多层存在)、tombstone(删除标记要保留到最底层合并)、待合并的临时数据。所以 “逻辑 100GB” 可能占用 150–200GB,直到 compaction 完成。

Q4 为什么 “布隆过滤器只省 33%” 也值得做?

▸ 答案:因为它省的是随机 IO:33% 的文件读取意味着 33% 的 IOPS 与延迟下降,而布隆过滤器本身只有几 bit/key 的内存开销、查询是 O(1) 哈希。对点查型负载,这是 “几乎免费” 的优化。

Q5 B+ 树的写放大在什么情况下会变高?

▸ 答案:随机更新大表时:每次更新都要读页、改页、可能分裂,写放大与随机 IO 一起上升;加上 WAL 的日志写入,实际 WA 可能是 2–3×。顺序插入(自增主键)则接近 1×。

Q6 如何 “监控” 这三个放大系数?

▸ 答案:WA:对比 “逻辑写入量” 与 “磁盘写字节”(/proc/diskstats 或云监控);RA:对比 “返回行数” 与 “扫描行数”(执行计划/慢日志);SA:对比 “逻辑数据量” 与 “磁盘占用”(表大小 vs 行数 × 行宽)。三者都有现成指标,关键是要 “定期看趋势”。

Q7 什么时候该接受 “高写放大”?

▸ 答案:当写入吞吐与顺序性比 IO 成本更重要时:日志、时序、消息存储等 “追加为主” 的场景,LSM 的 WA 可以用 “更大内存表 + 更少层” 压下来,换来远高于 B+ 树的写吞吐。反之,更新密集、容量敏感的场景应选 B+ 树或 “LSM + 定期重建”。

关联