etcd 实现与选型分析(一)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: etcd 实现与选型分析

背景

随着在 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(网络层)
  1. 包含 Client 访问 Server、Server 节点之间的通信协议
  2. Client 访问 Server 协议有两个版本:v2 API 采⽤ HTTP/1.x 协议,v3 API 采用 gRPC 协议
  3. Server 节点之间使用 HTTP 协议,通过 Raft 算法实现数据复制和 Leader 选举等功能
  • Raft(一致性算法层):维护节点的 Raft 状态机、Raft 日志等保障 etcd 多节点间的数据⼀致性
  • Server(业务逻辑层)
  1. 包括:Auth 鉴权模块、Quota 配额模块、KV 模块、Raftnode 一致性模块、Rafthttp 一致性通信模块、Lease 租约模块、Apply 持久化应用模块、MVCC 多版本并发控制模块、Watch 变更通知模块等
  2. MVCC 模块主要由 treeIndex B 树索引模块和 boltdb B+ 树数据库模块组成
  • Storage:存储层
  1. 包含 WAL 预写⽇志模块、Snapshot 快照模块、boltdb 数据库模块
  2. WAL 保障异常后数据不丢失,boltdb 则保存了集群元数据和写⼊的数据

核心工作流

数据写入

步骤

  1. 客户端发送一个 Put 请求给 KVServer。
  2. KVServer 将请求数据进行适当的封装处理之后,调用 Raft 模块的 Propose 接口方法(步骤 2),由 raft 模块来处理写请求。
  3. Raft 模块将记录 (entry) 添加到当前节点的 raftLog (步骤 3),并通知 RaftNode 模块执行相关操作 (步骤 4)
  4. RaftNode 模块
  • 首先,广播给其他节点(Follower)(步骤 5)
  • 同时,将记录保存到本地 WAL 文件中(步骤 6)
  • 最后,告诉 Raft 模块开始等待其他节点提交响应(步骤 7)
  1. 其它节点(Follower)接收到记录,并写到本地 raftlog 之后,就会给 Leader 发送一个响应。当 Leader 接收到超过半数节点的响应后,就认为这条记录已经 commit ,会更新本地 raftlog 的 commitID(步骤 8)。
  2. 一旦记录被 Raft 模块 commit 了,就开始通知 RaftNode 模块执行相关操作(步骤 9)。 RaftNode 模块应用(apply)数据记录(步骤 10),同时也将 commitID 广播给其它节点(步骤 11),然后通知 Raft 模块数据已经提交(步骤 12)。
  3. MVCC 模块异步 将数据应用(apply)到本地存储(步骤 13),并通知 KVServer。
  4. 最后 KVServer 将结果返回给 client,整个过程就处理结束了。

从上面的流程可以看出,一条记录首先是写入本地的 raftlog。然后发送给其它节点,当超过半数的节点接收到这条记录时,那么该记录就被认为已经 commit 了。最后才能被 KVServer apply。 所以下面的条件永远成立:

ApplyId <= CommitId <= RaftLogId
复制状态机

etcd 使用 Raft 协议来维护集群内各个节点状态的一致性。每个 etcd 节点都维护了一个状态机,并且任意时刻至多存在一个有效的主节点。主节点处理所有来自客户端写操作,通过 Raft 协议保证写操作对状态机的改动会可靠的同步到其他节点。

步骤:

  1. Client 客户端向 KVServer 发送请求
  2. Server 接收到请求后,向 Raft 模块提交 Proposal
  3. Raft 模块获取到 Proposal 后,会为 Proposal 生成日志条目,并追加到本地日志
  4. Leader 会向 Follower 广播消息,为每个 Follower 生成追加的 RPC 消息,包括复制给 Follower 的日志条目
  5. Follower 会持久化消息到 WAL 日志中,并追加到日志存储
  6. Follower 向 Leader 回复一个应答日志条目的消息,告知 Leader 当前已复制日志的最大索引
  7. Leader 在收到 Follower 的应答后,将已复制日志的最大索引信息更新到跟踪 Follower 进展的 Match Index 字段
  8. Leader 根据 Follower 的 MatchIndex 信息,计算出一个位置。如果该位置已经被一半以上的节点持久化,那么这个日志之前的日志条目都可以标记为已提交
  9. Leader 发送消息到 Follower 节点时,告知目前已经提交的索引位置
  10. 各个节点根据已提交的日志条目,将内容应用(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 数。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
存储 Kubernetes 算法
云原生|kubernetes|etcd集群详细介绍+安装部署+调优(一)
云原生|kubernetes|etcd集群详细介绍+安装部署+调优(一)
1349 0
云原生|kubernetes|etcd集群详细介绍+安装部署+调优(一)
|
存储 Prometheus 监控
高可用prometheus集群方案选型分享
高可用prometheus集群方案选型分享
6122 2
高可用prometheus集群方案选型分享
|
Kubernetes Cloud Native 索引
云原生|kubernetes|搭建部署一个稳定高效的EFK日志系统(三)
云原生|kubernetes|搭建部署一个稳定高效的EFK日志系统
271 0
云原生|kubernetes|搭建部署一个稳定高效的EFK日志系统(三)
|
存储 域名解析 缓存
|
6月前
|
存储 Kubernetes Cloud Native
云原生|kubernetes|etcd集群详细介绍+安装部署+调优
云原生|kubernetes|etcd集群详细介绍+安装部署+调优
1447 0
|
6月前
|
Kubernetes Cloud Native 网络协议
云原生|kubernetes|搭建部署一个稳定高效的EFK日志系统
云原生|kubernetes|搭建部署一个稳定高效的EFK日志系统
220 0
|
存储 JSON Kubernetes
云原生|kubernetes|搭建部署一个稳定高效的EFK日志系统(一)
云原生|kubernetes|搭建部署一个稳定高效的EFK日志系统
564 0
|
存储 Kubernetes 固态存储
etcd 实现与选型分析(二)
etcd 实现与选型分析(二)
202 0
|
存储 Kubernetes Cloud Native
Kubernetes 本地持久化存储方案 OpenEBS LocalPV 落地实践上——使用篇
Kubernetes 本地持久化存储方案 OpenEBS LocalPV 落地实践上——使用篇
722 0
|
Kubernetes Cloud Native Docker
云原生|kubernetes|搭建部署一个稳定高效的EFK日志系统(二)
云原生|kubernetes|搭建部署一个稳定高效的EFK日志系统
223 0
云原生|kubernetes|搭建部署一个稳定高效的EFK日志系统(二)