背景
随着在 Kubernetes 场景打磨下不断成长, etcd 逐渐成为技术圈众所周知的开源产品。
- 多版本并发控制
- 事务
- 租约
- 变更通知
etcd 因其丰富的功能,并被越来越多的选择,甚至于被当作 “银弹” 过度使用。本文的重点在于了解其发展历程、实现细节,并针对技术方案选型给出自己的理解。
本文所有内容基于 etcd v3.5.0
起源
2013 年,有一个叫 CoreOS 的创业团队,需要一个协调服务来存储服务配置信息、提供分布式锁等能力,来构建一款叫做 Container Linux 的产品。当分析过需求场景、痛点和核心目标,并评估社区开源的选项之后,CoreOS 团队最终选择自己造轮子,从 0 到 1 开发 etcd 以满足其需求。
时间线
基础架构
集群架构
架构说明
- 集群由一个 Leader 节点和 多个 Follower 节点组成,通过 Raft 协议达成共识
- 写请求:只有 Leader 节点能处理写请求,如果当前节点只是一个 Follower,则它会把请求转发给 Leader 处理。
- 读请求:
- 串行读:直接读状态机数据返回、无需通过 Raft 协议与集群进行交互。
- 线性读:如果当前节点只是一个 Follower,它首先会从 Leader 获取集群最新的已提交的日志索引 (committed index)。然后等待直到状态机已应用索引 (applied index) 大于等于 Leader 的已提交索引时 (committed Index),再读取数据返回。
单机架构
架构说明
- Client(客户端层):包含 Client v2 和 v3 两个⼤版本 API 客户端。
- API(网络层):
- 包含 Client 访问 Server、Server 节点之间的通信协议
- Client 访问 Server 协议有两个版本:v2 API 采⽤ HTTP/1.x 协议,v3 API 采用 gRPC 协议
- Server 节点之间使用 HTTP 协议,通过 Raft 算法实现数据复制和 Leader 选举等功能
- Raft(一致性算法层):维护节点的 Raft 状态机、Raft 日志等保障 etcd 多节点间的数据⼀致性
- Server(业务逻辑层):
- 包括:Auth 鉴权模块、Quota 配额模块、KV 模块、Raftnode 一致性模块、Rafthttp 一致性通信模块、Lease 租约模块、Apply 持久化应用模块、MVCC 多版本并发控制模块、Watch 变更通知模块等
- MVCC 模块主要由 treeIndex B 树索引模块和 boltdb B+ 树数据库模块组成
- Storage:存储层
- 包含 WAL 预写⽇志模块、Snapshot 快照模块、boltdb 数据库模块
- WAL 保障异常后数据不丢失,boltdb 则保存了集群元数据和写⼊的数据
核心工作流
数据写入
步骤:
- 客户端发送一个 Put 请求给 KVServer。
- KVServer 将请求数据进行适当的封装处理之后,调用 Raft 模块的 Propose 接口方法(步骤 2),由 raft 模块来处理写请求。
- Raft 模块将记录 (entry) 添加到当前节点的 raftLog (步骤 3),并通知 RaftNode 模块执行相关操作 (步骤 4)
- RaftNode 模块
- 首先,广播给其他节点(Follower)(步骤 5)
- 同时,将记录保存到本地 WAL 文件中(步骤 6)
- 最后,告诉 Raft 模块开始等待其他节点提交响应(步骤 7)
- 其它节点(Follower)接收到记录,并写到本地 raftlog 之后,就会给 Leader 发送一个响应。当 Leader 接收到超过半数节点的响应后,就认为这条记录已经 commit ,会更新本地 raftlog 的 commitID(步骤 8)。
- 一旦记录被 Raft 模块 commit 了,就开始通知 RaftNode 模块执行相关操作(步骤 9)。 RaftNode 模块应用(apply)数据记录(步骤 10),同时也将 commitID 广播给其它节点(步骤 11),然后通知 Raft 模块数据已经提交(步骤 12)。
- MVCC 模块异步 将数据应用(apply)到本地存储(步骤 13),并通知 KVServer。
- 最后 KVServer 将结果返回给 client,整个过程就处理结束了。
从上面的流程可以看出,一条记录首先是写入本地的 raftlog。然后发送给其它节点,当超过半数的节点接收到这条记录时,那么该记录就被认为已经 commit 了。最后才能被 KVServer apply。 所以下面的条件永远成立:
ApplyId <= CommitId <= RaftLogId
复制状态机
etcd 使用 Raft 协议来维护集群内各个节点状态的一致性。每个 etcd 节点都维护了一个状态机,并且任意时刻至多存在一个有效的主节点。主节点处理所有来自客户端写操作,通过 Raft 协议保证写操作对状态机的改动会可靠的同步到其他节点。
步骤:
- Client 客户端向 KVServer 发送请求
- Server 接收到请求后,向 Raft 模块提交 Proposal
- Raft 模块获取到 Proposal 后,会为 Proposal 生成日志条目,并追加到本地日志
- Leader 会向 Follower 广播消息,为每个 Follower 生成追加的 RPC 消息,包括复制给 Follower 的日志条目
- Follower 会持久化消息到 WAL 日志中,并追加到日志存储
- Follower 向 Leader 回复一个应答日志条目的消息,告知 Leader 当前已复制日志的最大索引
- Leader 在收到 Follower 的应答后,将已复制日志的最大索引信息更新到跟踪 Follower 进展的 Match Index 字段
- Leader 根据 Follower 的 MatchIndex 信息,计算出一个位置。如果该位置已经被一半以上的节点持久化,那么这个日志之前的日志条目都可以标记为已提交
- Leader 发送消息到 Follower 节点时,告知目前已经提交的索引位置
- 各个节点根据已提交的日志条目,将内容应用(apply)到存储、状态机
租约
创建租约:
- 当 LeaseServer 收到 Client 的创建一个 Lease 请求后,会通过 Raft 模块
完成日志同步 - 随后 Apply 模块通过 Lessor 模块的 Grant 接口执行日志条目内容。
- 首先 Lessor 的 Grant 接口会把 Lease 保存到内存的 ItemMap 数据结构中
- 然后它需要持久化 Lease,将 Lease 数据保存到 boltdb 的 Lease bucket
- 最终返回一个唯一的 LeaseID 给 client。
附加租约:
- 当用户新增一个带 租约的 Key 时,MVCC 模块它会通过 Lessor 模块的 Attach 方法,将 key 关联到 Lease 的 key 内存集合 ItemMap。
淘汰租约:
- 淘汰过期 Lease 的工作由 Lessor 模块的一个异步 goroutine 负责。它会定时从最小堆中取出已过期的 Lease,执行删除 Lease 和其关联的 key 列表数据的 RevokeExpiredLease 任务。
- Lessor 模块会将已确认过期的 LeaseID,保存在一个名为 expiredC 的 channel 中,而
etcd server 的主循环会定期从 channel 中获取 LeaseID,发起 revoke 请求,通过 Raft
Log 传递给 Follower 节点。 - 各个节点收到 revoke Lease 请求后,获取关联到此 Lease 上的 key 列表,从 boltdb 中 删除 key,从 Lessor 的 Lease map 内存中删除此 Lease 对象,最后还需要从 boltdb 的 Lease bucket 中删除这个 Lease。
注意:
租约影响性能因素源自多方面:
- 首先是 TTL,TTL 过长会导致节点异常后,无法及时从 etcd 中删除,影响服务可用性,而过短,则要求 client 频繁发送续期请求。
- 其次是 Lease 数,如果 Lease 成千上万个,那么 etcd 可能无法支撑如此大规模的 Lease 数,导致高负载。
- 再次,Lease 过期会触发写请求,再加上变更通知产生的读请求,对 etcd server 压力非常大。
- 最后,如果因为网络异常无法续期,导致数据过期。网络恢复正常,同一份数据再次写入,将导致 DB 大小迅速增加(历史版本数据并没有真正删除,数据库压缩才会实际删除)。
从实际使用场景上来,为了降低 Lease TTL 过期带来的影响,可以将 Lease 与 Key 独立开,由系统自行控制和判定存活状态和 Key 的删除。
为了降低 etcd server 的压力可以把多个 kv 关联在一个 lease 上的,比如:
kubernetes 场景中有大量的 event,如果一个 event 一个 Lease, Lease 数量是非常多的,Lease 过期会触发大量写请求,再加上变更通知产生的读请求,对 etcd server 压力非常大。
为了解决这个问题对 etcd server 性能的影响,Lease 过期淘汰会默认限速每秒 1000 个。因此 kubernetes 场景为了优化 Lease 数,会将最近一分钟内产生的 event key 列表,复用在同一个 Lease,大大降低了 Lease 数。