etcd 功能模块

各层功能如下:
- Client 层:Client 层包括 client v2 和 v3 两个大版本 API 客户端库,提供了简洁易用的 API,同时支持负载均衡、节点间故障自动转移,可极大降低业务使用 etcd 复杂度,提升开发效率、服务可用性。
- API 网络层:API 网络层主要包括 client 访问 server 和 server 节点之间的通信协议。一方面,client 访问 etcd server 的 API 分为 v2 和 v3 两个大版本。v2 API 使用 HTTP/1.x 协议,v3 API 使用 gRPC 协议。同时 v3 通过 etcd grpc-gateway 组件也支持 HTTP/1.x 协议,便于各种语言的服务调用。另一方面,server 之间通信协议,是指节点间通过 Raft 算法实现数据复制和 Leader 选举等功能时使用的 HTTP 协议。
- Raft 算法层:Raft 算法层实现了 Leader 选举、日志复制、ReadIndex 等核心算法特性,用于保障 etcd 多个节点间的数据一致性、提升服务可用性等,是 etcd 的基石和亮点。
- 功能逻辑层:etcd 核心特性实现层,如典型的 KVServer 模块、MVCC 模块、Auth 鉴权模块、Lease 租约模块、Compactor 压缩模块等,其中 MVCC 模块主要由 treeIndex 模块和 boltdb 模块组成。
- 存储层:存储层包含预写日志(WAL)模块、快照(Snapshot)模块、boltdb 模块。其中 WAL 可保障 etcd crash 后数据不丢失,boltdb 则保存了集群元数据和用户写入的数据。
etcd 默认读(线性读)的执行流程

etcd 读逻辑执行流程
- etcdctl 会对命令中参数解析,然后创建一个 clientv3 库对象来使用 KVServer 模块的 API 来访问 etcd server。其中,clientv3 库基于 gRPC client API 封装了操作 etcd KVServer、Cluster、Auth、Lease、Watch 等模块的 API,同时还包含了负载均衡、健康探测和故障切换等特性。
- 选择好 etcd server 节点,client 就可调用 etcd server 的 KVServer 模块的 Range RPC 方法,把请求发送给 etcd server。其中,client 与 server 之间的通信使用的是基于 HTTP/2 的 gRPC 协议。
- KVServer 模块收到线性读请求后,通过架构图中流程三向 Raft 模块发起 ReadIndex 请求,Raft 模块将 Leader 最新的已提交日志索引封装在流程四的 ReadState 结构体,通过 channel 层层返回给线性读模块,线性读模块等待本节点状态机追赶上 Leader 进度,追赶完成后,就通知 KVServer 模块,进行架构图中流程五,与状态机中的 MVCC 模块进行进行交互了。
- 先从 treeIndex 中读取 key 对应的版本号,然后读 buffer,未命中再读取 boltdb。
KVServer 模块
拦截器
etcd 提供了丰富的 metrics、日志、请求行为检查等机制,可记录所有请求的执行耗时及错误码、来源 IP 等,也可控制请求是否允许通过,比如 etcd Learner 节点只允许指定接口和参数的访问,帮助大家定位问题、提高服务可观测性等。etcd 通过拦截器来执行这方面的处理,例如 etcd 判断要求执行一个操作前集群必须有 Leader;请求延时超过阈值后打印日志。
串行读与线性读
串行读:直接读状态机数据返回、无需通过 Raft 协议与集群进行交互的模式。适用于数据敏感度较低的场景。
线性读:数据更改后,所有节点的数据都是最新的,不存在旧数据。适用于数据敏感度高的场景,默认读模式,对应图中的 3、4 流程。
线性读的实现方式是基于 ReadIndex。
- Follower 节点收到线性读请求,
- 先从 Leader 中获取集群最新的已提交的日志索引(committed index)。
- Leader 收到 ReadIndex 请求时,为防止脑裂等异常场景,会向 Follower 节点发送心跳确认,一半以上节点确认 Leader 身份后才能将已提交的索引(committed index)返回给节点。
- 节点等待状态机已应用索引(applied index)大于等于 Leader 的已提交索引时(committed Index),然后去通知读请求,数据已经赶上 Leader,可以去状态机中访问数据。
这就是 Raft 协议中,如果客户端访问 Follower 节点时如何保证数据是最新的其中一个办法。另一个办法就是将请求转发给 Leader。
MVCC
MVCC 核心由内存树形索引模块(treeIndex)和嵌入式的 KV 持久化存储库 boltdb 组成。
treeIndex 模块是基于 Google 开源的内存版 btree 库实现的,只存储 key 的版本号,相对于 ZooKeeper 和 etcd v2 全内存存储,内存要求更低。
boltdb 是基于 B+ 树实现的 key-value 键值库,支持事务,提供 get/put 等简易 API。
假设当前 key-value 存储历史版本有两种:第一种,一个 key 保存多个历史版本;第二种,每次修改,生成 version,以版本号为 key,value 为用户 key-value 等信息组成的结构体。第一种方法,value 较大,存在明显读写放大、并发冲突等问题。第二种是 etcd 目前用的。
treeIndex 中存储了 key 的版本号,boltdb 中存储了每个版本号对应的修改项目。当一个读操作来到时,先从 treeIndex 中读取 key 对应的版本号,然后读内存 buffer,未命中再读取 boltdb。

那么在 boltdb 是如何隔离集群元数据与用户数据的呢?答案是 bucket。boltdb 里每个 bucket 类似对应 MySQL 一个表,用户的 key 数据存放的 bucket 名字的是 key,etcd MVCC 元数据存放的 bucket 是 meta。
需要注意的是,etcd 在启动的时候,会通过 mmap 机制将 etcd db 文件映射到 etcd 进程地址空间,并设置了 mmap 的 MAP_POPULATE flag,它会告诉 Linux 内核预读文件,Linux 内核会将文件内容拷贝到物理内存中,此时会产生磁盘 1/0。节点内存足够的请求下,后续处理读请求过程中就不会产生磁盘 I/O 了。
若 etcd 节点内存不足,可能会导致 db 文件对应的内存页被换出,当读请求命中的页未在内存中时,就会产生缺页异常,导致读过程中产生磁盘 IO,你可以通过观察 etcd 进程的 majift 字段来判断 etcd 是否产生了主缺页中断。
综上,boltdb 在请求没有命中 buffer 的时候,并不一定需要访问磁盘文件,触发磁盘 I/O。
本地体验 etcd
环境准备
goreman 可以快速创建、停用本地的多节点 etcd 集群。
你可以通过如下 go get 命令快速安装 goreman,然后从 etcd release 页下载 etcd v3.4.9 二进制文件,再从 etcd 源码中下载 goreman Procfile 文件,它描述了 etcd 进程名、节点数、参数等信息。最后通过 goreman -f Procfile start 命令就可以快速启动一个 3 节点的本地集群了。
go get github.com/mattn/goreman
命令尝试
启动完 etcd 集群后,当你用 etcd 的客户端工具 etcdctl 执行一个 get hello 命令(如下)时,对应到图中流程一,etcdctl 是如何工作的呢?
etcdctl get hello --endpoints http://127.0.0.1:2379
hello
world