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 源码分析

posted @ 2026-06-13 18:16  左扬  阅读(2)  评论(0)    收藏  举报