在实际业务场景中,为了确保如 Kubernetes 调度器和控制器等关键组件的高可用性,通常需要部署多个副本。然而,为保证同一时刻只有一个副本对外提供服务,需要引入 Leader 选举机制。
Leader 选举主要解决两个核心问题:
唯一性(Safety):确保集群中任何时候只有一个 Leader,以维护业务逻辑的正确性和互斥性。
活性(Liveness)检测:当主节点出现故障时,备份节点能够快速感知并接替成为新的 Leader。这可以通过两种方式实现:
被动型检测:通过定时探测 Leader 节点的健康状态,例如 Redis Sentinel 的做法。
主动型上报:Leader 节点定期向协调服务发送“特殊心跳”,报告其健康状态。如果超过约定的最大存活时间未收到心跳,协调服务将移除该节点的 Leader 标识,并通知其他节点发起新选举。
Lease 是基于主动型上报模式的一种活性检测机制。它涉及到 Client 和 etcd server 之间的约定,在约定的有效期内(TTL),etcd server 不会删除与 Lease 相关联的 key-value 数据。若未在有效期内进行续租,etcd server 将自动删除这些数据。
利用 Lease 的 TTL 特性,可以解决一系列问题,包括但不限于 Leader 选举、Kubernetes Event 的自动淘汰以及服务发现过程中故障节点的自动剔除等。这种方法提高了系统的可靠性和自动化管理水平。

etcd 在启动的时候,创建 Lessor 模块的时候,它会启动两个常驻 goroutine,如上图所示:
RevokeExpiredLease 任务,定时检查是否有过期 Lease,发起撤销过期的 Lease 操作。
CheckpointScheduledLease,定时触发更新 Lease 的剩余到期时间的操作。
Lessor 模块提供了 Grant、Revoke、LeaseTimeToLive、LeaseKeepAlive API 给 client 使用,各接口作用如下:
Grant 表示创建一个 TTL 为你指定秒数的 Lease,Lessor 会将 Lease 信息持久化存储在 boltdb 中;
Revoke 表示撤销 Lease 并删除其关联的数据;
LeaseTimeToLive 表示获取一个 Lease 的有效期、剩余时间;
LeaseKeepAlive 表示为 Lease 续期。
通过 etcd 的 Lease API,客户端可以创建具有特定 TTL(生存时间)的租约(Lease),用于管理节点健康状态等指标。以下是创建和查看 Lease 的基本操作示例:
# 创建一个TTL为600秒的Lease,etcd server返回LeaseID
$ etcdctl lease grant 600
lease 326975935f48f814 granted with TTL(600s)
# 查看Lease的有效期和剩余时间
$ etcdctl lease timetolive 326975935f48f814
lease 326975935f48f814 granted with TTL(600s),remaining(590s)
当创建 Lease 时,Lease Server 会通过 Raft 模块同步日志,并通过 Lessor 模块将 Lease 存储在内存的 ItemMap 数据结构中,同时持久化到 boltdb 的 Lease bucket 中,最后返回唯一的 LeaseID 给客户端。
为了将节点健康状态数据关联到 Lease 上,可以通过 KV 模块的 --lease 参数实现。例如:
$ etcdctl put node healthy --lease 326975935f48f818
OK
# 查询key并查看其关联的LeaseID
$ etcdctl get node -w=json | python -m json.tool
{
"kvs":[
{
"create_revision":24,
"key":"bm9kZQ==",
"Lease":3632563850270275608,
"mod_revision":24,
"value":"aGVhbHRoeQ==",
"version":1
}
]
}
在此过程中,MVCC 模块通过 Lessor 模块的 Attach 方法将 key 关联到 Lease 的 key 集合中。这些关联关系保存在内存中,但在 etcd 重启时,可通过 MVCC 模块在 boltdb 中存储的 mvccpb.KeyValue 结构体(包含 LeaseID 等信息)重建各个 Lease 的 key 集合列表,从而恢复所有关联关系。这样就确保了即使在系统重启后,每个 Lease 及其关联的 key 也能被正确地重新加载和使用。
以上流程原理如下图所示,它描述了用户的 key 是如何与指定 Lease 关联的。

