第 9 篇 说明
Lease 过期删除会触发 Watch DELETE。K8s Node
Lease、分布式锁、concurrency.Mutex 都压在 Lease
轴上。常见误区是把 KeepAlive 失败 一律归因
Raft 或磁盘——实际上要先分列:请求是否到达 primary
lessor;Renew 走本地还是 HTTP 转发;Leader 切换窗口里
Promote/Demote 如何改 expiry。
本篇钉 lessor 与 KeepAlive RPC 在 v3.5.33 的分工:Grant/Revoke/Checkpoint 经 Raft;Renew 默认不经 Raft;checkpoint 把剩余 TTL 写入共识日志;Leader 切换时 Demote 冻结 expiry、Promote 展期。不写未测 TTL 漂移数字。
本篇在系列中的位置
篇目 核心内容 第 9 篇 · Watch 机制 watchableStore、ErrCompacted 第 10 篇 · Lease 与 KeepAlive lessor、TTL、checkpoint、Leader 切换 第 11 篇 · Txn 与并发语义 compare/mod、Jepsen 边界 系列目录 五轴、阅读路径
版本锚定:etcd v3.5.33(tag
v3.5.33)。对齐server/lease/lessor.go、server/etcdserver/v3_server.go(LeaseRenew)、server/etcdserver/api/v3rpc/lease.go@ etcd-io/etcd v3.5.33。Lease checkpoint 需--enable-lease-checkpoint(及可选--lease-checkpoint-interval,默认 5min)。无真实集群则不粘贴伪造etcdctl lease list输出。
一、lessor 结构与 primary 语义
lease.Lessor
接口(lessor.go)管理
Grant、Revoke、Renew、Attach/Detach、Checkpoint、Promote/Demote。每个
Lease 维护:
ttl(秒级,最小minLeaseTTL,最大MaxLeaseTTL = 9e9);expiry绝对过期时间;itemSet:绑定 key 集合(Attach 时写入itemMap反查)。
Primary lessor 只在 Leader
上生效:Follower 调用 Renew 返回
ErrNotPrimary,由
LeaseRenew 转发到 Leader peer
HTTP(leasehttp.RenewHTTP)。Follower 上
Demote() 把所有 lease 的 expiry 设为
forever,停止本地过期扫描——避免脑裂下双主各自续租。
flowchart LR
client["client KeepAlive"] --> grpc["LeaseKeepAlive gRPC"]
grpc --> renew["LeaseRenew"]
renew -->|"isLeader + ensureLeadership"| local["lessor.Renew"]
renew -->|"follower"| http["leasehttp → Leader"]
grant["LeaseGrant"] --> raft["Raft propose"]
raft --> apply["apply → lessor.Grant"]
cp["LeaseCheckpoint"] --> raft
二、Grant、Revoke 与过期删除
| 操作 | 是否经 Raft | Apply 后效果 |
|---|---|---|
| LeaseGrant | 是 | lessor.Grant;持久化到 bbolt lease
bucket |
| LeaseRevoke | 是 | Revoke → TxnDelete 删绑定 key
→ Watch DELETE |
| LeaseCheckpoint | 是 | Checkpoint(id, remainingTTL) 更新内存 +
可选 persist |
| KeepAlive / Renew | 否(Leader 本地;Follower 转发) | refresh(0) 重置 expiry;清
remainingTTL 时可能先写 checkpoint |
runLoop 每 500ms
扫描:revokeExpiredLeases 速率
leaseRevokeRate/2(默认 1000/s
量级,测试可调);过期 lease 送入 expiredC,由
etcdserver 走 Raft LeaseRevoke
保证 删除经共识复制。
Attach 在 Put 带 lease ID 时由 MVCC 路径调用,不单独占一条 Raft op——但绑定关系在 lease 元数据中跟踪。
三、KeepAlive:不经 Raft 的续租路径
LeaseKeepAlive 是
双向流(lease.go)。每次
Recv 到 ID 后:
- 先
hdr.fill(resp.Header)——注释写明:保证响应 header revision ≤ 续租发生时的本地 revision,避免「revoke 已在 rev 3 发生、客户端却看到 rev 4 keepalive 成功」的假象; - 调用
LeaseRenew→lessor.Renew。
LeaseRenew(v3_server.go)要点:
- Leader:
ensureLeadership()+waitAppliedIndex()后lessor.Renew; - 非 Leader:
waitLeader+ 对 peer URL 发 HTTP Renew; - 注释:renewals don’t go through raft; forward to leader manually。
Renew 成功则 l.refresh(0) 重算
expiry,更新
leaseExpiredNotifier
最小堆。若 lease 已过期但 revoke 未完成,Renew
阻塞在 revokec 或
demotec(Demote
时关闭)——客户端应重试或换 endpoint。
排障:KeepAlive 返回
ErrGRPCNoLeader 常见于 stream
context 取消映射(lease.go);与 MVCC
no leader 轴 1 一致,但
不经过 WAL 写放大。
四、Checkpoint 与 remainingTTL
Leader 切换或长时间分区后,新 Leader 的 wall clock 与旧 primary 的 expiry 可能不对齐。Lease checkpoint(v3.5 已 GA)周期性把 剩余 TTL 写入 Raft:
defaultLeaseCheckpointInterval = 5 * time.Minute;- 速率
leaseCheckpointRate(默认 1000 entries/s 量级); - 批大小
maxLeaseCheckpointBatchSize = 1000; - 需启动参数
--enable-lease-checkpoint;SetCheckpointer在server.go里raftRequestOnce(LeaseCheckpoint)。
Promote(extend) 在新 Leader apply
完上 term 的 noop entry
后调用(applyEntryNormal 空 entry 分支
+ isLeader()):
- 默认给所有 lease
refresh(extend),extend取ElectionTimeout; - lease 数量 ≥
leaseRevokeRate时 错开 expiry(每秒目标 revoke3/4 * leaseRevokeRate),防止切换后 revoke 风暴。
Renew 时若 remainingTTL > 0
且配置了 checkpointer,会先写
Remaining_TTL: 0 的 checkpoint
清除残留,限制 每 lease 每 interval 最多 2 条 Raft
entry(注释)。
五、Leader 切换时间线
| 阶段 | lessor 行为 | 客户端可见 |
|---|---|---|
| 旧 Leader 失去 leadership | Demote():expiry→forever,关闭
demotec |
KeepAlive 可能 ErrNotPrimary,需重连 |
| 新 Leader 当选 | 继续 Demote 直到 noop promote | — |
| 新 Leader noop committed | Promote(ElectionTimeout) 展期 |
TTL 跳变;短期「多活」窗口被展期吸收 |
| Follower | 非 primary,Renew 转发 | 转发失败则重试 peer |
与 Jepsen 的边界(详见第 11 篇):KV/Txn 经 Raft 可达 strict-serializable(Jepsen etcd 3.4.3);纯 Lease 锁在 clock + partition 组合下不在同一安全子集。distributed/53 与 distributed/51 分布式锁 的 fencing token(ModRevision) 是应用层补丁,不是 lessor 内建语义。
K8s Node
Lease(coordination.k8s.io/v1
Lease)由 kubelet 续租、controller 判 Ready——apiserver 写
etcd 的路径在第 13 篇;本篇只钉 TTL 失效落在 Lease
轴而非 Watch 轴。
六、谱系与开放问题
Chubby lease + session(Burrows 2006 工程传统)
→ ZK session timeout
→ etcd v3:独立 Lease 对象 + 多 key 绑定 + KeepAlive 流
→ v3.4+ checkpoint:Leader 切换后 TTL 可恢复性
→ 仍开放:KeepAlive 不经 Raft 与「会话线性一致」之间的产品语义缝隙
开放问题:Promote 展期 + checkpoint
间隔下 最大 TTL 漂移上界依赖
ElectionTimeout 与 lease 数量,官方未给形式化
bound;K8s 大规模 Node Lease 与 leaseRevokeRate
的交互属于运维调参,本篇无压测。
本篇不写什么:concurrency.Mutex
源码全书(见 client/v3/concurrency);Jepsen 测试复述(第 11
篇指针);伪造 lease 列表。
参考资料
规范 / 官方文档 / 源码(A)
- etcd-io/etcd
v3.5.33:server/lease/lessor.go;server/etcdserver/v3_server.go(LeaseRenew);server/etcdserver/api/v3rpc/lease.go;server/etcdserver/server.go(SetCheckpointer、Promote触发点) - etcd v3.5 · Lease API
- etcd
v3.5 ·
--enable-lease-checkpoint
博客 / 分析(B)
- Kingsbury, K., Jepsen: etcd 3.4.3(KV vs Lease 锁边界)
站内对照
上一篇:Watch 机制
下一篇:Txn 与并发语义
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【etcd】生产全景:缺口、五轴坐标系与 16 篇路线
相对 distributed/50、39、13 钉清 etcd 生产内核缺口;定义 Raft/WAL/MVCC/Watch/Lease 五轴排障坐标系,给出 16 篇阅读路线与 K8s 控制面耦合指针。版本锚定 v3.5.33。
【etcd】排障五轴:Raft/WAL/MVCC/Watch/Lease 口令表
按 Raft、WAL、MVCC、Watch、Lease 五轴做症状否证;解释 etcdctl endpoint status/health 字段语义;并对照 K8s 控制面 apiserver 超时与 Node Lease 漂移。
【etcd】单 Raft 组与服务器角色:Leader、Follower、Learner 与 request 路由
钉清 etcd v3.5.33 单 Raft 组拓扑、EtcdServer 与 go.etcd.io/raft/v3 边界、Leader/Follower/Learner 角色及 gRPC 写读路由;与 distributed/13 分工。
【etcd】WAL 与快照:预写日志、crash recovery 与 commit vs applied
拆解 etcd v3.5.33 的 WAL 段文件、Raft snapshot 触发与重启 recovery 顺序;钉清 committed index 与 applied index 分叉对排障与 K8s 控制面的含义。