etcd 压缩背景
每一次增删改数据,treeIndex 和 boltdb 中都会增加信息记录。长此以往,会导致 etcd 越来越大,进而超过默认限制。
etcd 压缩流程

etcd 支持两种压缩模式,分别是时间周期性压缩和版本号压缩。
压缩的流程为:Compact API 请求 → KV Server → Raft Apply → MVCC 的 Compact 接口执行。
Compact 接口首先会更新当前 server 已压缩的版本号,并将耗时昂贵的压缩任务保存到 FIFO 队列中异步执行。压缩任务执行时,它首先会压缩 treeIndex 模块中的 keyIndex 索引,其次会遍历 boltdb 中的 key,删除已废弃的 key。
etcd 命令行压缩
# 获取etcd当前版本号
$ rev=$(etcdctl endpoint status --write-out="json" | egrep -o '"revision":[0-9]*' | egrep -o '[0-9].*')
$ echo $rev
9
# 执行压缩操作,指定压缩的版本号为当前版本号
$ etcdctl compact $rev
Compacted revision 9
# 压缩一个已经压缩的版本号
$ etcdctl compact $rev
Error: etcdserver: mvcc: required revision has been compacted
# 压缩一个比当前最大版号大的版本号
$ etcdctl compact 12
Error: etcdserver: mvcc: required revision is a future revision- 压缩命令传递的版本号 ⇐ 当前 etcd server 记录的压缩版本号,错误
mvcc: required revision has been compacted - 压缩命令传递的版本号 > 当前 etcd server 最新的版本号,错误
mvcc: required revision is a future revision
etcd 压缩策略选择
一般情况下,配置 etcd 自带的压缩机制即可。包含两种模式,时间周期性压缩和保留版本号的压缩。etcd 会自动的进行压缩操作。
--auto-compaction-mode 'periodic'压缩策略。periodic为时间周期压缩,revision版本号压缩。--auto-compaction-retention '0'保留的时间周期 or 保留的历史版本号数目,‘0’ 关闭自动压缩
若更精细化的操作,可以通过定时任务通过 API 请求 etcd 进行压缩,需要控制好压缩的频率、比例等。
etcd 时间周期性压缩
etcd server 启动后,根据配置的模式 periodic,会创建 periodic Compactor,它会异步的获取、记录过去一段时间的版本号。periodic Compactor 组件获取你设置的压缩间隔参数 1h,并将其划分成 10 个区间,也就是每个区间 6 分钟。每隔 6 分钟,它会通过 etcd MVCC 模块的接口获取当前的 server 版本号,追加到 rev 数组中。
因为你只需要保留过去 1 个小时的历史版本,periodic Compactor 组件会通过当前时间减去上一次成功执行 Compact 操作的时间,如果间隔大于一个小时,它会取出 rev 数组的首元素,通过 etcd server 的 Compact 接口,发起压缩操作。
etcd 版本号压缩
etcd 启动后会根据压缩模式 revision,创建 revision Compactor。revision Compactor 会根据设置的保留版本号数,每隔 5 分钟定时获取当前 server 的最大版本号,减去你想保留的历史版本数,然后通过 etcd server 的 Compact 接口发起如下的压缩操作即可。
etcd 压缩具体流程

Compact 接口逻辑
- 检查版本号是否合理
- 通过 boltdb 的 API 在 meta bucket 中更新当前已调度的压缩版本号
- 将压缩任务追加到 FIFO Scheduled 中,异步调度执行
异步压缩任务逻辑
- 第一,压缩 treeIndex 模块中的各 key 的历史版本、已删除的版本。为了避免压缩工作影响读写性能,首先会克隆一个 B-tree,然后通过克隆后的 B-tree 遍历每一个 keyIndex 对象,压缩历史版本号、清理已删除的版本。压缩完成后,获得有效版本号。
- 第二,删除 boltdb 中废弃的历史版本数据。B+树遍历 key,调用 boltdb delete 接口删除无效 key,更新已经完成的压缩版本号 finishedCompactRev,保存到 boltdb 的 meta bucket 中。
- 任务遍历、删除 key 的过程可能会对 boltdb 造成压力,为了不影响正常读写请求,它在执行过程中会通过参数控制每次遍历、删除的 key 数(默认为 100,每批间隔 10ms),分批完成 boltdb key 的删除操作。
version 保存到 db 的原因
通过持久化存储 scheduledCompactedRev,节点 crash 重启后,会重新向 FIFO Scheduled 中添加压缩任务,已保证各个节点间的数据一致性。
Compact 后 db 大小不减少?
删除大量的 key、事务提交后,boltdb 会释放一定的 branch/leaft page 页面。但是由于调整 db 大小操作是昂贵的,会对性能有较大的损害,所以并不会将这些存储释放给磁盘。
boltdb 是通过 freelist page 记录这些空闲页的分布位置,当收到新的写请求时,优先从空闲页数组中申请若干连续页使用,而不是去申请磁盘空间,从而实现高性能的读写操作。只有当连续空闲页申请无法得到满足的时候, boltdb 才会通过增大 db 大小来补充空闲页。
etcd 压缩 vs 碎片整理
压缩,回收不再需要的数据。碎片整理,将不连续的空间合并。
碎片整理涉及大量数据移动,增加磁盘 I/O 操作,影响其他请求的响应速度。同时,碎片整理期间,会持有 boltdb 事务锁导致读写超时。一般情况下不会执行碎片整理,除非不合理的配置、线上突发大规模写入下导致 db 非常大时需要执行。
如果需要进行处理,可以通过分批而不是一次处理完、低峰期碎片整理的方式来避免影响线上业务。