Lease 是什么

K8S 服务上的 Leader 选举的时候,是需要尽早感知 Leader 故障的异常,也就是 liveness 检测。常见的 liveness 检测有两种:

  • 被动检测型。探测节点定时 ping Leader 节点,判断是否健康,如 Redis Sentinel。
  • 主动上报型。Leader 节点定期向协调服务发送心跳汇报健康状况。

Lease 是主动上报型,是 client 和 etcd server 的约定,在 TTL 时间内,不删除关联此 lease 的 key-value。可以通过心跳机制来刷新 TTL 时间。

可以基于 Lease 的 TTL 机制,实现类似 Leader 选举、K8S Event 自动淘汰、服务发现场景中故障节点自动淘汰等问题。

Lease 整体架构

|500

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 续期。
# 创建一个TTL为600秒的lease,etcd server返回LeaseID
$ etcdctl lease grant 600
lease 326975935f48f814 granted with TTL(600s)

# 查看lease的TTL、剩余时间
$ etcdctl lease timetolive 326975935f48f814
lease 326975935f48f814 granted with TTL(600s), remaining(590s)

Lease 创建与数据关联

假设此时有个异常节点自动剔除的情景,希望当节点异常时,表示节点健康的 key 能被从 etcd 集群中自动删除,那么整个的过程如下。

  • 首先,client 创建 Lease,etcd 同步 Lease。当 Lease server 收到 client 的创建一个有效期 n 秒的 Lease 请求后,会通过 Raft 模块完成日志同步,随后 Apply 模块通过 Lessor 模块的 Grant 接口执行日志条目内容。

  • 其次,保存 Lease,生成 LeaseID。Lessor 的 Grant 接口会把 Lease 保存到内存的 ItemMap 数据结构中,然后它需要持久化 Lease,将 Lease 数据保存到 boltdb 的 Lease bucket 中,返回一个唯一的 LeaseID 给 client。

  • 关联 key 与 Lease。KV 模块的 API 接口提供了一个”—lease”参数,可以通过 etcdctl put node healthy --lease 326975935f48f818 将 key node 关联到对应的 LeaseID 上,并通过 etcdctl get node -w=json | python -m json.tool 来查看 LeaseID。

  • 持久化数据。当通过 put 等命令新增一个指定了”—lease”的 key 时,MVCC 模块它会通过 Lessor 模块的 Attach 方法,将 key 关联到 Lease 的 key 内存集合 ItemSet 中。在 MVCC 模块持久化存储 key-value 的时候,会将 key-value、关联的 LeaseID 等信息保存到 boltdb 中,从而支持重启后恢复数据。

Lease 续约与性能优化

Lease 续约,就是定期放送请求给 etcd 来续约 Lease。而 etcd 内部就是将 Lease 的过期时间更新为当前系统时间加其 TTL。

那么当 Lease 过多后,性能能否保证?早期 etcd v2 版本,没有 Lease,直接对 key 操作 TTL,每一个 key 创建一个 HTTP/1.x 连接,定时发送续期请求给 etcd server。

etcd v3 提出 Lease 特性,将 TTL 独立出来,从而可以得以复用。同时协议从 HTTP/1.x 优化成 gRPC HTTP/2 协议,实现多路复用,流式传输,同一连接可支持为多个 Lease 续期,大大减少了连接数。

Lease 的复用有什么案例吗?K8S Events 事件存在过期机制,默认 1h。当 K8S 上服务较多时,Events 会很多,如果不复用 Lease,那么就会存在过多 Lease。Lease 过期淘汰会默认限速每秒 1000 个。因此 K8S 场景为了优化 lease 数,会将最近一分钟内产生的 event key 列表,复用在同一个 lease,大大降低了 lease 数。

Lease 淘汰机制

淘汰过期 Lease 的工作由 Lessor 模块的一个异步 goroutine 负责,会 500ms 定时从最小堆中取出已过期的 Lease,执行删除 Lease 和其关联的 key 列表数据的 RevokeExpiredLease 任务。

早期是全部遍历,每次检查都是 O(N) 时间复杂度。使用堆后,若堆顶不过期,那么时间复杂度就是 O(1).

获取到待过期的 LeaseID 后,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。

Lease 的 checkpoint 机制

Leader 不可用时,Follower 会基于 Lease map 信息重建 Lease 过期最小堆等管理数据结构。

早起 etcd 没有持久化剩余 TTL 信息,导致重建过程相当于给所有 Lease 续期,进而导致如果频繁 Leader 切换,那么 Lease 就永不过期。

为了解决这个问题,etcd 引入了检查点机制,有个 go 协程执行 CheckPointScheduledLeases 的任务。

一方面,etcd 启动的时候,Leader 节点后台会运行此异步任务,定期批量地将 Lease 剩余的 TTL 基于 Raft Log 同步给 Follower 节点,Follower 节点收到 CheckPoint 请求后,更新内存数据结构 LeaseMap 的剩余 TTL 信息。

另一方面,当 Leader 节点收到 KeepAlive 请求的时候,它也会通过 checkpoint 机制把此 Lease 的剩余 TTL 重置,并同步给 Follower 节点,尽量确保续期后集群各个节点的 Lease 剩余 TTL 一致性。

此特性对性能有一定影响,目前仍然是试验特性,可以通过 experimental-enable-lease-checkpoint 参数开启。