从一个落盘方案到六百万 TPS:十五篇的因果链
九月写了十五篇文章,源头只有一句问话。review 一套布谷鸟过滤器分片的落盘方案时,对方问了句「这套设计叫什么名字」——卡住了。它没有名字,因为它是五个经典思想的排列组合:槽位环、脏槽位、双代文件、位图翻转,每个零件都有三十到六十年的出处,拼在一起就成了「自研」。
这个月剩下的十四篇,全是那句问话滚出来的雪球。这篇目录把因果链摊开:谁引出了谁、哪篇是哪篇的地基、两条线在哪里汇流,附阅读顺序和一条快速通道。
1. 缘起:一个没有名字的方案
先把被拆解的对象摆出来:N 个布谷鸟过滤器排成环,当前槽位写满就序列化落盘、推进指针覆盖最旧的;每个槽位维护 A/B 双代文件轮流写,fsync 完成后翻转 index 位图;崩溃后读位图,按代加载。
三篇文章从三个视角拆它:
- 04《位打包两行代码的完全解剖》:零件级。
replica_bits位图的编码术——(N+7)/8算容量、1<<(7-i%8)放数据,两个惯用法拆到分子级别 - 06《状态上移,原子下沉》:结构级。这套方案的代码演化——
WalSnapshot重构为filterSnapshot+ 上层位图,把「决定哪代活跃」和「原子地写一个文件」拆成两层,包装优于修改 - 04-1《一个没有名字的持久化设计》:思想级。五个零件逐一溯源——Ring Buffer、脏页 writeback、双缓冲、WAL checkpoint、Shadow Paging,配 kfifo、InnoDB、LevelDB、ZFS、BoltDB 案例
04-1 是全系列的枢纽。它每节结尾都埋着钩子:「这里的门道值得单独展开」。后来的十四篇,一半是在还这些账。
2. 存储支线:欠条、切换、恢复
04-1 用一节带过的思想,各值得一篇完整展开:
- 08-4《dirty 是一张欠条》:记账只讲了半个故事,什么时候还钱 是另一整套设计——Linux 双阈值节流、InnoDB flush list、fsync 到底买了什么
- 08-5《切换必须原子》:双缓冲的三步舞谁都会跳,命根子全在切换那一步——前后缓冲的页翻转、蓝绿的路由表、LevelDB 的一行指针赋值、Redis 的 fork,外加 COW 这个隐形亲戚
- 08-3《重放与影分身》:崩溃后要么旧要么新——WAL 用顺序追加换重放恢复,shadow paging 用写时复制换 O(1) 恢复,从 ARIES 到 ZFS uberblock 与 BoltDB 双 meta page,最后发现两条路线在互相抄作业
三篇合起来正好是持久化的完整生命周期:平时怎么记账(08-4)、怎么原子换代(08-5)、崩溃后怎么收场(08-3)。
3. 无锁主线:kfifo 一句话滚出的雪球
04-1 讲 kfifo 时带过一句「单生产者单消费者完全无锁」。这句亏了,它背后是一整套可以迁移的并发设计纪律:
- 07《无锁环形队列为什么敢不用锁》:核心洞察是给每根指针找唯一写者——写冲突消失、CAS 多余、判空判满退化为单调计数比较。公式三件套:唯一写者 + release/acquire + 缓存行隔离
- 08《写了不等于看得见》:公式中间那件的地基。从 store buffer 到 x86 TSO 与 ARM 弱序,到 Go 内存模型与
-race的 vector clock,最后落回一条总原则:独占,或看守 - 07-1《两个生产者之后》:前提被打破怎么修——CAS 认领、生产者侧 mutex、per-slot 序列号三条路线,附完整 Go 实现与选型对比
- 08-1《一把锁罩住一切》:反问标准库——channel 的 hchan 干脆一把大锁,不是不会无锁,是算过账:正确性和可组合性是特性,微优化是选项
- 08-2《从自旋到休眠》:竞争存在时怎么等——纯自旋、让步、混合、futex、park 五档谱系,各档的让出单位与唤醒成本
- 08-6《实测:channel 到底比无锁队列慢多少》:账不能只算不验——32 核跑分,单对单四种实现挤在 110~160ns,竞争一上来 channel 反超手写队列一倍。结论升级:瓶颈不是锁,是 等待的方式
- 08-7《归并写者》:收官。数据结构层的修法是让竞争变便宜,架构层的修法是让竞争根本不发生——Actor 的单消费者 mailbox、LMAX 单线程核心、事件溯源的单写者 journal
注意一个发表顺序与逻辑顺序的错位:08 发表在 07-1 之后,但它才是 07 的地基——07 说「用 release/acquire」,08 说「凭什么必须」。按依赖读,08 应紧跟 07。同样,存储线的 08-3 到 08-5 与无锁线的 08-1、08-2、08-6、08-7 全是 09-08 同一天发的,两条线当天并行推进。
4. 汇流与注脚
08-8《六百万 TPS 的单线程》 是两条线的汇合点。表面上它是无锁线的终点形态(Disruptor 本体精读),内核里全是存储线的投影:单写者顺序 append 的 journal 是 WAL,快照 A/B 双代转正是 BoltDB 双 meta page 的同款。它结尾那张思想对照表把系列前作逐条对上——04-1、07、07-1 三次点名、三次一行带过的 Disruptor,在这里一次还清。
09-1《「满:等消费者腾地方」》 是收官后的注脚:07-1 归并写者里一句 Gosched 注释被追问了两句,索性用 GOMAXPROCS=1 实测自旋、让出、park 三种等待的运行时路径,顺手修正一个想当然的结论。
5. 阅读顺序
按依赖而非发表顺序:
源头 04-1(总纲,赶时间只读 §2 kfifo 一节也够)
存储线 04-1 → 08-4 欠条 → 08-5 切换 → 08-3 恢复
无锁线 04-1 → 07 立纪律 → 08 打地基 → 07-1 修前提
→ 08-1 对照标准库 → 08-2 等的成本
→ 08-6 实测裁决 → 08-7 架构层收官
汇流 08-8(LMAX 精读,两条线在此合流)
番外 09-1(一句注释的实测)
只有半小时就走快速通道,三篇看骨架:04-1(五个思想)→ 07(无锁公式)→ 08-7(归并写者)。中间每一篇,都是这三篇某一节的展开。
小结
回看这十五篇的生成方式:一次 review 的答不上来,变成一篇思想溯源;总纲里每个「值得单独展开」,变成一条支线;支线里每个「三次点名、一行带过」,变成一篇精读。写系列和做存储设计是同一个道理——不怕零件多,怕零件之间没有引用关系。每篇结尾留一个钩子,钩子就是下一篇的 WAL:中断在哪里,都能从上一篇恢复。
最后更新:2026-09-10