不是不信任,而是我终于知道,所有数据库、流处理器、分布式系统最底下的那层“铁”承诺,在字节级别到底有多少脆弱的缝隙。文档里写着“写入已持久化”,抽象模型拍胸脯说“崩溃恢复可靠”,可一次进程恰好在写入中途挂掉的实验,就让那些黑盒神话碎了一地。

我动手写了 VeriStore——一个追求正确性优先、用 C++ 从零构建的键值存储引擎,从最朴素的线程安全内存映射一口气演化到基于 Raft 共识复制的分布式系统,额外还搭了一个迷你 S3 风格的对象存储层。目的只有一个:亲眼看着每一个字节怎么在崩溃后活下来。下面拆开来看每一版究竟在解决什么问题,以及动手之后才看得见的魔鬼细节。

打开网易新闻 查看精彩图片

v0.1 内存键值库:唯一不需要“崩溃之后”的版本

地基很简单:一个受读写锁(std::shared_mutex)保护的无序映射,对外暴露出 PUTGETDEL 三个接口。没有持久化、没有日志,所有状态都捏在进程的虚拟内存里。进程一死,数据灰飞烟灭,连个墓碑都不留。

这版的意义在于建立整个引擎的抽象骨架——后续所有持久化、复制、对象接口,都是在这套命令模型上嫁接出来的。而且它给了一个基准:性能的极致天花板,后面每加一层可靠性,都是在用吞吐量换取存活保证。

v0.2 预写日志与崩溃恢复:开始跟字节较劲

第一个真问题:怎样让一次写入在进程挂了、操作系统崩了、甚至整机掉电之后还能恢复出来?光喊“持久化”没用,得在磁盘上留下一串清晰的足迹。方案是追加型预写日志,每条记录在修改内存映射之前先落盘:先追加日志,再应用内存,最后看情况刷盘。

具体流程是这样:
PUT x 100 → 追加到 WAL → 应用到映射
PUT y 200 → 追加到 WAL → 应用到映射
FLUSH → 调用 fsync 强制刷盘
——然后崩溃。

重启时从日志头开始重放:依次读出 x=100、y=200,重建内存状态。

真正的坑在崩溃刚好发生在写日志途中。一条记录可能只落下一半,磁盘上留下半拉字节。这时候全量循环校验(CRC)就登场了:每一条记录都带着校验码,重放时若发现校验不通过,立刻判定这是一条撕裂写入,直接忽略并停止重放。也就是宁缺毋滥:凡是不完整的,就当没发生过。这条保证很硬:只要对客户返回了 OK,那就一定能在崩溃后活过来,没有例外。

v0.3 快照与日志压缩:不让重启变成重播马拉松

崩溃恢复的代价会随着运行时长线性增长——每一条历史写入都要在重启时重放。系统跑上一周,日志能膨胀到数 GB,重启变成灾难。快照就是为了给恢复时间设置一个硬上限。

做法很简单:周期性地把当前内存状态序列化刷到磁盘,生成一个完整快照,然后把快照之前的所有日志记录一刀截掉。重启时先加载快照,再重放快照之后那一点点新增日志,启动时间被限定为一个常量,跟系统总运行时长解耦。

这步暴露了一个被许多介绍忽略的细节:快照本身也是一次“写入”,它在生成过程中如果正好有并发写入,怎么办?因为引擎已有 WAL 保证,快照生成时只需对内存结构拍一张一致性读的快照(利用读写锁),同时写操作继续在日志里记录即可。快照完成后截断日志的瞬间,因为快照已经包含了那一刻的全部状态,截断并不会丢失任何已提交的数据。

v0.4 组提交性能提升:每一笔都 fsync?写哭了

单笔写入立刻 fsync 确实最安全,但也慢到让人想删库跑路。组提交的思路是把一堆客户端的写请求攒到一个批次,在批次边界处做一次统一的刷盘。可以理解为:大家排好队,一起落到地上,而不是每个人单独跳一次深蹲。

实测下来吞吐量提升了大约 2.7 倍,压低的正是磁盘那些昂贵的刷新开销。这个优化看起来朴素,却是 PostgreSQL、RocksDB、etcd 等系统的标配。动手实现之后才感受到,减少 fsync 频率的同时还要保证崩溃后不丢上一个批次的数据,需要对刷盘时机和 WAL 记录顺序有精密控制——先记录、攒批次、再刷盘,断电后重放时只要能识别出批次边界,就不会把尚未刷盘的记录当作已提交。

v0.5 Raft 共识复制:单机再稳也怕整机消失

单节点哪怕把所有数据都写到铁打的盘上,也扛不住机器整机报废、机房掉电或者网络分区导致的脑裂。Raft 共识算法就是来解决多节点之间“到底谁说了算”的。这里需要让 VeriStore 变成一个集群:

  • 领导者选举:节点通过随机超时触发选举,避免废话争夺,保证任何时刻最多只有一个领导者。
  • 日志复制:领导者把每条写操作复制到所有追随者,只有追随者确认收到后,领导者才下决心提交。
  • 多数派提交:一条写操作必须在超过半数节点确认后才能被外界宣告成功——哪怕少数节点挂掉或分家,日志的一致性依然成立。
  • 追随者追赶:掉队的节点重启后自动发现落后,向领导者索要缺失的日志条目并逐步对齐。

跑起来的输出很直接,却透着一股机器式的笃定:
[raft] node 3 became LEADER term=1
ProposeP ...

每一个术语背后都是成堆的 corner case 处理——分裂投票怎么办、日志冲突怎么回滚、快照怎么与 Raft 日志联动——文档里轻描淡写的那句“崩溃后恢复一致”,代码里是几百行的状态机。

这一版真正让人看清:持久化的保证在分布式语境下是多层面的,磁盘的字节可靠性只是第一步,节点间的共识才是数据不死的关键。而那些高级数据库在宣传册子里划掉的“强一致性”,在实现者的世界里,是无数个 termcommitIndex 和心跳包的组合。

至此,VeriStore 从一个 200 行的内存 map 蜕变成一套能扛住单节点物理毁灭的分布式存储骨架。而整个项目的起源只因为一个执念:不想再被抽象骗了。每一条“持久化”承诺,都应该能翻译成:日志写了多少字节、fsync 什么时候调、多少个节点确认了。如果这三个问题答不上来,那么文档里的“数据安全”就只是一句漂亮的空话。