bigywater.tech
Kubernetes · 21

Leader election:Lease 对象怎么选主和续约

多副本的控制器怎么保证同一时刻只有一个在干活?答案是一个叫 Lease 的 API 对象。这篇不看文档,直接让三个真实的 kube-scheduler 争一把锁,然后用 kill -9、force delete、普通删除和篡改 Lease 四种方式弄掉 leader,量出每种情况下换人要多久、Lease 里发生了什么。

2.0 秒
kube-controller-manager 与 kube-scheduler 的 Lease 相邻两次续约的间隔
16.6 秒 vs 1.1 秒
kill -9 leader(租约没释放)与普通删除(租约被释放)后新 leader 接手所花的时间
Leaderelection lost
当前 leader 发现 Lease 被别人改写、续约失败之后的日志

Lease 里写了什么

实测 · 本机执行输出
  kube-controller-manager:holderIdentity=kx-main-control-plane_20b936db-e39b-4079-989e-e33644686e8a leaseDurationSeconds=15 leaseTransitions=0;9 秒内观察到 6 次续约,相邻两次 renewTime 间隔 [2.0, 2.0, 2.0, 2.0, 2.0] 秒
  kube-scheduler:holderIdentity=kx-main-control-plane_78a75147-e8dd-4964-9cbb-3ffe8be985c2 leaseDurationSeconds=15 leaseTransitions=0;9 秒内观察到 5 次续约,相邻两次 renewTime 间隔 [2.0, 2.0, 2.0, 2.0] 秒

选主用的“锁”是 coordination.k8s.io 里的 Lease 对象:holderIdentity(主机名加随机 uuid)是当前持有者,renewTime 是它最后一次续约,leaseDurationSeconds 是别人要等多久才能抢,leaseTransitions 记录换过几次主。实测 kube-controller-manager 和 kube-scheduler 的 Lease 每 2.0 秒续约一次,leaseDurationSeconds=15。

三个候选者

实测 · 本机执行输出
  Lease:holderIdentity=sched-a_0f50a938-c557-48d3-9b89-fa313786d39a;leaseDurationSeconds=15;leaseTransitions=0;acquireTime=2026-10-10T09:03:03.226351Z
    sched-a 的日志:I1010 09:03:03.226336       1 leaderelection.go:258] "Attempting to acquire leader lease..." lock="k15/k15-sched"
    sched-a 的日志:I1010 09:03:03.230680       1 leaderelection.go:272] "Successfully acquired lease" lock="k15/k15-sched"
    sched-b 的日志:I1010 09:03:04.438823       1 leaderelection.go:258] "Attempting to acquire leader lease..." lock="k15/k15-sched"
    sched-c 的日志:I1010 09:03:04.485850       1 leaderelection.go:258] "Attempting to acquire leader lease..." lock="k15/k15-sched"

用同样的 kube-scheduler 镜像再起三个副本(自定义调度器名 k15-scheduler 与 Lease k15/k15-sched,不影响默认调度器)。先启动的 sched-a 日志里 Attempting to acquire leader lease… 后紧跟 Successfully acquired lease;sched-b、sched-c 只有前一句,一直在重试。选主就是抢一个对象的 holderIdentity,抢到的人负责续约。

换人要多久

实测 · 本机执行输出
  ① 在节点上对 leader 进程 kill -9(没有机会释放租约):原 leader=sched-a(被动手时离上次续约约 1.5 秒)
    Lease 状态变化:[+0.0s holder=sched-a leaseDuration=15] → [+16.6s holder=sched-b leaseDuration=15]
    新 leader=sched-b,接手用了 16.6 秒;leaseTransitions 0 → 1
  ② kubectl delete pod --grace-period=0 --force(API 对象立刻删掉,kubelet 仍会终止进程):原 leader=sched-b(被动手时离上次续约约 1.2 秒)
    Lease 状态变化:[+0.0s holder=(空) leaseDuration=1] → [+2.5s holder=sched-a leaseDuration=15]
    新 leader=sched-a,接手用了 2.5 秒;leaseTransitions 1 → 2
  ③ kubectl delete pod(默认宽限期,进程收到 SIGTERM):原 leader=sched-a(被动手时离上次续约约 0.9 秒)
    Lease 状态变化:[+0.1s holder=(空) leaseDuration=1] → [+1.1s holder=sched-c leaseDuration=15]
    新 leader=sched-c,接手用了 1.1 秒;leaseTransitions 2 → 3

