基础架构

image.png

  • client 层:client 层包括 client v2 和 v3 两个大版本 API 客户端库,提供了简洁易用的 API,同时支持负载均衡、节点间故障自动转移,可极大降低业务使用 etcd 复杂度,提升开发效率、服务可用性。
  • API 网络层:API 网络层主要包括 client 访问 serverserver 节点之间的通信协议。
    • 一方面,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/2 协议。
  • Raft 算法层:Raft 算法层实现了 Leader 选举、日志复制、ReadIndex 等核心算法特性,用于保障 etcd 多个节点间的数据一致性、提升服务可用性等,是 etcd 的基石和亮点。
  • 功能逻辑层:etcd 核心特性实现层,如典型的 KVServer 模块、MVCC 模块、Auth 鉴权模块、Lease 租约模块、Compactor 压缩模块等,其中 MVCC 模块主要由 treeIndex 模块和 boltdb 模块组成。
  • 存储层:存储层包含预写日志 (WAL) 模块、快照 (Snapshot) 模块、boltdb 模块。其中 WAL 可保障 etcd crash 后数据不丢失,boltdb 则保存了集群元数据和用户写入的数据。

在下面这张架构图中,我用序号标识了 etcd 默认读模式(线性读)的执行流程,接下来,我们就按照这个执行流程从头开始说。

image.png

环境准备

安装:

  1. go get github.com/mattn/goreman

你可以通过如下go get命令快速安装 goreman,然后从etcd release页下载 etcd v3.4.9 二进制文件,再从etcd 源码中下载 goreman Procfile 文件,它描述了 etcd 进程名、节点数、参数等信息。最后通过goreman -f Procfile start命令就可以快速启动一个 3 节点的本地集群了。

注意: 要更改 Procfile 中 etcd 文件的路径.

client

流程一.

  1. etcdctl get hello --endpoints http://127.0.0.1:2379
  2. hello
  3. world

过程:

  1. 解析命令行参数
  2. 创建 clientv3 库对象
  3. 选择合适的 etcd server 节点 (负载均衡算法为 Round-robin)
  4. 发送请求给 KVServer 模块 (Range RPC 方法)

KVServer

流程二.

etcd 提供了丰富的 metrics、日志、请求行为检查等机制,可记录所有请求的执行耗时及错误码、来源 IP 等,也可控制请求是否允许通过,比如 etcd Learner 节点只允许指定接口和参数的访问,帮助大家定位问题、提高服务可观测性等,而这些特性是怎么非侵入式的实现呢?

答案就是拦截器。

拦截器

etcd server 定义了如下的 Service KV 和 Range 方法,启动的时候它会将实现 KV 各方法的对象注册到 gRPC Server,并在其上注册对应的拦截器。下面的代码中的 Range 接口就是负责读取 etcd key-value 的的 RPC 接口。

  1. service KV {
  2. // Range gets the keys in the range from the key-value store.
  3. rpc Range(RangeRequest) returns (RangeResponse) {
  4. option (google.api.http) = {
  5. post: "/v3/kv/range"
  6. body: "*"
  7. };
  8. }
  9. ....
  10. }

拦截器提供了在执行一个请求前后的 hook 能力.

其它特性:

  • 要求执行一个操作前集群必须有 Leader,防止脑裂;
  • 请求延时超过指定阈值的,打印包含来源 IP 的慢查询日志 (3.5 版本)。

server 收到 client 的 Range RPC 请求后,根据 ServiceName 和 RPC Method 将请求转发到对应的 handler 实现,handler 首先会将上面描述的一系列拦截器串联成一个执行,在拦截器逻辑中,通过调用 KVServer 模块的 Range 接口获取数据。

串行读与线性读

流程三和四.

写流程:

当 client 发起一个更新 hello 为 world 请求后,若 Leader 收到写请求,它会将此请求持久化到 WAL 日志,并广播给各个节点,若一半以上节点持久化成功,则该请求对应的日志条目被标识为已提交,etcdserver 模块异步从 Raft 模块获取已提交的日志条目,应用到状态机 (boltdb 等)。

image.png

此时若 client 发起一个读取 hello 的请求,假设此请求直接从状态机中读取, 如果连接到的是 C 节点,若 C 节点磁盘 I/O 出现波动,可能导致它应用已提交的日志条目很慢,则会出现更新 hello 为 world 的写命令,在 client 读 hello 的时候还未被提交到状态机,因此就可能读取到旧数据,如上图查询 hello 流程所示。