通过上面一系列操作,我们完成了 Lease 的创建和数据关联。为了保持节点的健康状态,在节点存活期间,需要定期发送 KeepAlive 请求给 etcd 来续期 Lease,否则 Lease 及其关联的数据将被删除。
Lease 续期的核心是更新 Lease 的过期时间为当前系统时间加上其 TTL(生存时间)。然而,影响 Lease 续期性能的因素包括 TTL 的长度和 Lease 数量:
TTL 长度:TTL 过长会导致异常节点不能及时从 etcd 中移除,影响服务可用性;TTL 过短则需要频繁发送续期请求。
Lease 数量:大量 Lease 可能导致 etcd 负载过高,影响性能。
在早期的 etcd v2 版本中,由于没有 Lease 概念,TTL 属性直接关联到 key 上,即使 TTL 相同,每个 key 也需要独立的 HTTP/1.x 连接来发送续期请求,这导致了性能瓶颈和扩展性问题。
为了解决这些问题,etcd v3 引入了 Lease 特性,并进行了以下优化:
TTL 属性转移:将 TTL 属性转移到 Lease 上,允许具有相同 TTL 的不同 key 复用同一个 Lease,从而显著减少了 Lease 的数量。
协议升级:从 HTTP/1.x 升级到了 gRPC 协议,利用 HTTP/2 实现多路复用和流式传输,使得同一连接可以支持多个 Lease 的续期,大大减少了所需的连接数。
这些改进极大地提升了 Lease 续期的性能,满足了各种业务场景的需求。通过这种方式,etcd v3 成功解决了高频率 Lease 续期带来的性能挑战。
在了解完节点正常情况下的 Lease 续期特性后,我们再看看节点异常时,未正常续期后,etcd 又是如何淘汰过期 Lease、删除节点健康指标 key 的。
淘汰过期 Lease 的工作由 Lessor 模块的一个异步 goroutine 负责。
如下面架构图虚线框所示,它会定时从最小堆中取出已过期的 Lease,执行删除 Lease 和其关联的 key 列表数据的 RevokeExpiredLease 任务。

