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 的压力。
// 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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。