KV存储笔记:WAL 与崩溃恢复

2 分钟阅读

发布于:

前言

WAL(Write-Ahead Log)是 KV 存储系统中保证数据持久性和崩溃恢复的关键机制。本文从 KV 存储系统的实际实现角度,介绍 WAL 在 KV 存储中的工作原理、格式设计、崩溃恢复流程以及性能优化策略。

注:本文聚焦于 KV 存储系统中 WAL 的具体实现。关于 WAL 的通用原理和底层机制,请参考「存储基础系列」的相关文章。

1. WAL 的作用

WAL 的核心思想是:在数据写入存储引擎之前,先将操作记录写入日志。这样可以:

  1. 保证持久性:即使系统崩溃,已提交的操作也不会丢失
  2. 支持崩溃恢复:重启后可以通过重放日志恢复数据
  3. 提高写入性能:顺序写入日志比随机写入数据更快

2. WAL 的写入流程

2.1 基本流程

1. 接收写入请求 (Put/Delete)
2. 将操作序列化并写入 WAL
3. 同步 WAL 到磁盘(可选,取决于持久性要求)
4. 将数据写入 MemTable
5. 返回成功

2.2 同步策略

  • 同步写入:每次写入后立即同步到磁盘,保证强持久性
  • 异步写入:批量同步,提高性能但可能丢失最近的数据
  • 定期同步:按时间间隔同步,平衡性能和持久性

3. WAL 的格式

WAL 通常采用二进制格式,包含:

  • 操作类型:Put、Delete 等
  • Key:键的长度和内容
  • Value:值的长度和内容(Delete 操作没有 Value)
  • 序列号:用于去重和排序
  • 校验和:用于检测数据损坏

4. 崩溃恢复流程

4.1 恢复步骤

  1. 扫描 WAL 文件:找到所有未处理的日志条目
  2. 重放日志:按顺序将操作应用到 MemTable
  3. 重建索引:如果使用 B+Tree,需要重建索引结构
  4. 验证数据:检查数据的完整性和一致性

4.2 去重处理

由于 WAL 可能包含重复的操作,恢复时需要:

  • 使用序列号去重
  • 只保留最新的操作
  • 跳过已处理的操作

5. WAL 的管理

5.1 日志轮转

当日志文件过大时,需要:

  • 创建新的日志文件
  • 将旧日志归档或删除
  • 更新日志文件列表

5.2 日志清理

当数据已持久化到存储引擎后:

  • 可以安全删除对应的日志
  • 需要跟踪哪些日志可以清理
  • 避免过早删除导致无法恢复

6. 性能优化

6.1 批量写入

  • 将多个操作合并写入日志
  • 减少系统调用和磁盘 IO
  • 提高写入吞吐量

6.2 压缩

  • 对日志进行压缩
  • 减少磁盘空间占用
  • 降低 IO 开销

6.3 异步刷新

  • 使用异步 IO 刷新日志
  • 不阻塞写入操作
  • 提高并发性能

7. KV 存储系统中的 WAL 实现案例

7.1 RocksDB 的 WAL 实现

RocksDB 的 WAL 特点:

  • 格式:使用 RecordBatch 格式,支持批量写入
  • 同步策略:支持 syncasync 两种模式
  • 压缩:支持 WAL 压缩,减少磁盘空间
  • 轮转:自动轮转 WAL 文件,避免单个文件过大

7.2 LevelDB 的 WAL 实现

LevelDB 的 WAL 特点:

  • 格式:使用变长记录格式,每条记录包含类型、长度、数据
  • 同步策略:默认异步写入,可配置同步写入
  • 恢复:启动时自动扫描并重放 WAL

7.3 BadgerDB 的 WAL 实现

BadgerDB 的 WAL 特点:

  • 格式:使用 Value Log(vlog)格式,专门优化大 Value
  • 分离设计:Key 和 Value 分离存储,Value 存储在 vlog 中
  • 异步刷新:使用异步 IO 刷新,提高性能

8. KV 存储系统中的特殊考虑

8.1 大 Value 的处理

KV 存储系统的 Value 可能很大,WAL 设计需要考虑:

  • Value 分离:大 Value 可以存储在单独的文件中,WAL 只记录引用
  • 压缩:对大 Value 进行压缩,减少 WAL 大小
  • 流式写入:支持流式写入大 Value,避免内存占用过大

8.2 批量操作的支持

KV 存储系统通常支持批量操作,WAL 需要支持:

  • 批量记录:将多个操作合并为一条 WAL 记录
  • 事务支持:支持事务的原子性写入
  • 部分失败处理:批量操作中部分失败时的恢复

8.3 多版本支持

KV 存储系统可能需要多版本,WAL 需要支持:

  • 版本号:每条记录包含版本号
  • 版本合并:恢复时合并多个版本
  • 版本清理:定期清理旧版本

9. 性能优化实践

9.1 写入路径优化

// KV 存储系统的 WAL 写入优化示例
class KVStorageWAL {
    // 批量写入缓冲区
    std::vector<WALRecord> write_buffer_;
    
    // 异步刷新线程
    std::thread flush_thread_;
    
    void write(const std::vector<KVOperation>& ops) {
        // 1. 序列化操作到缓冲区
        for (const auto& op : ops) {
            write_buffer_.emplace_back(serialize(op));
        }
        
        // 2. 批量写入 WAL(减少系统调用)
        if (write_buffer_.size() >= batch_size_) {
            flush();
        }
    }
    
    void flush() {
        // 批量写入磁盘
        wal_file_->write_batch(write_buffer_);
        
        // 根据持久性要求决定是否 fsync
        if (sync_mode_ == SYNC) {
            wal_file_->fsync();
        }
        
        write_buffer_.clear();
    }
};

9.2 恢复路径优化

// KV 存储系统的 WAL 恢复优化
class KVStorageRecovery {
    void recover() {
        // 1. 扫描 WAL 文件(并行扫描多个文件)
        std::vector<WALRecord> records = scan_wal_files();
        
        // 2. 按序列号排序
        std::sort(records.begin(), records.end(), 
                  [](const auto& a, const auto& b) {
                      return a.seq < b.seq;
                  });
        
        // 3. 去重(只保留最新的操作)
        records = deduplicate(records);
        
        // 4. 批量重放到 MemTable
        batch_replay_to_memtable(records);
    }
};

10. 小结

WAL 是 KV 存储系统保证数据持久性的关键机制。通过先写日志再写数据,可以在系统崩溃后恢复数据,保证已提交的操作不丢失。

核心要点

  • WAL 的作用:保证持久性、支持崩溃恢复、提高写入性能
  • 同步策略:需要在持久性和性能之间权衡
  • 恢复流程:扫描、重放、去重、验证
  • 性能优化:批量写入、压缩、异步刷新

在 KV 存储系统中的特殊考虑

  • 大 Value 处理:需要特殊设计避免 WAL 过大
  • 批量操作支持:需要支持批量写入和事务
  • 多版本支持:需要支持版本号和版本合并

在实际实现中,需要平衡持久性要求和性能开销,选择合适的同步策略和优化方案。不同的 KV 存储系统根据其应用场景,会采用不同的 WAL 实现策略。