3、日志与快照:Write-Ahead Log (WAL) 原理、快照生成策略、日志回放机制
好,咱们进入第三个核心话题。订单簿重建,说白了就两件事:一是怎么把数据安全地写下来,二是出问题后怎么快速恢复。这背后依赖的就是 WAL 和快照这对黄金搭档。
我个人习惯把 WAL 比作「记账本」,快照就是「定期汇总表」。记账本记录每一笔流水,汇总表告诉你某个时刻的总账目。两者缺一不可。
3.1 Write-Ahead Log (WAL) 原理
WAL 的核心思想很简单:先写日志,再写内存。你想想看,如果先改内存再写日志,机器突然挂了,内存数据丢了,日志里也没记,那订单簿就永远少了一笔。这谁受得了?
我在项目中遇到过这么个坑:早期版本为了追求低延迟,把 WAL 刷盘策略设成了「每秒刷一次」。结果有一次磁盘 IO 抖动,日志没来得及落盘,内存里的订单簿已经改了。恢复时发现少了几笔成交记录,排查了整整两天。嗯,从那以后我再也不敢在核心链路上用异步刷盘了。
- 收到订单簿变更事件(新增、修改、删除订单)
- 将变更序列化为日志条目,追加写入 WAL 文件
- 等待 WAL 刷盘确认(同步或批量同步)
- 更新内存中的订单簿数据结构
- 返回操作成功
这里有个关键点:日志必须是追加写。为什么?因为顺序 IO 比随机 IO 快一个数量级。你想想看,机械硬盘寻道时间 10ms,顺序写几乎可以忽略。SSD 虽然好一些,但顺序写的吞吐量依然远高于随机写。
日志条目的结构,我一般这样设计:
// 日志条目结构
struct WALEntry {
uint64_t sequence; // 全局递增序列号
int64_t timestamp; // 时间戳
uint8_t entry_type; // 类型:ADD/UPDATE/DELETE/SNAPSHOT
uint32_t data_length; // 数据长度
uint8_t data[]; // 变长数据,序列化的订单信息
uint32_t checksum; // 校验和,用于检测数据损坏
};
校验和这玩意儿,我建议一定要加。曾经有一次磁盘静默错误,读出来的数据跟写进去的不一样,没有校验和根本发现不了。加了之后,至少能保证「要么正确,要么报错」,不会出现静默数据损坏。
3.2 快照生成策略
WAL 会无限增长吗?当然不会。你想想看,如果系统运行一个月,WAL 文件可能有几十 GB,回放起来得等到猴年马月。所以我们需要快照——定期把内存中的订单簿全量保存下来。
快照生成策略,我总结下来有几种:
| 策略 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 定时快照 | 每隔固定时间(如 5 分钟)生成一次 | 实现简单,时间可控 | 可能生成无用快照(无变更时) |
| 日志量触发 | WAL 达到一定大小(如 1GB)时触发 | 恢复时间可控 | 高峰期可能频繁触发 |
| 混合策略 | 定时 + 日志量双重触发 | 兼顾两者优点 | 实现稍复杂 |
我个人比较推荐混合策略。比如设定:每 5 分钟检查一次,如果 WAL 超过 500MB 就生成快照;如果没超过,就等到下一个周期。这样既不会频繁生成快照,也能保证恢复时间在可控范围内。
- 快照生成期间,WAL 不能停。新来的变更继续写 WAL,同时记录一个「快照开始」标记。
- 快照完成后,记录一个「快照结束」标记,包含快照对应的 WAL 序列号。
- 旧快照可以删除了,但至少要保留一个完整快照 + 其后的 WAL。
快照的存储格式,我习惯用内存数据结构的序列化版本。比如订单簿用红黑树,那就把树的前序遍历结果存下来。恢复时直接反序列化,比一条条回放快得多。
3.3 日志回放机制
回放机制,说白了就是「从快照恢复 + 重放 WAL」。流程如下:
- 加载最新的完整快照,反序列化得到内存订单簿
- 找到快照对应的 WAL 序列号(快照结束标记中的值)
- 从该序列号之后开始,逐条读取 WAL 条目
- 对每条日志执行对应的操作(添加、修改、删除订单)
- 直到 WAL 末尾,完成重建
这里有个细节:回放必须是幂等的。什么意思?就是同一条日志回放多次,结果应该是一样的。为什么需要这个?因为回放过程中可能崩溃,重启后需要重新回放,如果日志不幂等,订单簿状态就乱了。
我在项目中遇到过这个问题:早期版本的回放逻辑是「先删除再插入」,结果有一次回放到一半崩溃了,重启后重新回放,发现某条日志对应的订单已经被删除了,再执行删除就报错了。后来改成了「先查询再操作」,如果订单不存在就跳过删除,这才解决了幂等问题。
- 批量读取 WAL 条目,减少 IO 次数。我一般一次读 1024 条。
- 使用内存映射文件(mmap)加速 WAL 读取。
- 回放时关闭校验和检查(如果信任磁盘),可以提升 30% 速度。
- 多线程回放?可以,但要注意订单之间的依赖关系。同个订单的变更必须串行。
还有一个容易被忽略的点:WAL 文件的清理。快照生成后,快照之前的 WAL 就可以删了。但要注意,不能删正在写的那个 WAL 文件。我一般用「文件轮转」的方式:写满一个文件就换下一个,快照生成后把旧的、不再需要的文件删掉。
3.4 核心逻辑流程图
下面我用一张 SVG 图来展示 WAL、快照和回放之间的协作关系。这张图我画了好几次才满意,希望能帮你理清思路。
这张图从左到右展示了三个流程的协作关系。正常写入流程持续产生 WAL,快照流程定期「拍照」,回放流程在崩溃后从快照和 WAL 重建。三者环环相扣,缺一不可。
最后说一句:不要等到线上出问题了才去验证恢复流程。我建议每两周做一次恢复演练,模拟磁盘损坏、进程崩溃等场景,确保快照和 WAL 真的能重建出正确的订单簿。这个习惯,救过我很多次。
公众号:蓝海资料掘金营,微信deep3321