kube-controller-manager的leader选举流程和策略

一、整体架构

kube-controller-manager 的 Leader Election 代码横跨三层:

层次 路径 职责
编排层 cmd/kube-controller-manager/app/controllermanager.go 决定是否启用 LE、编排多锁竞速、Controller 分配
选举引擎 staging/src/k8s.io/client-go/tools/leaderelection/leaderelection.go acquirerenewrelease 核心循环
锁实现 staging/src/k8s.io/client-go/tools/leaderelection/resourcelock/ 基于 Lease API 对象的分布式锁

二、三个核心时间参数

Leader Election 的行为由三个时间参数控制(默认值):

ini 复制代码
LeaseDuration = 15s    ← 租约有效期。非 Leader 等待这么久未观察到续约后,才能尝试抢占
RenewDeadline = 10s    ← 续约截止时间。Leader 必须在此时间内成功续约,否则放弃
RetryPeriod    = 2s    ← 重试间隔。每次尝试 Acquire/Renew 的间隔

约束条件 (在 NewLeaderElector 中强制校验):

scss 复制代码
LeaseDuration > RenewDeadline > RetryPeriod × 1.2 (JitterFactor)

时序关系示意:

ini 复制代码
时间轴:  0s        2s        4s        6s        8s       10s       12s       15s
         |---------|---------|---------|---------|---------|---------|---------|
Leader:  [Renew]   [Renew]   [Renew]   [Renew]   [Renew]   ↑
                                                         RenewDeadline=10s
                                                         (必须在此前续约成功)
                                                                              ↑
                                                                     LeaseDuration=15s
                                                                     (超时后其他候选者可抢占)

三、Leader Election 主流程

核心入口在 leaderelection.goRun() 方法(L211):

scss 复制代码
┌──────────────────────────────────────────────────────────┐
│                    LeaderElector.Run(ctx)                │
├──────────────────────────────────────────────────────────┤
│  1. acquire(ctx)          ← 阻塞直到获取锁或 ctx 取消     │
│     ├─ 循环: tryAcquireOrRenew(ctx) 每 RetryPeriod 一次   │
│     └─ 成功 → 退出循环                                    │
│                                                          │
│  2. go OnStartedLeading(ctx)  ← 异步启动业务逻辑           │
│                                                          │
│  3. renew(ctx)             ← 阻塞直到续约失败或 ctx 取消   │
│     ├─ 循环: PollUntilContextTimeout(                    │
│     │     RetryPeriod, RenewDeadline,                    │
│     │     tryAcquireOrRenew)                             │
│     ├─ 在 RenewDeadline 内续约成功 → 继续循环              │
│     └─ 续约失败 → 退出,放弃 Leader 身份                   │
│                                                          │
│  4. OnStoppedLeading()    ← 总是调用(无论是否当过 Leader) │
│     └─ 如果 ReleaseOnCancel → tryRelease() 主动释放锁      │
└──────────────────────────────────────────────────────────┘

四、核心方法:tryAcquireOrRenew 的细节

这是同时用于 获取锁续约锁 的核心方法(L444)。

Fast Path(快速路径)------ Leader 乐观续约:

kotlin 复制代码
if 我是 Leader && 租约未过期:
    直接 Update Lease 对象(使用上次观察到的 ResourceVersion)
    ├─ 成功 → 更新本地 observedRecord,return true
    └─ 失败(冲突)→ 进入 Slow Path

大多数情况下 Leader 的续约走 Fast Path,只需一次 API 调用。

Slow Path(慢速路径)------ 读-改-写:

sql 复制代码
1. Get Lease 对象
   ├─ NotFound → Create Lease(我是第一个候选者,直接成为 Leader)
   └─ 获取到记录 → 进入判断
   
2. 判断是否可抢占
   if HolderIdentity 非空 && 租约未过期 && 我不是 Holder:
       return false  ← 别人持有有效租约,不能抢占
       
3. Update Lease 对象
   ├─ 我是 Leader → 续约(LeaderTransitions 不变)
   └─ 我不是 Leader → 抢占(LeaderTransitions + 1)

租约过期判断isLeaseValid):

Go

arduino 复制代码
// 上次观察到续约的时间 + LeaseDuration > 当前时间 → 租约有效
observedTime + LeaseDuration > now

