随着你不停更新、删除,你的 etcd 进程内存占用和 db 文件就会越来越大。很显然,这会导致 etcd OOM 和 db 大小增长到最大 db 配额,最终不可写。

整体架构

image.png

目前 etcd 支持两种压缩模式,分别是时间周期性压缩和版本号压缩。

  1. 当你通过 API 发起一个 Compact 请求后,KV Server 收到 Compact 请求提交到 Raft 模块处理,在 Raft 模块中提交后,Apply 模块就会通过 MVCC 模块的 Compact 接口执行此压缩任务。
  2. Compact 接口首先会更新当前 server 已压缩的版本号,并将耗时昂贵的压缩任务保存到 FIFO 队列中异步执行。压缩任务执行时,它首先会压缩 treeIndex 模块中的 keyIndex 索引,其次会遍历 boltdb 中的 key,删除已废弃的 key。

压缩特性初体验

在使用 etcd 过程中,当你遇到”etcdserver: mvcc: database space exceeded“错误时,若是你未开启压缩策略导致 db 大小达到配额,这时你可以使用 etcdctl compact 命令,主动触发压缩操作,回收历史版本。

先通过 endpoint status 命令获取 etcd 当前版本号,然后再通过 etcdctl compact 命令发起压缩操作即可:

  1. # 获取etcd当前版本号
  2. $ rev=$(etcdctl endpoint status --write-out="json" | egrep -o '"revision":[0-9]*' | egrep -o '[0-9].*')
  3. $ echo $rev
  4. 9
  5. # 执行压缩操作,指定压缩的版本号为当前版本号
  6. $ etcdctl compact $rev
  7. Compacted revision 9
  8. # 压缩一个已经压缩的版本号
  9. $ etcdctl compact $rev
  10. Error: etcdserver: mvcc: required revision has been compacted
  11. # 压缩一个比当前最大版号大的版本号
  12. $ etcdctl compact 12
  13. Error: etcdserver: mvcc: required revision is a future revision

请注意,如果你压缩命令传递的版本号小于等于当前 etcd server 记录的压缩版本号,etcd server 会返回已压缩错误 (“mvcc: required revision has been compacted”) 给 client。如果版本号大于当前 etcd server 最新的版本号,etcd server 则返回一个未来的版本号错误给 client(“mvcc: required revision is a future revision”)。

  • 压缩的本质是回收历史版本,目标对象仅是历史版本,不包括一个 key-value 数据的最新版本,因此你可以放心执行压缩命令,不会删除你的最新版本数据。
  • 不过我在08介绍 Watch 机制时提到,Watch 特性中的历史版本数据同步,依赖于 MVCC 中是否还保存了相关数据,因此我建议你不要每次简单粗暴地回收所有历史版本

如何实现精细化控制呢?

  • 一种是使用 etcd server 的自带的自动压缩机制,根据你的业务场景,配置合适的压缩策略即可
  • 另外一种方案是基于 etcd 的 Compact API,在业务逻辑代码中、或定时任务中主动触发压缩操作

建议使用 etcd 自带的压缩机制.

周期性压缩

适用场景:

  • 当你希望 etcd 只保留最近一段时间写入的历史版本时,你就可以选择配置 etcd 的压缩模式为 periodic,保留时间为你自定义的 1h 等。

如何给 etcd server 配置压缩模式和保留时间呢?

  • etcd server 提供了配置压缩模式和保留时间的参数
  1. --auto-compaction-retention '0'
  2. Auto compaction retention length. 0 means disable auto Compaction.
  3. --auto-compaction-mode 'periodic'
  4. Interpret 'auto-Compaction-retention' one of: periodic|revision.
  • auto-compaction-mode 为 periodic 时,它表示启用时间周期性压缩,auto-compaction-retention 为保留的时间的周期,比如 1h。
  • auto-compaction-mode 为 revision 时,它表示启用版本号压缩模式,auto-compaction-retention 为保留的历史版本号数,比如 10000。

注意:

  • etcd server 的 auto-compaction-retention 为’0’时,将关闭自动压缩策略

etcd 是如何知道你配置的 1h 前的 etcd server 版本号呢?

  • etcd server 启动后,根据你的配置的模式 periodic,会创建 periodic Compactor,它会异步的获取、记录过去一段时间的版本号
  • 如果间隔大于一个小时,它会取出 rev 数组的首元素,通过 etcd server 的 Compact 接口,发起压缩操作

注意:

  • 在 etcd v3.3.3 版本之前,不同的 etcd 版本对周期性压缩的行为是有一定差异的,具体的区别你可以参考下官方文档

版本号压缩

适用场景:

  • 当你写请求比较多,可能产生比较多的历史版本导致 db 增长时,或者不确定配置 periodic 周期为多少才是最佳的时候,你可以通过设置压缩模式为 revision,指定保留的历史版本号数。

