DB Quota 概述
Etcd DB 的 quota 是由参数 --quota-backend-bytes 决定:
- 填 0 的话,使用的是 etcd 默认的 2GB。
- 当填小于 0 的话,会禁用 quota 功能。不建议这么做,因为这会让 db 处于失控状态,导致性能下降。
- 填大于 0 的值时,社区建议不超过 8GB(可以参考下文)。
以下几种情况会导致 etcd db 超过 quota:
-
集群规模与 quota 的配置不匹配,前者过大后者过小。db 配置的值较小,或者采用默认配置时,当集群规模增大、写入的 QPS 增多之后,etcd db 的大小就可能会很快超过 2G。
-
没有配置 compact 策略或者 compact 配置策略不当。etcd v3 是一个 MVCC 数据库,保存了 key 的历史版本。如果未配置 compact 策略的话,db 大小会不断增大,超过 quota。
当超过 quota 后,相应的解决方法是:
-
调大 quota,也就是调整 --quota-backend-bytes 的参数。
-
调大 quota 后,需要发送一个取消 alarm 的命令(etcdctl alarm disarm)以消除告警。这是因为,超过 quota 之后,etcd 会产生一个 NO SPACE 的 alarm(告警)请求,并通过 Raft 日志同步给其他节点,同时会将告警持久化到 db 中。而 Apply 模块在执行 commited proposal 的时候,会先检查当前是否存在这个 alarm,如果有的话则拒绝写入,因此需要将这个 alarm 去掉。
-
检查 etcd 的 compact 功能是否开启、配置策略是否合理。
为什么社区建议 db 文件不超过 8GB
Etcd 社区对 db 大小是不建议超过 8GB,主要的考虑是大数据量对 Etcd 集群会有以下影响,
-
启动耗时。
etcd 启动的时候,会打开 db 文件,读取 db 文件中所有的 key-value 数据,用于重建内存 treeIndex 模块。因此大量的 key 会导致 etcd 启动过慢。
etcd 重建 treeIndex 模块的过程如下,
- 首先是启动 goroutine。它会负责遍历 boltdb,获取所有 key-value 数据,并将其反序列化为 etcd 的 mvccpb.KeyValue 结构。由于 etcd 中存储的 key 是 revesion,是有序的,因此它会从 1 开始批量遍历,每次查询 10000 条 key-value 记录,直到查询数据为空。
- 其次是构建 treeIndex 索引的 goroutine。它从主 goroutine 获取 mvccpb.KeyValue 数据,基于 key、版本号、是否带删除标识等信息,构建 keyIndex 对象,并插入到 treeIndex 模块的 B-tree 中。因可能存在多个 goroutine 并发操作 treeIndex,比如 compaction 异步任务也会操作 treeIndex,因此 treeIndex 的 Insert 函数会加锁。但是,需要注意的是 etcd 启动时,只有一个 goroutine 负责构建 treeIndex 索引。
-
节点内存配置。
etcd 启动时,会通过 boltdb 的 Open API 获取数据库对象,而 Open API 会通过 mmap 机制将 db 文件映射到内存中。由于设置了 mmap 的 MAP_POPULATE flag,此时会预先将待映射文件的内容,在这里就是 db 文件,加载到内存中,而不是在实际访问页面时才进行按需加载。因此在节点内存充足的情况下,db 文件的内容会全被加载到内存中,此时看到的 etcd 占用内存,一般是 db 文件大小与内存 treeIndex 之和。client 后续发起的 etcd 读操作,都是直接通过内存获取到 boltdb 的 key-value 数据,不会产生任何磁盘 IO,具备良好的读性能、稳定性。
但是,当节点内存空间不足的时候,比如当 db 文件超过节点内存配置时,若查询的 key 所相关的 branch page、 leaf page 都不在内存时,就会频繁触发主缺页中断,导致读延时抖动、QPS 下降。
因此为了保证 etcd 集群性能的稳定性,建议 etcd 节点内存规格要至少大于 etcd db 文件大小。
-
快照。
当 Follower 节点落后 Leader 较多数据的时候(比如新加入一个 etcd 节点的时候),会通过快照传输(snapshotting)的方式快速同步状态。
- 此时 Leader 会从 boltdb 中读取内容来生成快照内容,通常涉及将整个键值存储序列化到一个快照文件中。此时的快照内容包含了能够重建 boltdb 的所有信息,包括键值数据和必要的元数据,如快照时的索引和任期(term)。因此较大的 db 文件会导致 Leader 生成快照消耗较多的 CPU。
- Leader 生成快照后,会将快照分割成小文件,发送给 Follwer。此时会消耗较多的网络带宽资源。
- Follower 在收到快照之后,会进行重建,此过程耗时会较长。并且由于耗时较长,在集群写 QPS 较大的时候,基于快照重建后的 Follower 依然无法通过正常的日志复制模式来追赶 Leader,只能继续触发 Leader 生成快照,进而进入死循环, Follower 一直处于异常中。
-
treeIndex 索引性能。
etcd 不支持数据分片(shard),所有的信息都保存在内存中的 treeIndex 上。如果 db 文件过大,也就意味着有几十万到上千万的 key。此时查询、修改操作的延时都会增加。
-
boltdb 性能。
boltdb 在提交事务(commit)时偶尔会出现较高延时。这个在上述讲 freelist 的时候提到过。
-
一方面是因为提交(commit)的时候,B+tree 会进行重平衡和分裂,此时可能会从 freelist 中申请若干连续的 page 存储数据,或释放空闲的 page。当申请一个连续的 n 个 page 存储数据时,它会遍历 boltdb 中所有的空闲页,直到找到连续的 n 个 page。因此它的时间复杂度是 O(N)。若 db 文件较大,又存在大量的碎片空闲页,则会导致延时增加,甚至很可能导致超时。同时也可能会释放若干个 page 给 freelist,在合并到 freelist 时,时间复杂度是 O(NLog N)。
针对这个问题,etcd 社区在 bbolt 项目中,实现了基于 hashmap 来管理 freelist。通过引入了如下的三个 map 数据结构(freemaps 的 key 是连续的页数, value 是空闲页的起始页 pgid 集合,forwardmap 和 backmap 用于释放的时候快速合并页),将申请和释放时间复杂度降低到了 O(1)。
freemaps map[uint64]pidSet forwardMap map[pgid]uint64 backwardMap map[pgid]uint64
现在可以通过 bbolt 的 FreeListType 参数来控制使用 array 还是 hashmap。在 etcd 3.4 版本中还是 array,3.5 版本将默认是 hashmap。
-
另一方面,db 中若存在大量空闲页,持久化 freelist 也需要消耗较多的 db 大小,并会导致额外的事务提交延时。
bbolt 支持启动时扫描全部 page 来构造 freelist,降低了 db 大小和提升写事务提交的性能(但是它会带来 etcd 启动延时的上升)。此行为可以通过 bbolt 的 NoFreelistSync 参数来控制,默认是 true 启用此特性。
-
-
集群稳定性。
db 文件增大后,另外一个非常大的隐患是用户 client 发起的 expensive request,容易导致集群出现各种稳定性问题。本质原因是 etcd 不支持数据分片,各个节点保存了所有 key-value 数据,同时它们又存储在 boltdb 的一个 bucket 里面。当集群含有百万级以上 key 的时候,任意一种 expensive read 请求都可能导致 etcd 出现 OOM、丢包等情况发生,而这主要原因是因为 etcd 会将查询的结果先缓存在内存中。比如,
- count only 查询。在 etcd 3.5 版本之前,统计 key 数会遍历 treeIndex,把 key 追加到数组中。然而当数据规模较大时,追加 key 到数组中的操作会消耗大量内存,同时数组扩容时涉及到大量数据拷贝,会导致延时上升。
- limit 查询。在 etcd 3.5 版本之前,当执行范围查询(例如,用于获取键的范围)时,如果用户指定了一个 limit 参数来限制返回结果的数量,etcd 的行为是首先检索整个范围的键,然后在 API 层应用 limit,这意味着即使用户只需要少量结果,etcd 也需要从 treeIndex 中检索更多的项,导致内存增加和时间消耗。etcd 3.5 中将 limit 参数下推到了 treeIndex,实现了查询性能百倍提升。简单来说,就是在处理查询时,它会考虑 limit 参数,当遍历达到限制数量的键值对时就返回。
- 大包查询。比如未分页批量遍历 key-value 数据(比如一次查询万级别的 key 时)或单 key-value 数据较大的时候。由于会将 key-value 保存到查询结果数据结构中,会导致内存占用变高。随着请求 QPS 增大,极易出现 OOM、丢包等。 etcd 这块未来的优化点是实现流式传输。