baidu / braft

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.84k stars 862 forks source link

现在通过 snapshot 持久化 apply index 是不是有问题? #414

Open lygn128 opened 11 months ago

lygn128 commented 11 months ago

1 对于 kv 存储 上次 snap shot 之后,apply 了许多条日志 ,但是重启之后,这些 日志会被重新 apply 了一遍,如果这些日志的操作是 ++i 这种操作的话就是有问题的吧? 谢谢

ehds commented 11 months ago

首先 snapshot 保存的状态是此时此刻 $ApplyIndex_{t}$ 完成时的状态机状态,在做 snapshot 的时候状态机是不会继续往前 apply 的,(串行化同步执行)。

是重启之后,这些 日志会被重新 apply 了一遍

重启时,braft 是会从最近的一次快照进行恢复 load_snapshot , 将状态机恢复到该 snapshot 的 $ApplyIndex{t}$ 是的状态,然后继续执行 $LogIndex{t+1}$ 。

如果这些日志的操作是 ++i 这种操作的话就是有问题的吧?

不会有问题, 因为恢复快照的里面的状态机只保存了快照时的状态,并不会保有快照后的状态,所以即使日志被 apply 了两次,但是都是基于同一个历史在apply,等价于只执行了一次。

例如你的状态机是执行 ++i 的操作。 当状态机执行到第 $3$ 条日志时,此时状态机 $i = 3$, 此时触发快照,快照内容为 $ApplyIndex = 3, i=3$。 然后后又有两条日志被执行,此时 $ApplyIndex=5, i=5$。 这时重启节点,braft 会从快照恢复状态为 $ ApplyIndex = 3, i=3$, 再继续重放后面 2 个日志到 $ApplyIndex=5, i=5$。完成恢复。

在实际情况中,可能真正做 snapshot 是很复杂或者耗时的,快照的执行可能会和状态机异步执行,取决于用户的实现,但是也得保证上述的条件,snapshot 的状态必须是此时此刻的状态(状态机的状态和ApplyIndex),否则在恢复过程中会造成数据的不一致。

lygn128 commented 11 months ago

首先 snapshot 保存的状态是此时此刻 ApplyIndext 完成时的状态机状态,在做 snapshot 的时候状态机是不会继续往前 apply 的,(串行化同步执行)。

是重启之后,这些 日志会被重新 apply 了一遍

重启时,braft 是会从最近的一次快照进行恢复 load_snapshot , 将状态机恢复到该 snapshot 的 ApplyIndext 是的状态,然后继续执行 LogIndext+1 。

如果这些日志的操作是 ++i 这种操作的话就是有问题的吧?

不会有问题, 因为恢复快照的里面的状态机只保存了快照时的状态,并不会保有快照后的状态,所以即使日志被 apply 了两次,但是都是基于同一个历史在apply,等价于只执行了一次。

例如你的状态机是执行 ++i 的操作。 当状态机执行到第 3 条日志时,此时状态机 i=3, 此时触发快照,快照内容为 ApplyIndex=3,i=3。 然后后又有两条日志被执行,此时 ApplyIndex=5,i=5。 这时重启节点,braft 会从快照恢复状态为 $ ApplyIndex = 3, i=3$, 再继续重放后面 2 个日志到 ApplyIndex=5,i=5。完成恢复。

在实际情况中,可能真正做 snapshot 是很复杂或者耗时的,快照的执行可能会和状态机异步执行,取决于用户的实现,但是也得保证上述的条件,snapshot 的状态必须是此时此刻的状态(状态机的状态和ApplyIndex),否则在恢复过程中会造成数据的不一致。

多谢大佬,这么做的话确实可以解决 重复 apply 的问题; 具体对于如果要使用 rocksdb 存储的系统,这种是不是会带来空间的浪费?要保存两份数据,一份 rocksdb 的数据,一份 rocksdb 快照的数据,每次重启后相当于删除掉rocksdb 的数据,然后从快照 apply ? 谢谢

lygn128 commented 11 months ago

rocksdb 快照硬链接会减少一部分空间,但如果持续性的写入,快照对应的文件一直删除不掉,应该依旧会带来空间的浪费吧?

ehds commented 11 months ago

应该依旧会带来空间的浪费吧

我们之前的做法是在 SaveSnapshot 的时候通过 RocksDB 的 snapshot 拿到当时的状态,然后遍历 DBIter 写到 SSTFile。这中做法虽然能保证snapshot的正确性,但是会有你提到的空间浪费,数据的大小最坏的情况可能占有实际 数据量3倍(2个快照+1个数据库文件)。而且 Iter 也会有很大的读开销。

后面我们使用的是 RocksDB 提供的 CheckPoint 功能,也就是你说的硬链接的方式。这种方式的好处是 快照里面的数据文件 和此时数据库的数据文件 有相同 FileNumber 的是共享的。只有不同的文件(即 snapshot 之后新增/删除 也就是 由 Flush/Compaction 产生/删除的)会占用一定的磁盘空间, 但是这部分文件实际占有总数据量很少的一部分 (10%)左右,主要取决于你数据库的负载和 snapshot 的频率,已经是可以接受的范围。

lygn128 commented 11 months ago

应该依旧会带来空间的浪费吧

我们之前的做法是在 SaveSnapshot 的时候通过 RocksDB 的 snapshot 拿到当时的状态,然后遍历 DBIter 写到 SSTFile。这中做法虽然能保证snapshot的正确性,但是会有你提到的空间浪费,数据的大小最坏的情况可能占有实际 数据量3倍(2个快照+1个数据库文件)。而且 Iter 也会有很大的读开销。

后面我们使用的是 RocksDB 提供的 CheckPoint 功能,也就是你说的硬链接的方式。这种方式的好处是 快照里面的数据文件 和此时数据库的数据文件 有相同 FileNumber 的是共享的。只有不同的文件(即 snapshot 之后新增/删除 也就是 由 Flush/Compaction 产生/删除的)会占用一定的磁盘空间, 但是这部分文件实际占有总数据量很少的一部分 (10%)左右,主要取决于你数据库的负载和 snapshot 的频率,已经是可以接受的范围。

谢大佬,我看有的实现是,在rocksdb 每次插入数据的时候,都吧 applay index 和数据 batch 写入了,不确定这个对性能影响有多大

ehds commented 11 months ago

这种实现有个问题是 rocksdb 的 wal 压力会比较大,相当于每次写都要把当前 applyindex 写一次到wal。