1. 读写总体概述
etcd
各个模块交互的总览,如下图所示:
总体上的请求流程从上至下依次为客户端 → API
接口层 → etcd Server
→ etcd raft
算法库。
- 读请求
客户端通过负载均衡选择一个 etcd
节点发出读请求,API
接口层提供了 Range RPC
方法,etcd
服务端拦截到 gRPC
读请求后,调用相应的处理器处理请求。
- 写请求
客户端通过负载均衡选择一个 etcd
节点发起写请求,etcd
服务端拦截到 gRPC
写请求,涉及一些校验和监控,之后 KVServer
向 raft
模块发起提案,内容即为写入数据的命令。经过网络转发,当集群中的多数节点达成一致并持久化数据后,状态变更且 MVCC
模块执行提案内容。
2. 读操作整体流程
将整个读操作划分成如下几个步骤:
-
etcdctl
会创建一个clientv3
库对象,选取一个合适的etcd
节点; -
调用
KVServer
模块的Range RPC
方法,发送请求; -
拦截器拦截,主要做一些校验和监控;
-
调用
KVServer
模块的Range
接口获取数据;
接着就进入了读请求的核心步骤,会经过线性读 ReadIndex
模块、MVCC
(包含 treeIndex
和 BlotDB
)模块。
线性读是相对串行读来讲的概念。集群模式下会有多个
etcd
节点,不同节点之间可能存在一致性问题,串行读直接返回状态数据,不需要与集群中其他节点交互。这种方式速度快,开销小,但是会存在数据不一致的情况。
线性读则需要集群成员之间达成共识,存在开销,响应速度相对慢。但是能够保证数据的一致性,etcd 默认读模式是线性读。
继续往下,看看如何读取 etcd
中的数据。etcd
中查询请求,查询单个键或者一组键,以及查询数量,到了底层实际都会调用 Range keys
方法。
Range
请求的结构图如下所示:
从上至下,查询键值对的流程包括:
-
在
treeIndex
中根据键利用BTree
快速查询该键对应的索引项keyIndex
,索引项中包含Revision
; -
根据查询到的版本号信息
Revision
,在Backend
的缓存Buffer
中利用二分法查找,如果命中则直接返回; -
若缓存中不符合条件,在
BlotDB
中查找(基于BlotDB
的索引),查询之后返回键值对信息。
图中 ReadTx
和 BatchTx
是两个接口,用于读写请求。在创建 Backend
结构体时,默认也会创建 readTx
和 batchTx
,readTx
实现了 ReadTx
,负责处理只读请求;batchTx
实现了 BatchTx
接口,负责处理读写请求。
总结客户端发起读请求之后的处理流程,如下图所示:
-
客户端发起请求之后,
clientv3
首先会根据负载均衡算法选择一个合适的etcd
节点,接着调用KVServer
模块对应的RPC
接口,发起Range
请求的gRPC
远程调用; -
gRPC Server
上注册的拦截器拦截到Range
请求,实现Metrics
统计、日志记录等功能; -
然后进入读的主要过程,
etcd
模式实现了线性读,使得任何客户端通过线性读都能及时访问到键值对的更新; -
线性读获取到
Leader
已提交日志索引构造的最新ReadState
对象,实现本节点状态机的同步; -
接着就是调用
MVCC
模块,根据treeIndex
模块B-tree
快速查找key
对应的版本号; -
通过获取的版本号作为
key
,查询存储在boltdb
中的键值对;
3. 写操作整体流程
将整个写操作划分成如下几个步骤:
-
客户端通过负载均衡算法选择一个
etcd
节点,发起gRPC
调用; -
etcd Server
收到客户端请求; -
经过
gRPC
拦截、Quota
校验,Quota
模块用于校验etcd db
文件大小是否超过了配额; -
接着
KVServer
模块将请求发送给本模块中的raft
,这里负责与etcd raft
模块进行通信,发起一个提案,命令为put foo bar
,即使用put
方法将foo
更新为bar
; -
提案经过转发之后,半数节点成功持久化;
-
MVCC
模块更新状态机;
put
接口的执行过程:
调用 put
向 etcd
写入数据时,首先会使用传入的键构建 keyIndex
结构体,基于 currentRevision
自增生成新的 Revision
如 {1,0}
,并从 treeIndex
中获取相关版本 Revision
等信息;写事务提交之后,将本次写操作的缓存 buffer
合并(merge
)到读缓存上(图中 ReadTx
中的缓存)。
revision{1,0}
是生成的全局版本号,作为 BoltDB
的 key
,经过序列化包括 key
名称、key
创建时的版本号(create_revision
)、value
值和租约等信息为二进制数据之后,将填充到 BoltDB
的 value
中,同时将该键和 Revision
等信息存储到 Btree
。
根据 etcd
读写流程图,可以知道读写操作依赖 MVCC
模块的 treeIndex
和 BoltDB
,treeIndex
用来保存键的历史版本号信息,而 BoltDB
用来保存 etcd
的键值对数据。通过这两个模块之间的协作,实现了 etcd
数据的读取和存储。
写请求的处理流程,如下图所示:
- 客户端发送写请求,通过负载均衡算法选取合适的 etcd 节点,发起 gRPC 调用。
- etcd server 的 gRPC Server 收到这个请求,经过 gRPC 拦截器拦截,实现 Metrics 统计和日志记录等功能。
- Quota 模块配额检查 db 的大小,如果超过会报etcdserver: mvcc: database space exceeded的告警,通过 Raft 日志同步给集群中的节点 db 空间不足,同时告警也会持久化到 db 中。etcd 服务端拒绝写入,对外提供只读的功能。
- 配额检查通过,KVServer 模块经过限速、鉴权、包大小判断之后,生成唯一的编号,这时才会将写请求封装为提案消息,提交给 Raft 模块。
- 写请求的提案只能由 Leader 处理,获取到 Raft 模块的日志条目之后,Leader 会广播提案内容。WAL 模块完成 Raft 日志条目内容封装,当集群大多数节点完成日志条目的持久化,即将提案的状态变更为已提交,可以执行提案内容。
- Apply 模块用于执行提案,首先会判断该提案是否被执行过,如果已经执行,则直接返回结束;未执行过的情况下,将会进入 MVCC 模块执行持久化提案内容的操作。
- MVCC 模块中的 treeIndex 保存了 key 的历史版本号信息,treeIndex 使用 B-tree 结构维护了 key 对应的版本信息,包含了全局版本号、修改次数等属性。版本号代表着 etcd 中的逻辑时钟,启动时默认的版本号为 1。键值对的修改、写入和删除都会使得版本号全局单调递增。写事务在执行时,首先根据写入的 key 获取或者更新索引,如果不存在该 key,则会给予当前最大的 currentRevision 自增得到 revision;否则直接根据 key 获取 revision。
- 根据从 treeIndex 中获取到 revision 、修改次数等属性,以及 put 请求传递的 key-value 信息,作为写入到 boltdb 的 value,而将 revision 作为写入到 boltdb 的 key。同时为了读请求能够获取最新的数据,etcd 在写入 boltdb 时也会同步数据到 buffer。因此上文介绍 etcd 读请求的过程时,会优先从 buffer 中读取,读取不到的情况下才会从 boltdb 读取,以此来保证一致性和性能。为了提高吞吐量,此时提案数据并未提交保存到 db 文件,而是由 backend 异步 goroutine 定时将批量事务提交。
- Server 通过调用网络层接口返回结果给客户端。
总的来说,这个过程为客户端发起写请求,由 Leader 节点处理,经过拦截器、Quota 配额检查之后,KVServer 提交一个写请求的提案给 Raft 一致性模块,经过 RaftHTTP 网络转发,集群中的其他节点半数以上持久化成功日志条目,提案的状态将会变成已提交。接着 Apply 通过 MVCC 的 treeIndex、boltdb 执行提案内容,成功之后更新状态机。
原文:
https://kaiwu.lagou.com/course/courseInfo.htm?courseId=613#/detail/pc?id=6411