简介
Etcd 是 CoreOS 基于 Raft 协议开发的分布式 key-value 存储。它提供了以下能力:
- 基本的 key-value 存储能力。
- key-value 数据改变的 watch 机制。
- Lease 机制,提供了数据 TTL 的能力。
- 事务能力。
可以使用于以下场景中,
- 键值对数据存储。
- 服务注册和服务发现。
- 基于 watch 机制的分布式异步系统。
整体架构
etcd 按照分层模型,可以分为 Client 层、API 网络层、Raft 层、逻辑层和存储层,如图所示。
Client 层:etcd 提供了简洁易用的 Client 库,封装并提供了操作 KVServer、Cluster、Auth、Lease、Watch 等模块的 API。同时,在 Client 库中支持了负载均衡(etcd 的负载均衡是在 client 端做的)、节点间故障自动转移等,极大了降低业务使用 etcd 的复杂度,提升了开发效率、服务可用性。
clientv3 使用基于 HTTP/2 的 gRPC 协议来与 etcd server 进行通信。相比 v2 的 HTTP/1.x,基于 HTTP/2 的 gRPC 协议具有低延迟、高性能的特点。在 etcd client 的版本 <= 3.3 的时候,当配置多个 endpoints 后,负载均衡算法会从中选择一个 IP 仅创建一个连接,这样做虽然可以节省 etcd server 的总连接数。但是,在 heavy usage 场景下会造成 server 负载不均衡。并且当这个 etcd server 节点异常了,client 访问 etcd server 就会出现异常。特别是在 kubernetes 场景中会导致 API Server 不可用。在 etcd 3.4 中,客户端库对于负载均衡有了改进,它可以更智能地选择和重新连接到不同的 etcd 服务器节点,而不是仅维持一个固定的连接。这样的更改有助于确保即使某个节点出现问题,客户端也可以尝试连接到集群中的其他健康节点,以此来提高整体的可靠性和容错性。
API 网络层:API 网络层则是 Etcd 提供的 API,包括 gRPC API、HTTP API 等,这些 API 用于 client 通信和节点之间的通信。
- 针对 client 的通信来说,client 访问 etcd server 的 API 分为 v2 和 v3 两个大版本。v2 API 使用 HTTP/1.x 协议,v3 API 使用 gRPC 协议。同时 v3 通过 etcd grpc-gateway 组件也支持 HTTP/1.x 协议,便于各种语言的服务调用。
- 针对 server 节点之间的通信来说,包含 server 节点间通过 Raft 算法实现数据复制和 Leader 选举时使用的 API。
Raft 层:Raft 层实现了 Raft 协议中的 leader 选举、日志复制等特性。Raft 层保证了 etcd 多个节点间的数据一致性、提升服务可用性等,是 etcd 的基石。
逻辑层:etcd 核心实现层,如 Quota 模块、KVServer 模块、MVCC 模块、Auth 鉴权模块、Lease 租约模块、Compactor 压缩模块等。这些模块在之后都会依次介绍到。
存储层:存储层包含预写日志(WAL)模块、快照(Snapshot)模块、boltdb 模块。
- WAL 模块用于存储 Raft 日志。
- boltdb 用于保存集群元数据和用户写入的 key value 数据。
核心知识概述
任期(term)
任期相当于 Etcd 中的逻辑时钟。Etcd 将时间划分成一个个任期,任期用连续的整数表示,每个任期从一次选举开始,赢得选举的节点在该任期内充当 Leader 的职责。随着时间的消逝,集群可能会发生新的选举,任期号也会单调递增。通过任期号,可以比较各个节点的数据新旧、识别过期的 Leader 等。
任期可以为第几届的意思,比如某班级第一届(任期为 1)班委中只会有一个班长(leader),当重新选举之后,就是第二届(任期为 2)了,此时会产生新的一个班长。在整个班级中,第二届班长才是有话语权的。
版本号(revsion)
Etcd 有一个全局 revision,是单调递增的整数,代表 etcd 存储空间自启用以来发生的变更次数,是 MVCC(多版本并发控制)的核心,watch 机制也基于 revision 获得该 revision 之后的键值对变化。Etcd 启用的时候,默认版本号是 1。客户端的写入操作(比如 put、delete 等),都会使得 revision 增加。
针对 key-value 来说,它会有以下几个元数据跟 revision 相关,
- create_revision: 表示键值对被创建时的全局 revision 是多少。
- mod_revision:表示键值对最后一次被修改时的全局 revision。这个字段可以用来追踪键值对最近一次修改的时间点。
- version:表示键值对的修改次数。每次对键进行更新时,version 会递增。如果键被删除,然后重新创建 version 会重置为 1。这个字段可以用来追踪一个键值对在其生命周期内发生了多少次更新。
Quota 模块
在进行写入操作的时候,会先经过 Quota 模块的判断。判断此次写入的数据加上当前 DB 的大小之后,是否超过了 DB Quota,如果超过了则拒绝写入。Etcd DB 的 Quota 由参数 --quota-backend-bytes 决定:
- 填 0 的话,使用的是 etcd 默认的 2GB。
- 当填小于 0 的话,会禁用 quota 功能。不建议这么做,因为这会让 db 处于失控状态,导致性能下降。
- 填大于 0 的值时,社区建议不超过 8GB。
当超过 quota 后,相应的解决方法是:
-
调大 quota,也就是调整 --quota-backend-bytes 参数。
-
调大 quota 后,需要发送一个取消 alarm 的命令(etcdctl alarm disarm)以消除告警。这是因为,超过 quota 之后,etcd 会产生一个 NO SPACE 的 alarm(告警)请求,并通过 Raft 日志同步给其他节点,同时会将告警持久化到 db 中。而 Apply 模块在执行 commited proposal 的时候,会先检查当前是否存在这个 alarm,如果有的话则拒绝写入,因此需要将这个 alarm 去掉。
-
检查 etcd 的 compact 功能是否开启、配置策略是否合理。
- compact 的主要作用就是负责回收旧版本的 value,需要回收的主要原因是 etcd 会保存一个 key 所有的变更历史,如果不回收这些 key 的旧版本 value,那么内存和 db 大小会一直膨胀,很容易超过 quota。
- compact 回收时,仅仅只将旧版本占用的空间打上了 Free 标记,并不会真实地回收该空间,这样的好处在于后续新数据写入的时间可以直接复用这块空间,无需申请新的空间。如果需要回收空间,减少 db 大小的话,可以使用碎片整理defrag 命令。它会遍历旧的 db 文件数据,然后重新写入到一个新的 db 文件。但是碎片整理对服务性能会有较大的影响,不建议在生产集群频繁使用。
- compact 支持多种方式配置,包括按时间周期性压缩和保留版本号数量压缩。
WAL 模块
下图是 WAL 日志文件的结构,它由多个 WAL 日志记录顺序追加写入组成。每个记录由类型(Type)、循环冗余校验码(CRC)、日志条目(Data)组成。
WAL 日志记录类型目前支持 5 种:
- 文件元数据记录,包含节点 ID、集群 ID 信息,它在 WAL 文件创建的时候写入。
- 日志条目记录,包含 Raft 日志条目,比如 put proposal 内容。
- 状态信息记录,包含集群的任期号、节点投票信息等,一个 WAL 日志文件会有多条,但是以最后的记录为准。
- CRC 记录,包含上一个 WAL 文件的最后的 CRC 信息,在创建、切割 WAL 日志文件时,作为第一条记录写入到新的 WAL 文件,用于校验数据文件的完整性、准确性。
- 快照记录,包含快照的任期号、日志索引信息,用于检查快照文件的准确性。

MVCC 模块
MVCC 由内存索引模块(treeIndex)和嵌入式的 KV 持久化存储库 boltdb 组成。
-
treeIndex 模块负责保存 key 的历史版本号(revision)信息,保存了 key 和 revision 的映射关系。当 etcd 重新启动的时候,etcd 会重建 treeIndex 模块,它会从 revision 为 1 开始读取 boltdb 中的数据,在某个 revision 未读取到数据的时候则结束。从这里可以看到,Etcd 其实不需要持久化一个全局 revision,因为重新启动的时候都有这个过程,都可以重新获得。
-
boltdb 是一个基于 B+ tree 实现的 key-value 键值数据库,支持事务,提供了 Get/Put 等简易 API,并且它通过 bucket 机制实现了类似 mysql 表的逻辑隔离。在 etcd 中,meta bucket 用来存储 etcd 的元数据信息,key bucket 用来保存用户的 key-value 数据。
在 boltdb 中,revision 被作为 boltdb 中的 key,用户的 key-value 等信息组成的结构体作为 boltdb 中的 value。由于 etcd 每次修改操作,都会递增生成一个新的版本号(revision),因此每次更新都会有新的 key-value 插入到 botldb 中。其中 value 包含以下内容,实际存储的内容是下述内容序列化后的二进制数据。
- key 名称。
- key 创建时的版本号(create_version)、最后一次修改时的版本号(mod_version)、key 自身修改的次数(version),这些信息在重建 treeIndex 索引信息的时候可以用到。
- value 值。
- Lease ID。
其他
- 在 Etcd 中,/a 和 a 是两个不同的 key。因此在 etcd 看来 / 符号并不起到分隔符的作用,它只是普通的字符。但是,使用 / 字符,可以从可阅读性上起到一定区分的作用。
- Lease 的特性,也就是 TTL 的功能在 kubernetes 中没有使用。
- Consul 与 Etcd 类似,但是 Consul 主要是做服务发现的。