[ k8s-operator 系列 ] 基于 k8s 实现 Leader 选举

无风

Leader election 实现方式:

  • 投票和广播:Raft、Paxos 算法。
  • 分布式锁:强制锁、建议锁。

本文的侧重点是基于 k8s 实现 leader 选举在 operator 中的应用,所主要介绍基于分布式锁实现 Leader 选举。

分布式锁的实现依赖分布式一致性数据中心, k8s 的数据一致性依赖于底层的 etcd,etcd 使用的是 Raft 算法,Raft 的 Leader 选举就是使用投票和广播实现,但 Raft 比较复杂,不是本文的重点,有兴趣可以自行查阅资料。

强制锁 和 建议锁 的术语借鉴于 Linux 文件锁,k8s 上这两种实现与其非常相似。

强制锁

源码:https://github.com/operator-f...

实现原理:

  • Lock:利用 k8s 资源的唯一性,成功创建资源既抢到锁。例如,创建一个 configMap。
  • UnLock:利用 k8s 的 GC,将 configMap 的 ownerReference 设为当前 pod,pod 销毁则 configMap 被 GC 删除,锁释放。

优点:

  • 实现简单。
  • 不会出现多 Leader。

缺点:

  • ownerReference 不可跨 namespace。
  • 依赖 k8s GC,如果 GC 故障或延迟,则会导致服务一段时间不可用。
  • pod 重启(pod 没有被删除,只是容器重启)也不会释放锁。
  • 抢占式,会导致惊群效应。
  • 没有锁超时机制。

使用

err := leader.Become(context.TODO(), "demo-operator-lock")
if err != nil {
  log.Error(err, "")
  os.Exit(1)
}

新版 operator-sdk(Ver >= v1.0.0)中已弃用基于 GC 的 LeaderElection。

建议锁

源码:https://github.com/kubernetes...

实现原理:

  • configMap / Endpoint / Lease 作为 Lock 资源,用于记录 Leader 信息。
  • Lock:

    • Lock 资源不存在时创建成功者成为 Leader。
    • Lock 资源记录的 Leader (HolderIdentity)为空 或者 Lease (LeaseDurationSeconds)过期,更新成功者成为新 Leader。
  • UnLock:调用 release 方法设置 HolderIdentity = ""; LeaseDurationSeconds = 1

优点:

  • 租约机制。
  • 可靠,被 k8s 自身的核心组件 controller-manager 和 scheduler 使用。
  • 可容忍一定程度的时间偏斜。

缺点:

  • 失去 Leader 权之后,容器重启。
  • Lock 资源只创建,无人回收。

实现细节

容忍时间偏斜

包头的注释中,作者阐明选举机制可以容忍一定程度的时间偏斜(时间不一致),但不能容忍时间速率偏斜(时间快慢不一致):

// A client only acts on timestamps captured locally to infer the state of the
// leader election. The client does not consider timestamps in the leader
// election record to be accurate because these timestamps may not have been
// produced by a local clock. The implemention does not depend on their
// accuracy and only uses their change to indicate that another client has
// renewed the leader lease. Thus the implementation is tolerant to arbitrary
// clock skew, but is not tolerant to arbitrary clock skew rate.

tryAcquireOrRenew 方法中判断租约过期:

le.observedTime.Add(le.config.LeaseDuration).After(now.Time)

使用的是本地时间,Lease 中的 RenewTimeAcquireTime 仅作为时间记录,比较的是 observedTimenow.Time

etcd 乐观锁

场景:A release 之后,B 和 C 同时抢 Leader,同时尝试将自己更新为 Leader。etcd 的乐观锁机制会保证后到达的更新会失败,因为 resourceVersion 已经改变,需要重新 Get 再 Update。

特殊场景

场景:Leader A crashed 了,但是 Lease 还未过期。必须等 Lease 过期后 B 才能成为 Leader。

场景:Leader A release 途中,Lease 已过期。B 更新 Lock 成为 Leader,A 和 B 在一小段时间内同时为 Leader。A release 更新会报错,但仍能正常退出。

// release attempts to release the leader lease if we have acquired it.
func (le *LeaderElector) release() bool {
    if !le.IsLeader() {
        return true
    }
    now := metav1.Now()
    leaderElectionRecord := rl.LeaderElectionRecord{
        LeaderTransitions:    le.observedRecord.LeaderTransitions,
        LeaseDurationSeconds: 1,
        RenewTime:            now,
        AcquireTime:          now,
    }
    if err := le.config.Lock.Update(context.TODO(), leaderElectionRecord); err != nil {
        klog.Errorf("Failed to release lock: %v", err)
        return false
    }
    le.observedRecord = leaderElectionRecord
    le.observedTime = le.clock.Now()
    return true
}

场景:Follower B 所在节点发生网络故障,Get Lock 资源失败,内存中 observed 数据老旧。恢复后,与本地 observedRawRecord 对比发现不一致,以远端数据为准,同时更新 observedTime,B 不会误判租约已过期。

if !bytes.Equal(le.observedRawRecord, oldLeaderElectionRawRecord) {
  le.observedRecord = *oldLeaderElectionRecord
  le.observedRawRecord = oldLeaderElectionRawRecord
  le.observedTime = le.clock.Now()
}

可以看出 client-go 的 LeaderElection 逻辑是非常健壮的。

Lease Resource

LeaderElection 依赖的 client 是直连 apiserver 的,configMap / endpoint 等资源被大量组件 watch,建议使用 Leader 选举专用的 Lease 资源,可减少对 apiserver 的压力。

Migrate all uses of leader-election to use Lease API · Issue #80289 · kubernetes/kubernetes (github.com)

// we use the Lease lock type since edits to Leases are less common
// and fewer objects in the cluster watch "all Leases".
lock := &resourcelock.LeaseLock{
  LeaseMeta: metav1.ObjectMeta{
    Name:      leaseLockName,
    Namespace: leaseLockNamespace,
  },
  Client: client.CoordinationV1(),
  LockConfig: resourcelock.ResourceLockConfig{
    Identity: id,
  },
}

使用

client-go

参考:https://github.com/kubernetes...

operator-sdk

operator-sdk 中使用时 controller-runtime 做了封装,传入 LeaderElection 和 LeaderElectionID 即可开启领导选举。

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
  Scheme:                 scheme,
  MetricsBindAddress:     metricsAddr,
  Port:                   9443,
  CertDir:                certDir,
  HealthProbeBindAddress: probeAddr,
  LeaderElection:         enableLeaderElection,
  LeaderElectionID:       "a73bd0c8.gogo.io",
})

controllerManager 中分为 leaderElectionRunnablesnonLeaderElectionRunnables。reconciler 属于 leaderElection,只有 Leader 会执行。 webhook 属于 nonLeaderElection,即不需要 LeaderElection,不用担心多副本情况下 webhook 单点问题。

    // leaderElectionRunnables is the set of Controllers that the controllerManager injects deps into and Starts.
    // These Runnables are managed by lead election.
    leaderElectionRunnables []Runnable
    // nonLeaderElectionRunnables is the set of webhook servers that the controllerManager injects deps into and Starts.
    // These Runnables will not be blocked by lead election.
    nonLeaderElectionRunnables []Runnable

Reference

https://kayn.wang/leader/

https://zdyxry.github.io/2019...

阅读 350

无风的内存空间
编程相关,随手记下
621 声望
50 粉丝
0 条评论
你知道吗?

621 声望
50 粉丝
宣传栏