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...