对数据敏感度较低的场景:

  • 直接读状态机数据返回、无需通过 Raft 协议与集群进行交互的模式,在 etcd 里叫做串行 (Serializable) 读,它具有低延时、高吞吐量的特点,适合对数据一致性要求不高的场景。

对数据敏感性高的场景:

  • 在 etcd 里面,提供了一种线性读模式来解决对数据一致性要求高的场景。

什么是线性读呢?

你可以理解一旦一个值更新成功,随后任何通过线性读的 client 都能及时访问到。虽然集群中有多个节点,但 client 通过线性读就如访问一个节点一样。etcd 默认读模式是线性读,因为它需要经过 Raft 协议模块,反应的是集群共识,因此在延时和吞吐量上相比串行读略差一点,适用于对数据一致性要求高的场景。

线性读之 ReadIndex

image.png

  1. 当收到一个线性读请求时,它首先会从 Leader 获取集群最新的已提交的日志索引 (committed index),如上图中的流程二所示。
  2. Leader 收到 ReadIndex 请求时,为防止脑裂等异常场景,会向 Follower 节点发送心跳确认,一半以上节点确认 Leader 身份后才能将已提交的索引 (committed index) 返回给节点 C(上图中的流程三)。
  3. C 节点则会等待,直到状态机已应用索引 (applied index) 大于等于 Leader 的已提交索引时 (committed Index)(上图中的流程四),然后去通知读请求,数据已赶上 Leader,你可以去状态机中访问数据了 (上图中的流程五)。

总体而言,KVServer 模块收到线性读请求后,通过架构图中流程三向 Raft 模块发起 ReadIndex 请求,Raft 模块将 Leader 最新的已提交日志索引封装在流程四的 ReadState 结构体,通过 channel 层层返回给线性读模块,线性读模块等待本节点状态机追赶上 Leader 进度,追赶完成后,就通知 KVServer 模块,进行架构图中流程五,与状态机中的 MVCC 模块进行进行交互了。

image.png

线性读其实就是在读数据之前等待已提交数据落盘.

MVCC

流程五中的多版本并发控制 (Multiversion concurrency control) 模块是为了解决上一讲我们提到 etcd v2 不支持保存 key 的历史版本、不支持多 key 事务等问题而产生的。

它核心由内存树形索引模块 (treeIndex) 和嵌入式的 KV 持久化存储库 boltdb 组成。

boltdb:

  • 基于 B+ tree 实现的 key-value 键值库,支持事务,提供 Get/Put 等简易 API 给 etcd 操作。

boltdb 的 key 是全局递增的版本号 (revision),value 是用户 key、value 等字段组合成的结构体,然后通过 treeIndex 模块来保存用户 key 和版本号的映射关系

treeIndex 与 boltdb 关系如下面的读事务流程图所示,从 treeIndex 中获取 key hello 的版本号,再以版本号作为 boltdb 的 key,从 boltdb 中获取其 value 信息。

image.png

key -> revision -> value

treeIndex

treeIndex 模块是基于 Google 开源的内存版 btree 库实现的,为什么 etcd 选择上图中的 B-tree 数据结构保存用户 key 与版本号之间的映射关系,而不是哈希表、二叉树呢?在后面的课程中我会再和你介绍。

treeIndex 相当于在内存中存储索引 (版本的索引), 数据还是在磁盘上, 节约了内存.

buffer

etcd 出于数据一致性、性能等考虑,在访问 boltdb 前,首先会从一个内存读事务 buffer 中,二分查找你要访问 key 是否在 buffer 里面,若命中则直接返回。

boltdb

我们知道 MySQL 通过 table 实现不同数据逻辑隔离,那么在 boltdb 是如何隔离集群元数据与用户数据的呢?答案是 bucket。boltdb 里每个 bucket 类似对应 MySQL 一个表,用户的 key 数据存放的 bucket 名字的是 key,etcd MVCC 元数据存放的 bucket 是 meta。

因 boltdb 使用 B+ tree 来组织用户的 key-value 数据,获取 bucket key 对象后,通过 boltdb 的游标 Cursor 可快速在 B+ tree 找到 key hello 对应的 value 数据,返回给 client。

小结

  • client 的通过负载均衡、错误处理等机制实现了 etcd 节点之间的故障的自动转移,它可助你的业务实现服务高可用,建议使用 etcd 3.4 分支的 client 版本。
  • etcd 提供的两种读机制 (串行读和线性读) 原理和应用场景。通过线性读,对业务而言,访问多个节点的 etcd 集群就如访问一个节点一样简单,能简洁、快速的获取到集群最新共识数据。