etcd 读写概述
etcd 总体是基于 Raft 实现的,本文对 etcd 读写流程关键点进行简单记录。
etcd 读流程
一个读请求从 client 通过 Round-robin 负载均衡算法,选择一个 etcd server 节点,发出 gRPC 请求,经过 etcd server 的 KVServer 模块进入核心的读流程,进行串行读或线性读(默认),通过与 MVCC 的 treeIndex 和 boltdb 模块紧密协作,完成读请求。
- 串行读(非强一致性读):直接读状态机返回数据,无需通过 Raft 协议与集群进行交互,具有低延时、高吞吐量的特点,适合对数据一致性要求不高的场景。
- 线性读(强一致性读):需要经过 Raft 协议模块,反应集群共识的最新数据,因此在延时和吞吐量上相比串行读略差一点,适用于对数据一致性要求高的场景。
ReadIndex
线性读的 ReadIndex 可以使 follower 的读请求不必转发给 leader。它的实现原理是:
- 当 Follower 节点 收到一个线性读请求时,它首先会从 leader 获取集群最新的已提交的日志索引 (committed index);
- leader 收到 ReadIndex 请求时,为防止脑裂等异常场景,会向 follower 节点发送心跳确认,一半以上节点确认 leader 身份后才能将已提交的索引 (committed index) 返回给请求的 follower 节点;
- follower 节点则会等待,直到状态机已应用索引 (applied index) 大于等于 leader 的已提交索引时 (committed Index)才会去通知读请求,数据已赶上 leader,然后去状态机中访问数据。
ReadIndex 是非常轻量的,不会导致 leader 负载变高,ReadIndex 机制使得每个 follower 节点都可以处理读请求,进而提升了系统的整体写性能。
MVCC
它由内存树形索引模块 (treeIndex) 和嵌入式的 KV 持久化存储库 boltdb 组成。treeIndex 模块是基于 Google 开源的内存版 btree 库实现的;boltdb 是个基于 B+ tree 实现的 key-value 键值库,支持事务,提供 Get/Put 等简易 API 给 etcd 操作。
首先,有个全局递增的版本号(put hello a 时,hello 对应的版本号若为 1,下个请求 put world b 时,world 对应的版本号则为 2),每次修改操作,都会生成一个新的版本号。treeIndex 模块保存用户的 key 和相关版本号,以版本号为 key,value 为用户 key-value 数据存储在 boltdb 里面。另外,并不是所有请求都一定要从 boltdb 获取数据,etcd 出于数据一致性、性能等考虑,在访问 boltdb 前,首先会从一个内存读事务 buffer 中,二分查找你要访问 key 是否在 buffer 里面,若命中则直接返回。具体如下图:
etcd 写流程
一个写请求从 client 通过负载均衡算法选择一个 etcd 节点,发出 gRPC 调用,etcd 节点收到请求后会经过 gRPC 拦截、Quota 校验,接着进入 KVServer 模块,KVServer 模块将请求发送给本模块中的 raft,这里负责与 etcd raft 模块进行通信,发起一个提案,内容为 put foo bar,即使用 put 方法将 foo 更新为 bar,提案经过转发之后,半数节点成功持久化,MVCC 模块更新状态机,完成写请求。
与读流程不一样的是写流程涉及 Quota、WAL、Apply 三个模块。
- Quota 模块配额检查 db 的大小,如果超过会报 etcdserver: mvcc: database space exceeded 的告警,通过 Raft 日志同步给集群中的节点 db 空间不足,整个集群将不可写入,对外提供只读的功能。
- 只有 leader 才能处理写请求。leader 收到提案后,会将提案封装成日志条目广播给集群各个节点,同时需要把内容持久化到一个 WAL 日志文件中。日志条目包含 leader 任期号、条目索引、日志类型、提案内容。WAL 持久化内容包含日志条目内容、WAL 记录类型、校验码、WAL 记录的长度。
- Apply 模块用于执行提案,首先会判断该提案是否被执行过,如果已经执行,则直接返回结束;未执行过的情况下,将会进入 MVCC 模块执行持久化提案内容的操作。
MVCC
MVCC 在执行 put 请求时,会基于当前版本号自增生成新的版本号,然后从 treeIndex 模块中查询 key 的创建版本号、修改次数信息。这些信息将填充到 boltdb 的 value 中,同时将用户的 key 和版本号信息存储到 treeIndex。在读流程中介绍过,boltdb 的 value 的值就是①key 名称;②key 创建时的版本号(create_revision)、最后一次修改时的版本号(mod_revision)、key 自身修改的次数(version);③value 值;④租约信息,将这些信息的结构体序列化成的二进制数据。另外,为了提高吞吐量,此时数据并未提交,而是存在 boltdb 所管理的内存数据结构中,由 backend 异步 goroutine 定时将批量事务提交,和 MySQL 类似,写时优先写入 Buffer。具体如下图: