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 中的 RenewTime
和 AcquireTime
仅作为工夫记录,比拟的是 observedTime
和 now.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 中分为 leaderElectionRunnables
和 nonLeaderElectionRunnables
。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…