Kubernetes 的控制器和调度器这类核心组件,通常都会以多副本形式部署以保证高可用,但你有没有想过一个问题:如果一个 Deployment 跑了 3 个副本,每个副本都在监听集群状态并执行调谐逻辑,同一条数据岂不是会被重复处理三次?答案是这三个副本中同一时刻只有一个在工作,其余的都在待命,这个选主的动作就是通过 Leader Election 机制完成的。本文把这个机制的来龙去脉拆开讲清楚,包括它的演进历史、核心实现以及生产环境的踩坑经验。

一、为什么需要 Leader Election,它解决了什么问题
先看一个具体场景。假设你基于 controller-runtime 编写了一个自定义 Operator,负责管理某种自定义资源的生命周期。这个 Operator 的 Deployment 设置了 replicas 为 3。如果没有选主机制,三个副本都会通过 Informer 监听到同样的资源变更事件,然后各自执行一次创建、更新操作。对于幂等的调谐逻辑来说,结果也许只是浪费资源,但如果逻辑中涉及对外部系统的调用,比如调用云厂商 API 创建一台虚拟机,就会造成真金白银的损失。
更严重的是并发写入冲突。多个副本同时更新同一个对象,会频繁触发 OptimisticLockError(也就是基于 resourceVersion 的乐观锁冲突),控制器陷入反复重试的死循环。因此,多副本部署的控制器必须引入某种协调机制,保证同一时刻只有一个实例持有执行权,这就是 Leader Election 要解决的问题。
值得强调的是,Kubernetes 中的 Leader Election 选出来的“主”只是业务层面唯一的工作者,和数据库主从复制那种数据层面的主完全不同。被选出的 Leader 不承担数据复制的职责,它只是拿到了执行调谐逻辑的资格,其他副本随时准备在 Leader 挂掉后接管,这决定了整个机制的实现可以做得相对轻量。
二、核心实现原理:基于 Lease 的分布式锁
早期版本的实现依赖 Endpoint 或 ConfigMap 对象上的 annotation 来记录锁持有者,但从 1.14 开始官方推荐使用专门的 Lease 资源,并在 Kubernetes 1.24 之后将其设为默认锁类型。Lease 是一个非常轻量的对象,核心字段如下:
apiVersion: coordination.k8s.io/v1 kind: Lease metadata: name: my-operator-leader-election namespace: my-system spec: holderIdentity: my-operator-abcde-1 # 当前持有者的唯一标识 leaseDurationSeconds: 15 # 租约时长 acquireTime: "2024-01-01T00:00:00Z" # 获取时间 renewTime: "2024-01-01T00:00:10Z" # 最近一次续约时间 leaseTransitions: 3 # 领导权切换次数
整个选主流程可以概括为三步。第一步是竞争获取:所有副本启动后都会尝试以原子的方式更新这个 Lease 对象,把自己的身份写入 holderIdentity 字段,只有第一个成功的副本成为 Leader,失败的副本进入周期性重试。第二步是持续续约:Leader 按照 renewDeadline 的周期不断刷新 renewTime,向其他副本表明自己还活着。第三步是故障接管:如果 Leader 因为进程崩溃或网络分区无法续约,只要超过 leaseDuration 的时长没有更新 renewTime,其他副本就会认为该 Lease 已过期,重新发起竞争,新的 Leader 由此产生。
在 client-go 中,这套逻辑由 k8s.io/client-go/tools/leaderelection 包提供。下面是一段可以直接运行的选主代码,用 Lease 作为资源锁:
package main
import (
"context"
"flag"
"os"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/leaderelection"
"k8s.io/client-go/tools/leaderelection/resourcelock"
)
func main() {
kubeconfig := flag.String("kubeconfig", "", "kubeconfig 路径")
flag.Parse()
var config *rest.Config
var err error
if *kubeconfig != "" {
config, err = clientcmd.BuildConfigFromFlags("", *kubeconfig)
} else {
config, err = rest.InClusterConfig()
}
if err != nil {
panic(err)
}
client := clientset.NewForConfigOrDie(config)
// 用 Pod 名称和 UID 组成唯一身份标识
id, _ := os.Hostname()
lock := &resourcelock.LeaseLock{
LeaseMeta: metav1.ObjectMeta{
Name: "my-operator-leader",
Namespace: "default",
},
Client: client.CoordinationV1(),
LockConfig: resourcelock.ResourceLockConfig{
Identity: id,
},
}
leaderelection.RunOrDie(context.Background(), leaderelection.LeaderElectionConfig{
Lock: lock,
ReleaseOnCancel: true, // 程序退出时主动释放锁,加快接管
LeaseDuration: 15 * time.Second,
RenewDeadline: 10 * time.Second,
RetryPeriod: 2 * time.Second,
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: func(ctx context.Context) {
// 成为 Leader,启动控制器主循环
startController(ctx)
},
OnStoppedLeading: func() {
// 失去领导权,停止工作并准备退出
os.Exit(0)
},
OnNewLeader: func(identity string) {
// 观察到新 Leader,副本处于待命状态
},
},
})
}
func startController(ctx context.Context) {
// 这里放置真正的业务调谐逻辑
<-ctx.Done()
}注意一个细节:上面的代码在 OnStoppedLeading 回调中直接调用了 os.Exit(0)。这是一种常见做法,因为一旦副本失去领导权,说明它的状态可能已经不可信,最安全的方式是让 Deployment 重新拉起一个全新副本参与竞争,而不是试图原地降级为待命状态。
三、关键参数调优与生产环境注意事项
三个时间参数直接决定了故障切换的速度和系统稳定性。LeaseDuration 是租约有效期,RenewDeadline 是 Leader 续约的超时上限,RetryPeriod 是重试间隔。官方经验法则是 RenewDeadline 应大于 RetryPeriod 的数倍,且小于 LeaseDuration。假设把 LeaseDuration 设成 15 秒,那么 Leader 宕机后最长需要等 15 秒才有新 Leader 接管;如果对切换速度有更高要求,可以把整组参数等比缩小,但要注意过短的租约在 API Server 抖动或负载高时容易造成锁频繁易主,控制器反复启停,这种“主备抖动”比慢速切换危害更大。
网络分区是需要重点防范的场景。如果 Leader 与 API Server 之间出现网络分区,但 Leader 本身还在正常运行业务逻辑,它会继续操作下游资源,而另一侧的副本可能已经选出了新 Leader,这就形成了短暂的“双主”窗口。缓解办法是让 Leader 的每一次业务操作都依赖与 API Server 的实时连接,一旦续约失败就立即停止工作;或者在业务侧引入额外的幂等保护,确保即使短暂双主也不产生副作用。
最后还有几条实践建议。第一,ReleaseOnCancel 建议开启,配合优雅停机可以显著缩短正常发布时的切换时间。第二,锁对象的 Namespace 要和控制器部署在同一个命名空间,并确保 ServiceAccount 拥有 coordination.k8s.io 组下 leases 资源的 get、create、update 权限。第三,同一组副本必须使用完全相同的锁名称,不同控制器之间绝不能共用一个 Lease,否则会出现两个互不相干的组件互相抢锁的诡异现象。第四,排查选主问题时可以直接 kubectl describe lease 查看当前 holderIdentity 与续约时间,这是最直观的诊断手段。
KubernetesLeader Election分布式锁修改时间:2026-09-15 07:04:33