实现原理:

  • etcd 启动后会根据你的压缩模式 revision,创建 revision Compactor。revision Compactor 会根据你设置的保留版本号数,每隔 5 分钟定时获取当前 server 的最大版本号,减去你想保留的历史版本数,然后通过 etcd server 的 Compact 接口发起如下的压缩操作即可。
  1. # 获取当前版本号,减去保留的版本号数
  2. rev := rc.rg.Rev() - rc.retention
  3. # 调用server的Compact接口压缩
  4. _err := rc.c.Compact(rc.ctx,&pb.CompactionRequest{Revision: rev})

压缩原理

Compact 请求经过 Raft 日志同步给多数节点后,etcd 会从 Raft 日志取出 Compact 请求,应用此请求到状态机执行。

  1. MVCC 模块的 Compact 接口首先会检查 Compact 请求的版本号 rev 是否已被压缩过,若是则返回 ErrCompacted 错误给 client。其次会检查 rev 是否大于当前 etcd server 的最大版本号,若是则返回 ErrFutureRev 给 client
  2. 通过检查后,Compact 接口会通过 boltdb 的 API 在 meta bucket 中更新当前已调度的压缩版本号 (scheduledCompactedRev) 号,然后将压缩任务追加到 FIFO Scheduled 中,异步调度执行。

image.png

为什么 Compact 接口需要持久化存储当前已调度的压缩版本号到 boltdb 中呢?

  • 如果不保存这个版本号,etcd 在异步执行的 Compact 任务过程中 crash 了,那么异常节点重启后,各个节点数据就会不一致。

异步的执行压缩任务会做哪些工作呢?

  1. treeIndex 索引模块,它是 etcd 支持保存历史版本的核心模块,每个 key 在 treeIndex 模块中都有一个 keyIndex 数据结构,记录其历史版本号信息。

image.png

  1. 异步压缩任务的第一项工作,就是压缩 treeIndex 模块中的各 key 的历史版本、已删除的版本。为了避免压缩工作影响读写性能,首先会克隆一个 B-tree,然后通过克隆后的 B-tree 遍历每一个 keyIndex 对象,压缩历史版本号、清理已删除的版本
    1. 假设当前压缩的版本号是 CompactedRev, 它会保留 keyIndex 中最大的版本号,移除小于等于 CompactedRev 的版本号,并通过一个 map 记录 treeIndex 中有效的版本号返回给 boltdb 模块使用。

为什么要保留最大版本号呢?

  • 最大版本号是这个 key 的最新版本,移除了会导致 key 丢失。而 Compact 的目的是回收旧版本。
  • 当然如果 keyIndex 中的最大版本号被打了删除标记 (tombstone), 就会从 treeIndex 中删除这个 keyIndex,否则会出现内存泄露。
  1. Compact 任务执行完索引压缩后,它通过遍历 B-tree、keyIndex 中的所有 generation 获得当前内存索引模块中有效的版本号,这些信息将帮助 etcd 清理 boltdb 中的废弃历史版本。

image.png

  1. 压缩任务的第二项工作就是删除 boltdb 中废弃的历史版本数据。如上图所示,它通过 etcd 一个名为 scheduleCompaction 任务来完成。
  2. scheduleCompaction 任务会根据 key 区间,从 0 到 CompactedRev 遍历 boltdb 中的所有 key,通过 treeIndex 模块返回的有效索引信息,判断这个 key 是否有效,无效则调用 boltdb 的 delete 接口将 key-value 数据删除。
  3. 在这过程中,scheduleCompaction 任务还会更新当前 etcd 已经完成的压缩版本号 (finishedCompactRev),将其保存到 boltdb 的 meta bucket 中。
    1. scheduleCompaction 任务遍历、删除 key 的过程可能会对 boltdb 造成压力,为了不影响正常读写请求,它在执行过程中会通过参数控制每次遍历、删除的 key 数(默认为 100,每批间隔 10ms),分批完成 boltdb key 的删除操作。

为什么压缩后 db 大小不减少呢?

当我们通过 boltdb 删除大量的 key,在事务提交后 B+ tree 经过分裂、平衡,会释放出若干 branch/leaf page 页面,然而 boltdb 并不会将其释放给磁盘,调整 db 大小操作是昂贵的,会对性能有较大的损害。

小结

  • etcd 压缩操作可通过 API 人工触发,也可以配置压缩模式由 etcd server 自动触发。
  • 压缩模式支持按周期和版本两种
    • 在周期模式中你可以实现保留最近一段时间的历史版本数
    • 在版本模式中你可以实现保留期望的历史版本数。

压缩的核心工作原理分为两大任务:

  • 第一个任务是压缩 treeIndex 中的各 key 历史索引,清理已删除 key,并将有效的版本号保存到 map 数据结构中。
  • 第二个任务是删除 boltdb 中的无效 key。基本原理是根据版本号遍历 boltdb 已压缩区间范围的 key,通过 treeIndex 返回的有效索引 map 数据结构判断 key 是否有效,无效则通过 boltdb API 删除它。

在执行压缩的操作中,虽然我们删除了 boltdb db 的 key-value 数据,但是 db 大小并不会减少