etcd/lease lessor · 到期检查与撤销通知

用索引堆检查租约到期,并重试尚未完成的撤销

etcd 用按检查时间排序的小顶堆寻找待检查租约,用租约 ID 索引直接定位堆元素;到期检测只发起撤销,实际删除还要经过一致性处理。

v3.5.17调度与资源分配核验 2026-10-12
相关概念

工程问题#

大量租约不断续期,逐轮扫描全部租约代价高;为每个租约独立创建定时器也会增加管理开销。同时,发现到期不等于撤销已完成,通知拥塞、领导者变更或请求失败都可能延迟删除。

实际实现#

LeaseExpiredNotifier 同时维护最小堆和 ID 到堆元素的 map,元素保存堆下标,Swap 时同步下标。续租刷新 expiry 后,以 map 找到旧元素并 heap.Fix。lessor 每轮查看堆顶:未到检查时间就停止;已经移除的租约清出堆;到检查时间的租约先把下次检查时间推迟,再核验租约自身是否过期并加入通知批次。服务器收到批次后调用 LeaseRevoke,经 Raft 请求处理撤销,因此检查时间与最终删除时间是两个不同阶段。

处理流程#

  1. 授予或续租后,注册或更新租约的堆内检查时间
  2. 主 lessor 周期性查看最早的检查项,不扫描所有未到期租约
  3. 清除已不存在的租约项;对到时的项登记下一次重试时间
  4. 检查实际租约 expiry,把真正过期的租约加入受批次限制的通知
  5. 服务器确认领导权并发起 LeaseRevoke,撤销完成后再清理后续检查项

源码片段#

