Raft日志落盘需用fsync保证原子性,快照截断须同步更新lastIncludedIndex等元数据并原子替换文件,序列化禁用memcpy而应使用protobuf或自定义协议,读取日志需按快照、内存、磁盘优先级查询。

日志落盘必须绕过缓冲,用 fsync 保证原子写入
Raft 要求日志条目在返回客户端成功前已持久化,否则节点崩溃后可能丢失已提交的日志。C++ 标准库的 std::ofstream 默认带缓冲,直接 write() + close() 不够——close() 不触发 fsync,内核可能延迟刷盘。
实操建议:
- 用
open()系统调用打开文件,带上O_WRONLY | O_CREAT | O_APPEND | O_SYNC(O_SYNC开销大但最简单);或用O_DSYNC(只同步数据不强制同步元数据,性能更好) - 若用 C++ iostream,必须显式调用
flush()+fsync():std::ofstream f("log.bin", std::ios::binary | std::ios::app); f.write(buf, len); f.flush(); ::fsync(f.fileno()); // 注意:需包含 <unistd.h> - 每条日志单独
fsync性能差,可批量写入后一次fsync,但需维护“已提交索引”与“已落盘索引”的映射,避免覆盖未同步条目
Snapshot 截断不能只删旧日志文件,要更新 lastIncludedIndex 和 lastIncludedTerm
快照不是简单压缩打包,而是状态机当前一致点的完整备份。截断日志时若只删除 log_0001 到 log_0010,但没更新 Raft 模块内部的 lastIncludedIndex,下次重启会误判日志起始位置,导致 AppendEntries 请求被拒绝或重放错误日志。
关键动作必须成对发生:
立即学习“C++免费学习笔记(深入)”;
- 生成快照文件(如
snapshot_0015.bin)的同时,把对应状态机的lastIncludedIndex = 15、lastIncludedTerm = 4写入快照元数据头(固定前 16 字节) - 调用
truncateLogTo(lastIncludedIndex)前,先将新快照原子替换旧快照(推荐先写临时文件 +rename()) - 日志文件截断后,需重写日志索引映射表(如有),例如 LevelDB 中的
log_index_to_offset.db必须删除 ≤lastIncludedIndex的所有键
快照序列化别用 memcpy 直拷内存,优先走 protobuf 或自定义二进制协议
状态机对象(比如一个 Map<string, int> 缓存)若直接 memcpy(&obj, sizeof(obj)),会出严重问题:指针成员(如 std::string 内部的 char*)只拷地址,反序列化后访问野指针;虚函数表、RTTI 信息也全丢。
安全做法:
- 用
google::protobuf定义SnapshotDatamessage,调用SerializeToString()—— 它自动处理嵌套、变长字段、字节序 - 若追求零依赖,手写序列化需分两遍:第一遍计算总长度(含字符串长度前缀),第二遍按顺序写入长度+内容;反序列化时严格校验长度字段防越界
- 快照文件末尾追加 CRC32 校验和(非 MD5/SHA),读取时先验 checksum 再加载,避免磁盘位翻转导致状态机静默损坏
日志与快照共存时,readLogEntry(index) 必须查三处:快照、内存缓存、磁盘文件
客户端请求查询 index=12 的日志,此时系统可能:快照截止到 index=10,内存缓存了 index=11~13,磁盘上还有 index=11~20 的文件。硬编码只查磁盘会漏掉快照里已固化的内容,也不知该从哪份数据源取。
标准查询逻辑应为:
- 若
index <= lastIncludedIndex→ 从快照元数据中提取(快照本身不存全部历史日志,但需提供GetStateAt(index)接口供回滚用) - 若
index在内存 buffer 范围内 → 直接返回 - 否则定位到对应日志文件(如
log_0011.bin),用预建的 offset index 二分查找偏移量再pread()
这里最容易被忽略的是快照的“覆盖语义”:index ≤ lastIncludedIndex 的日志条目在状态机层面已不可见,readLogEntry 返回空或抛异常都比返回过期内容安全。


















