etcd 历史版本回溯的方法

小得盈满

2023/11/18

在使用 etcd 作为配置存储或者其他的场景,如果因为误操作对其中 key 的值进行了修改,如果想要找回原来的值,可以利用 etcd 的版本机制进行回溯找回以前的值。在具体操作之前,我们首先获取一下 key 完整的信息来举个例子:

# 如果开启认证要指定 --user <username>:<password>
etcdctl get --endpoints=$ENDPOINTS <key> -w=json

上面 -w=json 是以 JSON 格式输出指定 key 的数据和元数据,例如下面的内容:

{"header":{"cluster_id":6783024485397960691,"member_id":2129759788194751561,"revision":583,"raft_term":5},"kvs":[{"key":"YWJjZA==","create_revision":370,"mod_revision":375,"version":4,"value":"NA=="}],"count":1}

我们首先看 header 里面的 revision 是当前 etcd 集群全局最新的版本号,注意这个版本号是当前存在数据的版本号,也就是最新写入键值的版本号,不是下一个版本号。然后 kvs 里面就是数据了,其中 key, value 都做了 Base64 编码,其中 create_revision 表示这个 key 创建时所对应的全局版本号,mod_revision 是表示当前版本的修订版本号,version 表示当前这个 key 的局部版本号,创建时的版本是 1,version 是 4 也就是说当前这个 key 在创建后修改了 3 次。

假如我们由于误操作而覆盖了这个 key 的值,那么可以就可以基于版本号来进行历史版本的回溯,前提是不能执行 compact,否则为了压缩空间所有的版本数据都会被清理掉,无法再进行恢复了。

然后如果没有合并过,我们就可以执行回溯,以上面的数据为例,创建版本为 370,修改版本为 375,那么我们要获得上一个版本,首先就需要拿 mod_revision - 1 去查询,然后就可以查询到上一个版本:

etcdctl get --endpoints=$ENDPOINTS <key> -w=json --rev=374

注意对于某个 key 来说,版本不是连续的,因为同时有其他 key 也在操作,这点要明白,现在我们拿到的信息如下:

{"header":{"cluster_id":6783024485397960691,"member_id":2129759788194751561,"revision":583,"raft_term":5},"kvs":[{"key":"YWJjZA==","create_revision":370,"mod_revision":373,"version":3,"value":"Mw=="}],"count":1}

可以看到上一个实际的版本是 373,然后我们再继续使用 372 获取:

etcdctl get --endpoints=$ENDPOINTS <key> -w=json --rev=372

结果如下:

{"header":{"cluster_id":6783024485397960691,"member_id":2129759788194751561,"revision":583,"raft_term":5},"kvs":[{"key":"YWJjZA==","create_revision":370,"mod_revision":371,"version":2,"value":"Mg=="}],"count":1}

可以看到上一个版本实际是 371,那么由于创建版本是 370,两者之间不可能再有其他版本号,所以到这里我们就得到了所有的版本号,当然在实际情况下也可能是不连续的,我们要迭代到两者相等为止,所以我们再继续查询一下:

etcdctl get --endpoints=$ENDPOINTS <key> -w=json --rev=370
{"header":{"cluster_id":6783024485397960691,"member_id":2129759788194751561,"revision":583,"raft_term":5},"kvs":[{"key":"YWJjZA==","create_revision":370,"mod_revision":370,"version":1,"value":"MQ=="}],"count":1}

毫无疑问,现在 create_revisionmod_revision 是相等的,那么也就意味着迭代完毕,拿到的版本号就是 370。

所以根据我们的迭代顺序,将版本号序列反转,结果为:370, 371, 373, 375,这样就得到的全部的版本号序列,然后我们就可以按照版本号依次查看 key 具体版本的值了:

# 只需要去掉 -w 选项,正常输出即可
etcdctl get --endpoints=$ENDPOINTS <key> --rev=<revision>

上面就是指定对于指定 key 整个回溯历史版本的过程,需要注意的是 etcd 只能根据全局的 revision 来回溯,不支持按照每个 key 的 version 来回溯,不过局部这个 version 可以用来直观地查看这个 key 被改了几次。

我们也可以根据上面的过程编写一个程序来实现快速查找 key 历史版本的功能,帮助我们快速拿到指定 key 的所有历史记录。

另外也可以使用 watch 的方法来拿到所有的历史记录,比如我们知道这个 key 对应的 create_revision,那么可以 watch 时指定初始版本,这样就会输出所有的历史:

etcdctl watch --endpoints=$ENDPOINTS <key> --rev=<create_revision>

这样就可以一次性拿到所有的修改记录,不过命令是阻塞的,需要手动结束,写程序时也需要做好控制。上面指定的版本号也不一定是 create_revision,指定个比较小的也可以,比如 1。

以上操作基于 etcd 3.5 版本。

基于 Rust 编写的 etcd 版本回溯工具:https://github.com/monchickey/etcd-version-history 可以参考使用。

Reference:

  1. https://etcd.io/docs/v3.5/learning/data_model/
  2. https://github.com/etcd-io/etcd/issues/7292
  3. https://github.com/etcd-io/etcd/issues/11027