An industrial-grade C++ implementation of RAFT consensus algorithm based on brpc, widely used inside Baidu to build highly-available distributed systems.
Apache License 2.0
3.99k
stars
886
forks
source link
Snapshot logic has flaws in scenarios where the disk is full #461
问题背景
我们的线上服务使用了braft框架, 在运行期间服务所在的机器磁盘使用量达到上限, 查看日志, 在磁盘达到上限的期间有部分关于snapshot的异常日志, 随后我们扩容了磁盘, 并且重启了服务, 但是故障恢复期间在log_manager.cpp内部一直校验失败, 服务无法正常启动.
原因分析
通过查看本地目录结构, 发现最近一次成功生成的快照目录名称为
snapshot_00000000000000009354
, 快照的生成时间是20240701 11:11:20.936393
, 说明重启之后加载该快照后应该从点位9355
处的日志开始进行回放, 但是log中的第一条有效日志的编号却是10709, 说明日志有缺失(缺失的区间[9355, 10709)
), 校验确实应该失败, 不允许启动成功.然后我们去看了最后一次成功生成快照的时间点之后的日志, 发现在
20240701 13:11:54.375097
和20240701 13:35:03.102234
这个两个时间点都触发了快照, 并且当时由于磁盘已经满了, 导致产生了部分错误日志(两次快照的日志差不多, 这里截取20240701 13:11:54.375097
的日志进行分析)从这里可以看出来在快照期间
__raft_snapshot_meta
存盘操作失败了, 所以后面删除了新生成的temp快照目录(此时最近一次成功快照实际上还是snapshot_00000000000000009354
), 但是快照流程还是在继续, 后续清理了区间[9352, 10083]
之间的日志并且更新了log_meta
文件中的点位信息到10090
, 此时快照数据就已经和日志对应不上了(实际上由于日志的清理, 这里已经存在丢失数据的问题了).函数调用链
在业务层快照逻辑完成之后框架会回调
on_snapshot_save_done()
函数:其中有一段逻辑是
_snapshot_storage
调用close进行快照元信息的存盘以及快照目录的rename操作:writer->sync()
的内部实现实际上就是将快照的元信息进行存盘操作, 在磁盘满导致内部存盘失败的场景下, 这个函数的返回值是-1
, 然后跳到函数末尾判断return ret != EIO ? 0 : -1;
, 这里实现实际上是有严重问题的, 只有ret的值等于EIO(5)
的时候才认为出错了, 如果ret为-1
, 实际上close()
函数返回了0, 静默了内部错误, 导致上层认为close()
函数正常执行, 并且进行了raft 日志的清理工作.最终导致了实际上快照目录没有更新(一直是最后一次成功快照生成的目录, 也就是
20240701 11:11:20.936393
时刻生成的snapshot_00000000000000009354
), 但是raft日志缺被清理掉了, 并且log_meta
的文件中记录的点位一直后移, 造成快照目录数据和raft日志接不上的问题.修复办法
问题分析清楚之后实际上修复办法还是比较简单的, 当
writer->sync()
失败之后, 将ret设置成EIO
, 将错误向上层抛, 而不是静默错误让上层继续清理braft日志.