固定版本 · 原文摘录
server/lease/lease_queue.gov3.5.17 · L35–L49
查看原文件
35func (pq LeaseQueue) Less(i, j int) bool {36	return pq[i].time.Before(pq[j].time)37}3839func (pq LeaseQueue) Swap(i, j int) {40	pq[i], pq[j] = pq[j], pq[i]41	pq[i].index = i42	pq[j].index = j43}4445func (pq *LeaseQueue) Push(x interface{}) {46	n := len(*pq)47	item := x.(*LeaseWithTime)48	item.index = n49	*pq = append(*pq, item)
LeaseQueue · 最小堆与位置索引

Less 以时间排序;Swap 在交换节点后更新 index,供后续 heap.Fix 直接定位。Push 是 Go heap.Interface 的底层操作,完整堆维护由 container/heap 完成。

server/lease/lease_queue.gov3.5.17 · L83–L104
查看原文件
83func (mq *LeaseExpiredNotifier) RegisterOrUpdate(item *LeaseWithTime) {84	if old, ok := mq.m[item.id]; ok {85		old.time = item.time86		heap.Fix(&mq.queue, old.index)87	} else {88		heap.Push(&mq.queue, item)89		mq.m[item.id] = item90	}91}9293func (mq *LeaseExpiredNotifier) Unregister() *LeaseWithTime {94	item := heap.Pop(&mq.queue).(*LeaseWithTime)95	delete(mq.m, item.id)96	return item97}9899func (mq *LeaseExpiredNotifier) Poll() *LeaseWithTime {100	if mq.Len() == 0 {101		return nil102	}103	return mq.queue[0]104}
LeaseExpiredNotifier · 定位、修复和查看堆顶

map 找到旧项后只修改时间并修复堆,不为同一租约积累多个检查记录。Poll 只查看最早项,Unregister 才弹出并删除索引。

server/lease/lessor.gov3.5.17 · L700–L718
查看原文件
700	item := le.leaseExpiredNotifier.Poll()701	l = le.leaseMap[item.id]702	if l == nil {703		// lease has expired or been revoked704		// no need to revoke (nothing is expiry)705		le.leaseExpiredNotifier.Unregister() // O(log N)706		return nil, false, true707	}708	now := time.Now()709	if now.Before(item.time) /* item.time: expiration time */ {710		// Candidate expirations are caught up, reinsert this item711		// and no need to revoke (nothing is expiry)712		return l, false, false713	}714715	// recheck if revoke is complete after retry interval716	item.time = now.Add(le.expiredLeaseRetryInterval)717	le.leaseExpiredNotifier.RegisterOrUpdate(item)718	return l, true, false
expireExists · 到时后保留重试项

不存在的租约从堆中清除;未来项使本轮停止;到时项先推迟下次检查,防止尚未完成的撤销在同轮立即反复触发。

server/lease/lessor.gov3.5.17 · L726–L745
查看原文件
726	for {727		l, ok, next := le.expireExists()728		if !ok && !next {729			break730		}731		if !ok {732			continue733		}734		if next {735			continue736		}737738		if l.expired() {739			leases = append(leases, l)740741			// reach expired limit742			if len(leases) == limit {743				break744			}745		}
findExpiredLeases · 核验实际 expiry

堆中到时表示需要检查,最终是否进入撤销批次还要调用 l.expired。批次限制统计真正过期的租约数,而非所有访问的堆项。

server/etcdserver/server.gov3.5.17 · L1182–L1203
查看原文件
1182		// Increases throughput of expired leases deletion process through parallelization1183		c := make(chan struct{}, maxPendingRevokes)1184		for _, curLease := range leases {1185			select {1186			case c <- struct{}{}:1187			case <-s.stopping:1188				return1189			}11901191			f := func(lid int64) {1192				s.GoAttach(func() {1193					ctx := s.authStore.WithRoot(s.ctx)1194					_, lerr := s.LeaseRevoke(ctx, &pb.LeaseRevokeRequest{ID: lid})1195					if lerr == nil {1196						leaseExpired.Inc()1197					} else {1198						lg.Warn(1199							"failed to revoke lease",1200							zap.String("lease-id", fmt.Sprintf("%016x", lid)),1201							zap.Error(lerr),1202						)1203					}
revokeExpiredLeases · 发起撤销

服务器使用有界并发处理批次,并调用 LeaseRevoke。错误被记录,检查堆保留的后续重试仍有机会重新通知。

server/etcdserver/v3_server.gov3.5.17 · L290–L296
查看原文件
290func (s *EtcdServer) LeaseRevoke(ctx context.Context, r *pb.LeaseRevokeRequest) (*pb.LeaseRevokeResponse, error) {291	resp, err := s.raftRequestOnce(ctx, pb.InternalRaftRequest{LeaseRevoke: r})292	if err != nil {293		return nil, err294	}295	return resp.(*pb.LeaseRevokeResponse), nil296}
LeaseRevoke · 进入 Raft 请求路径

堆顶检测没有直接删除键。真正的撤销请求被提交到 Raft 处理路径,完成时刻受共识和应用进度影响。

六段原文覆盖索引堆、检查项更新、候选判断、实际 expiry 核验以及撤销请求入口;周期调度、续租和领导权检查见补充来源。

收益与取舍#

工程分析

带来的收益

  • 查看最早检查项为 O(1),借助 ID 索引更新堆项为 O(log N),避免续租时线性寻找租约
  • 只保存每个租约的一条检查项,重复续租不会不断积累过期定时任务
  • 撤销未完成时保留重试机会,通知通道暂时忙也不会直接丢弃该租约

付出的代价

  • 索引 map 和堆下标必须同步维护,空间为 O(N);比只追加定时记录的实现更复杂
  • 每轮实际工作取决于到期候选和陈旧项数量;撤销批次上限不等于严格限制本轮检查的堆项总数
  • 500 ms 的循环等待、批次限制、重试间隔及 Raft 提交都会带来延迟;TTL 到点不保证关联键在同一时刻立刻删除

可比较的选择

定期扫描全部租约实现直接,但即使几乎没有到期租约,每轮仍需检查 O(N) 个对象。

时间轮大量粗粒度定时任务可减少堆维护,但需要选择槽宽、处理跨轮长 TTL 与续租迁移;精度和批处理行为不同。

版本与适用范围#

固定 etcd v3.5.17 的 lease lessor,分析到期检查堆,不混同另一个 lease checkpoint 堆。最初堆时间对应 expiry;触发检查后也会被改为重试时刻,所以不是所有时候都等于租约的真实到期时间。撤销通知仅由 primary 发出,最终删除由服务器的 Raft 请求与应用路径完成。

参考来源#

源码、官方文档与原始论文
01
etcd · LeaseQueue · 最小堆与位置索引

server/lease/lease_queue.go · LeaseQueue · 最小堆与位置索引

源码v3.5.17
02
etcd · LeaseExpiredNotifier · 定位、修复和查看堆顶

server/lease/lease_queue.go · LeaseExpiredNotifier · 定位、修复和查看堆顶

源码v3.5.17
03
etcd · expireExists · 到时后保留重试项

server/lease/lessor.go · expireExists · 到时后保留重试项

源码v3.5.17
04
etcd · findExpiredLeases · 核验实际 expiry

server/lease/lessor.go · findExpiredLeases · 核验实际 expiry

源码v3.5.17
05
etcd · revokeExpiredLeases · 发起撤销

server/etcdserver/server.go · revokeExpiredLeases · 发起撤销

源码v3.5.17
06
etcd · LeaseRevoke · 进入 Raft 请求路径

server/etcdserver/v3_server.go · LeaseRevoke · 进入 Raft 请求路径

源码v3.5.17
07
etcd · runLoop / revokeExpiredLeases · 周期、primary 与通道背压

server/lease/lessor.go · runLoop / revokeExpiredLeases · 周期、primary 与通道背压

源码v3.5.17
08
etcd · Renew · 续租与更新检查项

server/lease/lessor.go · Renew · 续租与更新检查项

源码v3.5.17
09
etcd · revokeExpiredLeases · 确认领导权

server/etcdserver/server.go · revokeExpiredLeases · 确认领导权

源码v3.5.17

相关实现与比较#

4 个相关案例
QEMU·TCG 翻译块v9.2.0

QEMU TCG 用 QHT 保存 TranslationBlock 索引,帮助复用与当前客体执行状态一致的翻译结果。

附源码片段 · 缓存与复用
SwarmKit·服务调度器1fd637ba5

SwarmKit 的调度决策树在叶子中使用 nodeMaxHeap,把候选节点整理为从优到劣的顺序。

附源码片段 · 调度与资源分配
19 个匹配案例