五、ResourceLock:基于 Lease API 的锁实现

锁的具体实现是 LeaseLockleaselock.go),操作的是 coordination.k8s.io/v1Lease 资源。

历史演进:Endpoints → ConfigMaps → ConfigMapsLeases/EndpointsLeases → Leases。现在只支持 leases 类型,旧类型已移除。

Lease 对象的 .spec 包含:

字段 含义
HolderIdentity 当前持有者的唯一标识(hostname_UUID
LeaseDurationSeconds 租约时长(秒)
AcquireTime 获取锁的时间
RenewTime 最近一次续约时间
LeaseTransitions Leader 切换次数
PreferredHolder 优先持有者(用于协调选举)
Strategy 协调选举策略(OldestEmulationVersion

Identity 生成controllermanager.go L307-L313):

Go

ini 复制代码
id = hostname + "_" + uuid.NewUUID()

这样即使同一台机器上运行两个进程,也不会冲突。


六、kube-controller-manager 层的编排逻辑

Run() 方法(L199)中的编排分为三种模式:

模式 1:无 Leader Election(--leader-elect=false

scss 复制代码
直接启动所有 Controller → run(ctx, allDescriptors)

单实例部署或调试时使用。

模式 2:标准 Leader Election(默认)

ini 复制代码
                    ┌─ 获锁成功 ─→ run(ctx, allControllers)
leaderElectAndRun ──┤
  (main lock)       └─ 获锁失败 ─→ 阻塞等待,持续竞争
                    
                    ┌─ 获锁成功 ─→ OnStartedLeading → run()
LeaderElector.Run ──┤
                    └─ 续约失败 ─→ OnStoppedLeading
                                    ├─ ReleaseOnCancel=true → 主动释放锁,优雅退出
                                    └─ ReleaseOnCancel=false → os.Exit(1) 硬退出

关键:ReleaseOnCancel

ControllerManagerReleaseLeaderElectionLockOnExit Feature Gate 启用时,续约失败后不会立即 os.Exit(1),而是主动将 Lease 的 LeaseDurationSeconds 设为 1 秒(L344),让其他候选者更快接管,减少 Leader 切换的停机时间。

模式 3:Leader Migration(Leader 迁移)

当配置了 LeaderMigration 时(L319-L454),存在 两个独立的锁

scss 复制代码
                    ┌─────────────────────────────────────┐
                    │         Main Lock (主锁)             │
                    │  kube-controller-manager             │
                    │  ┌─────────────────────────────┐     │
                    │  │ Non-Migrated Controllers    │     │
                    │  │ (Deployment, ReplicaSet...) │     │
                    │  │ SA Token Controller         │     │
                    │  └─────────────────────────────┘     │
                    └─────────────────────────────────────┘

                    ┌─────────────────────────────────────┐
                    │      Migration Lock (迁移锁)         │
                    │  kube-controller-manager             │
                    │  ┌─────────────────────────────┐     │
                    │  │ Migrated Controllers        │     │
                    │  │ (如 ServiceAccount, HPA...) │     │
                    │  └─────────────────────────────┘     │
                    └─────────────────────────────────────┘

流程

  1. 先竞争 Main Lock,启动 SA Token Controller
  2. SA Token Controller 启动后 → 关闭 MigrationReady channel
  3. 收到信号后 → 竞争 Migration Lock
  4. 两个锁可以分别被 不同的 KCM 实例 持有
  5. 每个 Controller 通过 FilterFunc 决定归属哪个锁

这样可以将部分 Controller 从主 KCM 实例迁移到另一个实例运行,实现 Controller 的平滑迁移。


七、Coordinated Leader Election(协调选举,Alpha)

当启用 CoordinatedLeaderElection Feature Gate 时(L345-L371):

  1. 额外创建一个 LeaseCandidate 对象(类型为 coordination.k8s.io/v1beta1
  2. LeaseCandidate 定期 RenewTime 续约(每 5 分钟)
  3. API Server 侧根据所有 LeaseCandidate 的状态,协调决定谁是 Leader
  4. tryCoordinatedRenew 替代 tryAcquireOrRenew,使用协调策略

与传统模式的核心区别:

特性 传统模式 Coordinated 模式
选举决策 客户端竞争,先到先得 服务端根据策略协调
LeaseCandidate 有(声明候选资格)
PreferredHolder 不支持 支持(优雅交接)
策略 OldestEmulationVersion(优先选择最低模拟版本的实例)

PreferredHolder 被设置时,当前 Leader 会主动放弃续约(L408),实现无中断的 Leader 交接。


八、Health Check & Watchdog

Leader Election 的状态通过 Healthz 暴露(L228-L231):

Go

ini 复制代码
electionChecker = leaderelection.NewLeaderHealthzAdaptor(time.Second * 20)
  • 当 Leader 续约成功时 → healthz 返回 OK
  • 当续约失败超过 20 秒 → healthz 返回失败,触发就绪探测失败
  • 配合 Check() 方法检测死锁场景(如续约 goroutine 卡死但进程未退出)

九、完整时序图

scss 复制代码
实例 A (候选者)                           API Server                  实例 B (候选者)
     │                                        │                            │
     │  acquire(): tryAcquireOrRenew          │                            │
     │──Get Lease────────────────────────────→│                            │
     │←─NotFound──────────────────────────────│                            │
     │──Create Lease (HolderIdentity=A)──────→│                            │
     │←─OK (A 成为 Leader)────────────────────│                            │
     │                                        │                            │
     │  OnStartedLeading → run(controllers)   │                            │
     │                                        │                            │
     │  renew(): tryAcquireOrRenew (FastPath) │  acquire(): tryAcquireOrRenew
     │──Update Lease─────────────────────────→│←─Get Lease─────────────────│
     │←─OK────────────────────────────────────│──Lease{A, 未过期}──────────→│
     │                                        │  return false (等待...)     │
     │  ... 每 2s 续约一次 ...                │  ... 每 2s 检查一次 ...    │
     │                                        │                            │
     │  ❌ 续约失败 (网络超时)                │                            │
     │  renew() 在 RenewDeadline=10s 内重试   │                            │
     │  全部失败                              │                            │
     │                                        │                            │
     │  OnStoppedLeading                      │  LeaseDuration=15s 到期     │
     │  ReleaseOnCancel → tryRelease()        │                            │
     │  os.Exit(1)                            │  Get Lease → 租约过期!      │
     │                                        │  Update Lease (Holder=B)    │
     │                                        │←─OK─────────────────────────│
     │                                        │  B 成为新 Leader            │
     │                                        │  OnStartedLeading → run()   │

十、总结

kube-controller-manager 的 Leader Election 核心策略:

  1. 基于 Lease API 的乐观锁:利用 Kubernetes 的 ResourceVersion 实现 CAS 语义
  2. Fast Path + Slow Path:Leader 续约走乐观更新,减少 API 调用;冲突时回退到读-改-写
  3. Jitter 防惊群:RetryPeriod 使用 1.2 倍随机抖动,避免所有候选者同时竞争
  4. 主动释放ReleaseOnCancel 将 Lease 时长缩短为 1 秒,加速 Leader 切换
  5. Leader Migration:通过双锁机制实现 Controller 在不同实例间的平滑迁移
  6. Coordinated Election(Alpha):服务端策略协调替代客户端盲目竞争
相关推荐
妙码生花2 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十四):管理员个人资料页面、管理员日志优化
前端·后端·go
大卫陈2 小时前
PCB 拼版系统近期迭代复盘:大小拼跃迁、横直料重构与引擎打磨
后端·架构
二月龙2 小时前
JS 垃圾回收:为什么你明明释放了变量,内存还是爆了?
后端
长大19882 小时前
Promise 从入门到踩坑:为什么你的异步代码还是一团乱麻
后端
颜进强2 小时前
Calude Code - 25 CodeGraph:让 AI 真正读懂你的代码库
前端·后端·ai编程
七牛开发者2 小时前
Agent 小知识|长任务不重来:Agent 状态保存的工程设计
前端·javascript·后端
二月龙2 小时前
JS 事件循环完整解析:宏任务、微任务,浏览器到底怎么执行代码
后端
长大19882 小时前
写了多年 JS 却仍被“变量提升”拿捏?这篇文章一次讲透
后端