Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Leader 选举机制:高可用控制器的必备技能
Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Leader 选举机制:高可用控制器的必备技能
当我们部署一个高可用的 Kubernetes Controller 时,通常会部署多个副本。
但问题是:如果每个副本都独立运行,都会执行 Reconcile 逻辑,会不会导致重复操作、数据不一致?Leader 选举机制就是为了解决这个问题:同一时间只有一个副本在真正工作,其他副本处于待机状态。当主副本挂掉后,其他副本会自动选举出新的 Leader,继续工作。
Kubernetes Leader Election 高可用 Lease v1.36.1 🔓 学习重点提示 — 建议先通读全文,再重点回顾标注内容
★ 重点掌握(必须)
• LeaderElectionConfig 核心字段:LeaseDuration、RenewDeadline、RetryPeriod 的含义与调优
• Leader 选举流程:获取锁 → 更新锁 → 续约 → 释放锁
• Callbacks 回调:OnStartedLeading、OnStoppedLeading、OnNewLeader 的使用场景
☆ 次重点(了解即可)
• 多锁机制(MultiLock)
一、为什么需要 Leader 选举?
想象一个场景:我们部署了 3 个 Controller 副本,每个都监听同一个 Deployment。当用户修改了 Deployment 的副本数时:
- 如果没有 Leader 选举:3 个副本都收到事件,都执行 Reconcile,都去更新 ReplicaSet。造成竞态条件、资源冲突、甚至多次更新导致状态不一致。
- 如果有 Leader 选举:只有 Leader 副本处理事件,其他副本处于待机状态。主副本挂掉后,选举出新 Leader 继续工作。
Leader 选举就像公司的 CEO 职位:同一时间只有一个 CEO,当 CEO 辞职或被解雇后,董事会选举新的 CEO。Kubernetes 内置组件(kube-controller-manager、kube-scheduler)都使用了 Leader 选举。
二、LeaseLock:Leader 选举的实现原理
Kubernetes 的 Leader 选举依赖 Lease(续约锁)对象。Lease 是 Kubernetes 1.14 之后引入的新机制,比之前的 Endpoints/ConfigMaps 锁更轻量。
在 Kubernetes 源码中,resourcelock.Interface 定义了锁的抽象:
// staging/src/k8s.io/client-go/tools/leaderelection/resourcelock/interface.go
// Interface 定义了 Leader 选举锁的抽象
type Interface interface {
// Get 获取当前的 Leader 选举记录
Get(ctx context.Context) (*LeaderElectionRecord, []byte, error)
// Create 创建 Leader 选举记录
Create(ctx context.Context, ler LeaderElectionRecord) error
// Update 更新 Leader 选举记录(核心!续约操作)
Update(ctx context.Context, ler LeaderElectionRecord) error
// RecordEvent 记录事件到资源上
RecordEvent(string)
// Identity 返回当前锁持有者的唯一标识
Identity() string
// Describe 返回锁的描述信息
Describe() string
}
LeaderElectionRecord 存储在 Lease 对象的 Annotation 中:
// LeaderElectionRecord 结构体
// LeaderElectionRecord 存储 Leader 选举的状态
type LeaderElectionRecord struct {
// HolderIdentity:当前 Leader 的标识(通常是 Pod 名称)
HolderIdentity string `json:"holderIdentity"`
// LeaseDurationSeconds:租约时长(秒)
LeaseDurationSeconds int `json:"leaseDurationSeconds"`
// AcquireTime:获取 Leader 位置的时间
AcquireTime metav1.Time `json:"acquireTime"`
// RenewTime:最后一次续约的时间
RenewTime metav1.Time `json:"renewTime"`
// LeaderTransitions:Leader 切换次数(用于监控)
LeaderTransitions int `json:"leaderTransitions"`
// Strategy:协调策略
Strategy v1.CoordinatedLeaseStrategy `json:"strategy"`
// PreferredHolder:优先持有者(用于有状态选举)
PreferredHolder string `json:"preferredHolder"`
}
三、LeaderElectionConfig:配置详解
NewLeaderElector 函数负责创建 LeaderElector,它需要传入一个 LeaderElectionConfig 配置:
// staging/src/k8s.io/client-go/tools/leaderelection/leaderelection.go(行 116-166)
type LeaderElectionConfig struct {
// Lock:锁对象,用于存储 Leader 状态
Lock rl.Interface
// LeaseDuration:租约有效期
// 非 Leader 节点在 LeaseDuration 时间内没有观察到 Leader 更新,
// 就可以认为 Leader 丢失,从而尝试获取 Leader
LeaseDuration time.Duration
// RenewDeadline:续约间隔
// Leader 每隔 RenewDeadline 时间就要续约一次
// 如果续约失败(超过 RenewDeadline 没有续约),其他节点会认为 Leader 丢失
RenewDeadline time.Duration
// RetryPeriod:重试间隔
// 节点尝试获取锁或观察 Leader 状态的重试间隔
RetryPeriod time.Duration
// Callbacks:选举状态变化的回调函数
Callbacks LeaderCallbacks
// WatchDog:健康检查器(可选)
WatchDog *HealthzAdaptor
// ReleaseOnCancel:收到取消信号时是否主动释放锁
ReleaseOnCancel bool
// Name:锁的名称(用于调试和监控)
Name string
// Coordinated:是否使用协调 Leader 选举(Alpha 特性)
Coordinated bool
}
三个时间参数的关系非常重要:
LeaseDuration(租约时长)
│────────────────────────────────────────────────│
▲ ▲
│ │
RenewDeadline LeaseExpires
(续约间隔) (过期时间)
│ │
└──────────┬─────────┘
│
Leader 续约(每 RENEW_DEADLINE 一次)
│
│ 正常情况下 Leader 一直续约
▼
如果 Leader 挂了,没有续约
等待 LEASE_DURATION 后,其他节点认为 Leader 丢失
开始选举新的 Leader
时间参数的经验公式:
- LeaseDuration > RenewDeadline:必须满足,否则代码会报错
- RenewDeadline > RetryPeriod × 1.2:必须满足,确保重试有足够时间
- 建议比例:LeaseDuration : RenewDeadline : RetryPeriod = 15s : 10s : 2s
四、LeaderCallbacks:回调函数
LeaderCallbacks 定义了 Leader 选举状态变化时的回调函数:
// LeaderCallbacks 定义
type LeaderCallbacks struct {
// OnStartedLeading:当节点成为 Leader 时调用
// ctx 是可取消的 context,当不再是 Leader 时会被取消
OnStartedLeading func(context.Context)
// OnStoppedLeading:当节点失去 Leader 地位时调用
// 注意:这个回调不仅在主动让出 Leader 时触发,
// 在 Leader 意外丢失时也会触发
OnStoppedLeading func()
// OnNewLeader:当观察到新的 Leader 时调用
// 包括第一次观察到 Leader
OnNewLeader func(identity string)
}
典型的使用场景:
// 创建 LeaderElector 并传入回调
elector, err := leaderelection.NewLeaderElector(leaderelection.LeaderElectionConfig{
Lock: lock,
LeaseDuration: 15 * time.Second,
RenewDeadline: 10 * time.Second,
RetryPeriod: 2 * time.Second,
Callbacks: leaderelection.LeaderCallbacks{
// 成为 Leader 后,启动 Controller 主循环
OnStartedLeading: func(ctx context.Context) {
klog.Info("Became leader, starting controller...")
// 启动 Controller
controller.Run(ctx, 2)
},
// 失去 Leader 后,停止 Controller
OnStoppedLeading: func() {
klog.Info("Lost leadership, stopping controller...")
// 清理资源
controller.Shutdown()
},
// 观察到新 Leader 时打印日志
OnNewLeader: func(identity string) {
if identity == lock.Identity() {
klog.Info("Still the leader")
} else {
klog.Infof("New leader elected: %s", identity)
}
},
},
})
五、完整使用示例
下面是一个完整的 Leader 选举使用示例:
// 完整的 Leader 选举使用示例
package main
import (
"context"
"fmt"
"time"
v1 "k8s.io/api/core/v1"
coordinationv1 "k8s.io/api/coordination/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/leaderelection"
"k8s.io/client-go/tools/leaderelection/resourcelock"
"k8s.io/client-go/tools/clientcmd"
)
func main() {
// 第一步:加载 kubeconfig
config, err := clientcmd.BuildConfigFromFlags("", "")
if err != nil {
panic(err)
}
// 第二步:创建 Kubernetes clientset
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err)
}
// 第三步:创建 Leader 选举锁
// 使用 Lease 锁(Kubernetes 1.14+ 推荐)
lock, err := resourcelock.New(
resourcelock.LeasesResourceLock, // 锁类型
"default", // namespace
"example-operator-leader", // lock 名称
clientset.CoordinationV1(), // coordination client
clientset.CoreV1(), // core client(用于记录 Events)
resourcelock.ResourceLockConfig{
Identity: fmt.Sprintf("pod-%s", getPodName()), // 唯一标识(通常是 Pod 名)
},
)
if err != nil {
panic(err)
}
// 第四步:创建 Controller 实例(伪代码)
controller := NewController(clientset)
// 第五步:创建 LeaderElector
leaderElector, err := leaderelection.NewLeaderElector(leaderelection.LeaderElectionConfig{
Lock: lock,
LeaseDuration: 15 * time.Second, // 租约时长
RenewDeadline: 10 * time.Second, // 续约间隔
RetryPeriod: 2 * time.Second, // 重试间隔
ReleaseOnCancel: true, // 取消时释放锁
Name: "example-operator",
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: func(ctx context.Context) {
// 成为 Leader 后启动 Controller
klog.Info("Starting controller as leader")
controller.Run(ctx, 2)
},
OnStoppedLeading: func() {
// 失去 Leader 后停止 Controller
klog.Info("Stopping controller, lost leadership")
controller.Shutdown()
},
OnNewLeader: func(identity string) {
// 观察到新 Leader
klog.Infof("Leader changed to: %s", identity)
},
},
})
if err != nil {
panic(err)
}
// 第六步:启动 Leader 选举
// Run 函数会一直运行,直到 context 取消
context, cancel := context.WithCancel(context.Background())
defer cancel()
leaderElector.Run(context)
}
// getPodName 获取当前 Pod 名称
func getPodName() string {
// 实际环境中从 downward API 获取
return "example-operator-abc123"
}
六、Leader 选举流程图
Leader 选举的完整流程:
┌──────────────────────────────────────────────────────────────────────────┐ │ Leader 选举流程 │ ├──────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ │ │ │ 非 Leader │ │ │ └──────┬──────┘ │ │ │ │ │ ▼ 检查 Lock │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Lease 对象中存储的 HolderIdentity 是什么? │ │ │ └──────────────────────────┬───────────────────────────────────────┘ │ │ │ │ │ ┌────────────────────┼────────────────────┐ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ HolderIdentity │ │ HolderIdentity │ │ HolderIdentity │ │ │ │ = 本节点 │ │ = 其他节点 │ │ = 空(无Leader)│ │ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 待机观察 │ │ 尝试获取锁 │ │ 创建锁记录 │ │ │ │ 每RetryPeriod│ │ (Update操作)│ │ (Create操作)│ │ │ │ 检查一次 │ └──────┬──────┘ └──────┬──────┘ │ │ └─────────────┘ │ │ │ │ │ 成功? │ 成功? │ │ ▼ ▼ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ 成为 Leader │ │ 成为 Leader │ │ │ │ 启动 Controller│ │ 启动 Controller│ │ │ └─────────────┘ └─────────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────────────────────────────────┐ │ │ │ Leader 主循环: │ │ │ │ 每 RenewDeadline 续约一次 │ │ │ │ 更新 Lease 的 RenewTime │ │ │ └─────────────────────────────────────┘ │ │ │ │ │ │ │ 续约失败? │ 节点被删除? │ │ ▼ ▼ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ 失去 Leader │ │ 失去 Leader │ │ │ │ 停止 Controller│ │ 停止 Controller│ │ │ └──────┬──────┘ └──────┬──────┘ │ │ │ │ │ │ └────────────────────┴─────→ 回到起点 │ │ 检查 Lock │ └──────────────────────────────────────────────────────────────────────────┘
七、生产环境配置建议
在生产环境中配置 Leader 选举时,需要考虑以下因素:
高流量 APIServer 场景
如果 APIServer 负载较高,可以增加 RetryPeriod 来减少竞争:
// 保守配置:减少 APIServer 压力
leaderelection.LeaderElectionConfig{
LeaseDuration: 30 * time.Second, // 较长的租约
RenewDeadline: 20 * time.Second, // 较长的续约间隔
RetryPeriod: 5 * time.Second, // 较长的重试间隔
// ...
}
快速故障转移场景
如果需要快速故障转移,可以缩短时间参数:
// 快速切换配置:减少故障恢复时间
leaderelection.LeaderElectionConfig{
LeaseDuration: 10 * time.Second, // 较短的租约
RenewDeadline: 6 * time.Second, // 较短的续约间隔
RetryPeriod: 1 * time.Second, // 较短的重试间隔
// ...
}
Identity 的最佳实践
Identity 应该唯一标识每个候选者,通常使用 Pod 名称:
# Pod 中配置 downward API 获取 Pod 名称作为 Identity
spec:
containers:
- name: operator
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
# 或使用 downward API
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
⚠️ 警告
Identity 必须全局唯一。如果两个节点使用相同的 Identity,它们会相互覆盖对方的 Leader 记录,导致频繁的 Leader 切换。
八、监控与排查
Leader 选举可以通过以下方式监控:
# 查看 Lease 对象状态
kubectl get lease -n default example-operator-leader -o yaml
# 查看 Leader 切换次数
kubectl get lease -n default example-operator-leader -o jsonpath='{.metadata.annotations.control-plane\.alpha\.kubernetes\.io/leader}'
# 如果有 LeaderElectionRecordAnnotationKey annotation,查看详细内容
kubectl get lease -n default example-operator-leader -o jsonpath='{.metadata.annotations}' | jq
常见的 Leader 选举问题:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Leader 频繁切换 | RenewDeadline 太短,APIServer 响应慢 | 增加 RenewDeadline 和 RetryPeriod |
| 没有 Leader 被选出 | 所有候选者都无法创建 Lease | 检查 RBAC 权限和 Lease 创建权限 |
| LeaderTransitions 异常高 | 网络不稳定或节点不稳定 | 检查网络和节点健康状态 |
九、总结
这一节我们深入理解了 Leader 选举机制:
- 为什么需要 Leader 选举:确保同一时间只有一个 Controller 实例在处理事件,避免竞态条件
- LeaseLock 实现:基于 Kubernetes Lease 对象存储 Leader 状态,比旧版 Endpoints/ConfigMaps 更轻量
- 三个时间参数:LeaseDuration > RenewDeadline > RetryPeriod × 1.2
- Callbacks 的使用:OnStartedLeading 启动 Controller,OnStoppedLeading 停止 Controller
- 生产配置建议:根据 APIServer 负载和故障恢复需求调整时间参数
下一节我们将学习 错误处理与重试机制,了解 WorkQueue 的限速器如何帮助 Controller 处理失败的任务。敬请期待!
Kubernetes 编程 / Operator 专题【左扬精讲】—— Leader 选举机制 · 来源:Kubernetes v1.36.1 client-go 源码分析

浙公网安备 33010602011771号