「etcd」- 通过 Lease 特性,检测节点存活

解决方案

原理简述:
1)首先,需要创建一个与节点健康指标相关的 Lease,并将该 Lease 与节点健康指标 key 关联;
2)然后,在执行健康检查的时候,需要周期更新 Lease 的 TTL;
3)如果某个节点异常,则其将无法对 Lease 进行正常续期,那么随着时间消逝,对应的 Lease 则会过期,Lessor 主循环定时轮询过期的 Lease。获取到 ID 后,Leader 发起 revoke 操作,通知整个集群删除 Lease 和关联的数据;

第一步、创建 Lease 实例

为节点健康指标创建一个租约:

# 创建一个 TTL 为 600s 的 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 server 收到 client 的创建一个有效期 600s 的 Lease 请求后,会通过 Raft 模块完成日志同步,随后 Apply 模块通过 Lessor 模块的 Grant 接口执行日志条目内容。然后 Lessor 的 Grant 接口会把 Lease 保存到内存的 ItemMap 数据结构中,然后它需要持久化 Lease,将 Lease 数据保存到 boltdb 的 Lease bucket 中,返回一个唯一的 LeaseID 给 client;

第二步、Key 关联 Lease

然后,将节点的健康指标数据关联到此 Lease 上:

KV 模块的 API 接口提供 –lease 参数,来将 key node 关联到对应的 LeaseID 上:

$ etcdctl put node healthy --lease 326975935f48f818
OK

// 查询的时候增加 -w 参数输出格式为 json,就可查看到 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
        }
    ]
}

通过 put 等命令,当新增一个指定 –lease 参数的 key 时,MVCC 模块它会通过 Lessor 模块的 Attach 方法,将 key 关联到 Lease 的 key 内存集合(ItemSet)中;

Q:一个 Lease 关联的 key 集合是保存在内存中的,那么 etcd 重启时,是如何知道每个 Lease 上关联了哪些 key 呢?
A:etcd 的 MVCC 模块在持久化存储 KV 的时候,保存到 boltdb 的 value 是个结构体(mvccpb.KeyValue), 它不仅包含你的 KV 数据,还包含了关联的 LeaseID 等信息。因此当 etcd 重启时,可根据此信息,重建关联各个 Lease 的 key 集合列表;

第三步、发送健康检查(续期)

通过以上流程,我们完成了 Lease 创建和数据关联操作;

在正常情况下,当节点存活时,需要定期发送 KeepAlive 请求给 etcd 来续期用于健康状态的 Lease,否则 Lease 及其关联的数据就会被删除;

续期操作 Client 是必须要直接发送给 Leader 的,如果 Follower 收到 Keepalive 请求,会转发给 Leader 节点;

续期操作不经过 raft 协议处理同步,而 lease Grant/Revoke 请求会经过 raft 协议同步给各个节点,因此任意节点都可以处理它;

性能优化:提高 Lease 续期性能

Lease 续期其实很简单,核心是将 Lease 的过期时间更新为当前系统时间加其 TTL;

关键问题在于续期的性能能否满足业务诉求。作为一个高频率的请求 API,etcd 需要优化 Lease 续期的性能;

然而影响续期性能因素又是源自多方面的:
1)TTL:过长,将导致节点异常后无法及时从 etcd 中删除,影响服务可用性;过短,则要求 client 频繁发送续期请求;
2)其次是 Lease 数,如果 Lease 成千上万个,那么 etcd 可能无法支撑如此大规模的 Lease 数,导致高负载;

在早期 v2 版本中,没有 Lease 概念,TTL 属性是在 key 上面。为了保证 key 不删除(续期),即便 TTL 相同,client 也需要为每个 TTL、key 创建一个 HTTP/1.x 连接,定时发送续期请求给 etcd server;

etcd v3 版本提出 Lease 特性:
1)TTL 属性转移到 Lease 上, 不同 key 若 TTL 相同,可复用同一个 Lease, 显著减少了 Lease 数
2)协议从 HTTP/1.x 优化成 gRPC 协议,通过 gRPC HTTP/2 实现多路复用,流式传输,同一连接可支持为多个 Lease 续期,大大减少了连接数;

