概述
Lease 相当于 TTL 的一种抽象,可以将 Lease 绑定到一个 key 上。etcd server 会保证在 Lease 约定的有效期内,不会删除关联到此 Lease 上的 key-value。如果未在有效期内续租该 Lease,那么 etcd server 就会删除此 Lease 及其关联的 key-value。
Lease 的几个作用,
-
解决类似 Leader 选举的问题。
为了解决单副本的可用性问题,往往会多副本部署。但是,此时要求同一个时刻内只能有一个副本对外提供服务,为了确保这点,需要使用 Leader 选举机制。
利用 Lease 实现 Leader 选举机制的大概方式是:
- 一个副本先启动,往 Etcd 中 put 了某个 key,并给这个 key 绑定了 lease。此时它成为了 Leader,并且之后它会定期主动地续约该 lease,从而确保该 key 不会被删除。
- 其他副本启动之后,会发现已存在该 key,那么就不会成为 leader 提供服务。但是,其他副本会定期检查是否存在该 key。
- 假设 leader 因为网络等原因,无法续租相应的 lease,则该 key 会被删除。而其他副本通过定期检查发现不存在该 key 了,就会 put 同一个 key,然后定期主动地续约该 lease。
-
服务发现场景中故障节点自动剔除等。跟上述类似的方式。
整体架构
etcd 在启动的时候,会创建 Lessor 模块。Lessor 模块会启动两个常驻 goroutine,
- 一个是 RevokeExpiredLease,定期检查是否有过期 Lease,发起删除过期的 Lease 操作。
- 一个是 CheckpointScheduledLease,定期更新 Lease 的剩余到期时间,并定期将剩余到期时间同步给其他节点。
Lessor 模块还提供了 Grant、Revoke、LeaseTimeToLive、LeaseKeepAlive API 给 client 使用,各接口作用如下:
- Grant 表示创建一个 Lease,并且 TTL 为你指定的秒数。此时 Lessor 会将 Lease 信息持久化存储在 boltdb 中。
- Attach 表示将 key 和 Lease 进行关联。
- Revoke 表示删除 Lease 并删除其关联的 key-value 数据。
- LeaseTimeToLive 表示获取一个 Lease 的有效期、剩余时间。
- LeaseKeepAlive 表示为 Lease 续期。
创建 Lease
- client 通过 clientv3 库的 Lease API 发起 RPC 调用。
- etcd server 收到该请求,先交给 Raft 模块,由 Raft 模块完成创建 lease 的日志同步。
- 之后 Apply 模块获取到该条日志,调用 Lessor 模块的 Grant 接口执行日志条目内容。
- Lessor 模块先将 Lease 保存到内存的 ItemMap 数据结构中。
- Lessor 模块再将 Lease 数据保存到 boltdb 的 Lease bucket 中。
- 最后给 client 返回一个唯一的 LeaseID。
关联 Lease 和 Key
-
Lessor 模块的 Attach 方法,先将 key 保存到内存的 ItemMap 数据结构中,也就是保存到该 Lease 对应的 key 集合中。
-
之后 MVCC 模块在持久化该 key-value 的时候,由于保存的 value 是个结构体(mvccpb.KeyValue),它不仅包含了 key-value 数据,还包含了关联的 LeaseID 信息,因此 Lease 和 key 的关联信息也被保存了。
当 etcd 重启的时候,会通过这些信息重建关联各个 Lease 的 key 集合。
注意:不同 key 如果 TTL 相同,其实可以复用同一个 Lease,以显著减少 Lease 数量。这对于下文提到的 Lease 续期和 Lease 检测的性能提升有很大帮助。
etcd v2 版本是没有 Lease 概念,TTL 属性是附加在 key 上。因此如果要续期的话,其实是针对 key 进行续期。并且由于 v2 采用的是 HTTP/1.x 协议,相当于要为每个 key 定期创建一个连接进行续期,etcd v2 是无法支撑较大规模的 Lease 场景。etcd v3 采用了 Lease 概念并支持复用,同时还使用了 gRPC 协议实现了多路复用,这两大优化有效减少了连接数。
Lease 续期
- 正常情况下,client 需要定期发送 KeepAlive 请求给 etcd 续期健康状态的 Lease,否则 Lease 和关联的数据就会被删除。
- etcd server 收到之后将 Lease 的过期时间更新为当前系统时间加上其 TTL。
Lease 过期淘汰
淘汰过期 Lease 的工作由 Lessor 模块的 RevokeExpiredLease goroutine 负责。Leader 节点会按 Lease 过期时间维护一个最小堆,这个 goroutine 会定时从最小堆中取出已过期的 LeaseID,之后 Leader 发起 revoke 操作通知整个集群删除 Lease 和关联的 Key。
- 每次新增、续期 Lease 的时候,它会插入、更新一个对象到最小堆中,对象含有 LeaseID 和其到期时间 unixnano,对象之间按到期时间升序排序。
- etcd Lessor 主循环每隔 500ms 执行一次撤销 Lease 检查(RevokeExpiredLease)。它每次会轮询堆顶的元素,若已过期则加入到待淘汰列表,直到堆顶的 Lease 过期时间大于当前,然后结束本轮轮询。
- Lessor 模块之后会将已确认过期的 LeaseID,保存在一个名为 expiredC 的 channel 中,而 etcd server 的主循环会定期从 channel 中获取 LeaseID,发起 revoke 请求,通过日志传递给 Follower 节点。
- 各个节点收到 revoke Lease 请求后,获取关联到此 Lease 上的 key 列表,从 boltdb 中删除 key,从 Lessor 的 ItemMap 内存中删除此 Lease 对象,最后从 boltdb 的 Lease bucket 中删除这个 Lease。
etcd 早起淘汰 Lease 的方式是直接遍历所有 Lease,逐个检查 Lease 是否过期,过期则从 Lease 关联的 key 集合中,取出 key 列表,删除它们,时间复杂度是 O(N)。相比早期 O(N) 的遍历时间复杂度,使用堆后,插入、更新、删除的时间复杂度是 O(Log N),查询堆顶对象是否过期时间复杂度仅为 O(1),性能大大提升,可支撑大规模场景下 Lease 的高效淘汰。
lease checkpoint 机制
checkpoint 意思是指保存一个系统或应用程序在某个时刻的运行状态,以便在后续需要时能够快速恢复到该状态。
检查 Lease 过期、维护最小堆、针对过期的 Lease 发起 revoke 操作,都是 Leader 节点负责。这就会出现一个问题,当 Leader 因重启、crash、磁盘 IO 等异常不可用时,Follower 节点会发起 Leader 选举,如果是其他节点当选了 Leader,则它要完成必须重建 Lease 过期最小堆等管理数据结构。如果没有持久化 Lease 剩余的 TTL 信息,当出现频繁的 Leader 切换时,且切换时间小于 Lease 的 TTL,则会导致 Lease 永远无法删除,进而可能导致大量 key 堆积,db 大小超过配额等异常。
因此在 Lease 中,需要引入 checkpoint 机制。
-
一方面 etcd 启动的时候,Leader 节点后台会运行图中黑色虚线框所示的 CheckPointScheduledLeases 任务,定期更新 Lease 的剩余到期时间,并定期批量地将 Lease 剩余的 TTL 通过 Raft 日志同步给 Follower 节点。Follower 节点收到 CheckPoint 请求后,更新内存数据结构 LeaseMap 的剩余 TTL 信息。
-
另一方面当 Leader 节点收到 KeepAlive 请求的时候,它会把此 Lease 的 TTL 重置,并通过 checkpoint 机制同步给 Follower 节点,尽量确保续期后集群各个节点的 Lease 剩余 TTL 一致。
checkpoint 特性对性能有一定影响,目前是试验特性,通过 experimental-enable-lease-checkpoint 参数开启。