三种让 leader 消失的方式差别很大。在节点上对进程 kill -9,它没机会释放租约,sched-b 要等租约过期:16.6 秒。kubectl delete pod --grace-period=0 --force 只是让 API 对象立刻消失,kubelet 仍会终止进程,进程退出前把 Lease 置成 holderIdentity 为空、leaseDurationSeconds=1(释放),别的候选者 2.5 秒就接手;普通删除(SIGTERM)同样释放,1.1 秒接手。所以“滚动升级时 leader 切换很快、节点突然掉电时要十几秒”是正常现象。

Lease 被改写

实测 · 本机执行输出
  把 Lease 的 holderIdentity 改成 intruder(当前 leader 是 sched-c):lease.coordination.k8s.io/k15-sched patched
  sched-c 的容器重启次数 0 → 1(12.9 秒后重启:True)
    sched-c 重启前的日志:E1010 09:03:55.382179       1 leaderelection.go:473] "Error retrieving lease lock" err="context deadline exceeded" lock="k15/k15-sched"
    sched-c 重启前的日志:I1010 09:03:55.382351       1 leaderelection.go:304] "Failed to renew lease" lock="k15/k15-sched" err="context deadline exceeded"
    sched-c 重启前的日志:I1010 09:03:55.387038       1 requestheader_controller.go:194] Shutting down RequestHeaderAuthRequestController
    sched-c 重启前的日志:E1010 09:03:55.387304       1 server.go:337] "Leaderelection lost"
  intruder 一直占着 Lease,直到 6.0 秒后 holderIdentity 才变成 sched-b(从我改写 Lease 起算 19.1 秒);leaseTransitions=4

把 holderIdentity 改成 intruder 后,当前 leader sched-c 的续约失败(Failed to renew lease … context deadline exceeded),随即 Leaderelection lost、进程退出,容器 12.9 秒后重启——leader 一旦发现自己不再持有锁就停止工作,而不是继续。新 leader 是 sched-b,在改写之后 19.1 秒才接手:它必须自己观察满一个 leaseDuration(15 秒)没有变化才敢抢,用的是自己本地的时钟,而不是 renewTime 里的时间戳。

分步演示

Lease 选主
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / kind(Docker 里的单节点或多节点真实集群,节点是 Docker Desktop 虚拟机里的容器)/ kubectl;用空的 kubeconfig 之外的独立 kubeconfig,不碰用户自己的集群;计时用 date +%s%N / kubectl 事件时间戳,数字每次运行会有小幅抖动。三节点 kind 集群 kx-main(Kubernetes v1.37.0);候选者是三个裸 Pod(sched-a/b/c),运行 registry.k8s.io/kube-scheduler:v1.37.0(节点镜像自带),配置文件设 leaderElection(leaseDuration 15s、renewDeadline 10s、retryPeriod 2s、resourceLock leases、命名空间 k15、名字 k15-sched)和 schedulerName k15-scheduler,用独立的 ServiceAccount 与 RBAC;kill -9 通过 docker exec 进节点用 crictl inspect 找到进程 pid 后 kill -9;Lease 状态用每 50 毫秒读取一次的结果记录变化;每种情形各一次,接手时间每次运行不同
    • 没有验证:真正的控制面多副本(kind 本次是单控制面);client-go 的 leaderelection 在时钟漂移下的行为;resourceLock 为 configmaps/endpoints 的旧实现;Lease 的 holderIdentity 被改写时 leader 的精确判定逻辑(只观察到了续约失败后退出);LeaderElectionCoordinated(协调式选主,Kubernetes 较新的特性);大量 Lease 对 etcd 的写入压力
    京ICP备2026067502号-1京公网安备11010502063487号