第四步、淘汰过期 Lease 实例

当节点异常时,将无法正常续期后,etcd 需要淘汰过期 Lease,并删除节点健康指标 key 的;

淘汰过期 Lease 的工作由 Lessor 模块的 RevokeExpiredLease(一个异步 goroutine)负责;

它会定时从最小堆中取出已过期的 Lease,执行删除 Lease 和其关联的 key 列表数据的 RevokeExpiredLease 任务;

所示如图,目前 etcd 是基于最小堆来管理 Lease,实现快速淘汰过期的 Lease;

早期

etcd 早期的时候,淘汰 Lease 非常暴力。etcd 会直接遍历所有 Lease,逐个检查 Lease 是否过期,过期则从 Lease 关联的 key 集合中,取出 key 列表,删除它们,时间复杂度是 O(N)。然而这种方案随着 Lease 数增大,毫无疑问它的性能会变得越来越差;

现在(性能优化:过期 Lease 高效淘汰)

我们能否按过期时间排序呢?这样每次只需轮询、检查排在前面的 Lease 过期时间,一旦轮询到未过期的 Lease, 则可结束本轮检查;

刚刚说的就是 etcd Lease 高效淘汰方案最小堆的实现方法。每次 Lease 新增或续期的时候,它会插入、更新一个对象到最小堆中,对象含有 LeaseID 和其到期时间 unixnano,对象之间按到期时间升序排序;

etcd Lessor 主循环每隔 500ms 执行一次撤销 Lease 检查(RevokeExpiredLease),每次轮询堆顶的元素,若已过期则加入到待淘汰列表,直到堆顶的 Lease 过期时间大于当前,则结束本轮轮询;

当使用堆后,插入、更新、删除,它的时间复杂度是 O(Log N),查询堆顶对象是否过期时间复杂度仅为 O(1),性能大大提升,可支撑大规模场景下 Lease 的高效淘汰;

删除 Follower 的 Lease 实例

获取到待过期的 LeaseID 后,Leader 是如何通知其他 Follower 节点淘汰它们呢?

Lessor 模块会将已确认过期的 LeaseID,保存在一个名为 expiredC 的 channel 中,而 etcd server 的主循环会定期从 channel 中获取 LeaseID,发起 revoke 请求,通过 Raft Log 传递给 Follower 节点;

各个 Follower 收到 revoke Lease 请求后:
1)获取关联到此 Lease 上的 key 列表,从 boltdb 中删除 key,
2)从 Lessor 的 Lease map 内存中删除此 Lease 对象,最后还需要从 boltdb 的 Lease bucket 中删除这个 Lease;

性能优化:checkpoint

问题描述

检查 Lease 是否过期、维护最小堆、针对过期的 Lease 发起 revoke 操作,都是 Leader 节点负责的,它类似于 Lease 的仲裁者,通过以上清晰的权责划分,降低了 Lease 特性的实现复杂度;

那么当 Leader 因重启、crash、磁盘 IO 等异常不可用时,Follower 节点就会发起 Leader 选举,新 Leader 要完成以上职责,必须重建 Lease 过期最小堆等管理数据结构;

当你的集群发生 Leader 切换后,新的 Leader 基于 Lease map 信息,按 Lease 过期时间构建一个最小堆时,etcd 早期版本为了优化性能,并未持久化存储 Lease 剩余 TTL 信息,因此重建的时候就会自动给所有 Lease 自动续期了;

然而若较频繁出现 Leader 切换,切换时间小于 Lease 的 TTL,这会导致 Lease 永远无法删除,大量 key 堆积,db 大小超过配额等异常;

解决方案

为了解决这个问题,etcd 引入了检查点机制,也就是下面架构图中黑色虚线框所示的 CheckPointScheduledLeases 的任务;

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

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

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