随着 Lease 数量的增加,简单的遍历检查方法性能会逐渐下降。为了解决这个问题,etcd 采用了基于最小堆的高效淘汰方案来管理 Lease 的过期。
最小堆实现:新增或续期 Lease 时,会在最小堆中插入或更新一个对象,该对象包含 LeaseID 和到期时间(unixnano),并且对象按到期时间升序排序。每次执行撤销过期 Lease 检查(RevokeExpiredLease)时,每隔 500ms 轮询堆顶元素,若已过期则加入待淘汰列表,直到堆顶 Lease 的过期时间大于当前时间为止。
这种机制相比早期 O(N) 的遍历复杂度,使用最小堆后,插入、更新和删除的时间复杂度降低到了 O(Log N),而查询堆顶对象是否过期仅需 O(1) 的时间复杂度,大大提升了性能,支持大规模场景下的 Lease 高效淘汰。
Leader 和 Follower 协作淘汰 Lease:Lessor 模块将确认过期的 LeaseID 存储在 expiredC channel 中。etcd server 主循环定期从这个 channel 获取 LeaseID 并发起 revoke 请求,通过 Raft Log 同步给所有 Follower 节点。
处理过期 Lease:Follower 节点接收到 revoke Lease 请求后,会查找并删除与该 Lease 关联的所有 key,同时从内存中的 Lease map 和 boltdb 的 Lease bucket 中移除该 Lease。
这样,etcd 实现了 Lease 的自动过期淘汰逻辑。如果某个节点出现异常未能正常续期,随着时间推移,对应的 Lease 将被标记为过期,随后 Lessor 主循环定时轮询这些过期 Lease,并由 Leader 发起 revoke 操作通知整个集群进行相应的数据清理。这一过程确保了即使在高并发环境下也能高效、准确地管理 Lease 及其关联的数据。
在了解了 Lease 的创建、续期及自动淘汰机制后,可以看出这些操作主要由 Leader 节点负责,它扮演着 Lease 管理的仲裁者角色。这种清晰的职责划分简化了 Lease 特性的实现复杂度。
然而,当 Leader 节点由于重启、崩溃或磁盘 I/O 异常等原因不可用时,Follower 节点会发起 Leader 选举,新当选的 Leader 需要重建如最小堆等管理数据结构来继续执行 Lease 管理任务。
早期版本的 etcd 在处理这种情况时遇到了问题:由于未持久化存储 Lease 剩余的 TTL 信息,在 Leader 切换后重建最小堆时,所有 Lease 会被自动续期。如果频繁发生 Leader 切换且切换时间短于 Lease 的 TTL,则可能导致 Lease 永远无法被删除,造成大量 key 积压和数据库大小超过配额的问题。
为解决上面的问题,etcd 引入了检查点机制,也就是下面架构图中黑色虚线框所示的 CheckPointScheduledLeases 的任务,包括以下两个方面:
异步任务同步TTL:Leader 节点后台运行异步任务,定期批量地将 Lease 剩余的 TTL 通过 Raft Log 同步给 Follower 节点,确保各个节点内存中的 LeaseMap 数据结构保持最新的剩余 TTL 信息。
KeepAlive 请求同步:当 Leader 节点收到 KeepAlive 请求时,也会通过 checkpoint 机制将此 Lease 的剩余 TTL 重置,并同步给 Follower 节点,以保证集群内各节点 Lease 剩余 TTL 的一致性。
需要注意的是,虽然此特性有助于解决频繁 Leader 切换导致的问题,但对性能有一定影响,目前仍处于试验阶段,可以通过 experimental-enable-lease-checkpoint 参数开启此功能。这确保了即使在高频率的 Leader 切换情况下,也能有效管理和维护 Lease 及其关联的数据。

本文的内容围绕 Lease 的创建、关联 key、续期、淘汰及 checkpoint 机制展开,核心在于 TTL(生存时间)管理。以下是主要内容的概括:
Lease 创建及续期:
创建 Lease 时,etcd 将 Lease 信息保存到 boltdb 的 Lease bucket 中。
为防止 Lease 被淘汰,需定期发送 LeaseKeepAlive 请求给 etcd server 来更新 Lease 的到期时间。
针对性能挑战,etcd 从早期将 TTL 属性直接关联到 key 上,发展到抽象出独立的 Lease 支持多 key 复用相同 TTL,并且协议从 HTTP/1.x 升级到 gRPC,支持多路连接复用,减少了服务器资源开销。
Lease 淘汰机制:
etcd 实现了基于最小堆的高效淘汰算法,从原先 O(N) 的时间复杂度优化至 O(Log N)。
系统轮询最小堆顶部 Lease 是否过期,一旦过期即生成 revoke 请求以清理 Lease 及其关联的数据。
Lease Checkpoint 机制:
为应对 Leader 异常情况下 TTL 自动续期导致 Lease 永不被淘汰的问题,etcd 引入了 checkpoint 机制。
此机制通过后台异步任务定期批量同步 Lease 剩余 TTL 到 Follower 节点,并在收到 KeepAlive 请求时同步剩余 TTL,确保集群内各节点 Lease 剩余 TTL 的一致性。
这些改进和机制共同确保了即使在高频率的 Leader 切换或异常情况下,也能有效管理和维护 Lease 及其关联的数据,同时提升了系统的稳定性和性能。Checkpoint 机制目前仍处于试验阶段,可以通过特定参数启用。
本文内容摘抄自极客时间专栏 etcd 实战课