选举的目的
Kubernetes作为分布式集群,本身运行了许多组件,尤其是Master层面的组件,每个都肩负着重要的功能,其中某个组件的故障,必然会对整个集群造成致命的影响,为了保障高可用,每个组件都需要多节点部署;但多节点会衍生出数据一致性的问题,所以同一时刻,多节点的组件必须有且只能有一个提供正常服务,这就是Kubernetes选举的目的(选出leader节点并保证同一时间只有一个leader)
选举的实现
Kubernetes选举机制采用分布式锁的方式,实现分布式锁的方式需要依赖存储服务,如Redis、zookeeper、Etcd等,由于Kubernetes依赖于etcd,所以Kubernetes的选举实现方式是通过Etcd实现分布式锁,具体原理如下:
分布式锁的种类
基于Etcd的key而创建的分布式锁也称为资源锁,Kubernetes的资源锁主要有三种:
- EndpointsResourceLock :依赖于Endpoints资源,默认资源锁为该类型
-
ConfigMapsResourceLock :依赖于Configmaps资源
-
LeasesResourceLock :依赖于Leases资源
Leader选举过程
func (le *LeaderElector) Run(ctx context.Context) {
defer func(){
runtime.HandleCrash()
le.config.Callbacks.OnStoppedLeading()
}()
if !le.acquire(ctx) {
return
}
...
go le.config.Callbacks.OnStartedLeading(ctx)
le.renew(ctx)
}
资源锁获取过程
func (le *LeaderElector) acquire(ctx context.Context) bool {
...
wait.JitterUntil(func() {
succeeded = le.tryAcquireOrRenew()
le.maybeReportTransition()
if !succeeded {
return
}
...
cancel()
}, le.config.RetryPeriod, JitterFactor, true, ctx.Done())
return succeeded
}
获取资源锁的过程通过wait.JitterUntil定时器定时执行,它接收一个func匿名函数和一个stopCh Chan,内部会定时调用匿名函数,只有当stopCh关闭时,该定时器才会停止并退出。
执行le.tryAcquireOrRenew函数来获取资源锁。如果其获取资源锁失败,会通过return等待下一次定时获取资源锁。如果其获取资源锁成功,则说明当前节点可以成为领导者节点,退出acquire函数并返回true。le.tryAcquireOrRenew代码示例如下。
首先,通过le.config.Lock.Get函数获取资源锁,当资源锁不存在时,当前节点创建该key(获取锁)并写入自身节点的信息,创建成功则当前节点成为领导者节点并返回true。
oldLeaderElectionRecord, err := le.config.Lock.Get()
if err !=nil {
if !errors.IsNotFound(err) {
return false
}
if err = le.config.Lock.Create(leaderElectionRecord); err != nil {
return false
}
le.observedRecord = leaderElectionRecord
le.observedTime = le.clock.Now()
return true
}
当资源锁存在时,更新本地缓存的租约信息
if !reflect.DeepEqual(le.observedRecord, *oIdLeaderElectionRecord) {
le.observedRecord = *oldLeaderElectionRecord
le.observedTime = le.clock.Now()
}
候选节点会验证领导者节点的租约是否到期,如果尚未到期,暂时还不能抢占并返回false
if len(oldLeaderElectionRecord.HolderIdentity) > 0 &&
le.observedTime.Add (le.config.LeaseDuration).After(now.Time) &&
!le.IsLeader() {
...
return false
}
如果是领导者节点,那么AcquireTime(资源锁获得时间)和LeaderTransitions (领导者进行切换的次数)字段保持不变。如果是候选节点,则说明领导者节点的租约到期,给LeaderTransitions字段加1并抢占资源锁
if le.IsLeader() {
leaderElectionRecord.AcquireTime =
oldLeaderElectionRecord.AcquireTime
leaderElectionRecord. LeaderTransitions = oldLeaderElectionRecord.LeaderTransitions
} else {
LeaderElectionRecord.LeaderTransitions = oldLeaderElectionRecord.LeaderTransitions + 1
}
通过le.config.Lock.Update函数尝试去更新租约记录,若更新成功,函数返回true
if err = le.config.Lock.Update (leaderElectionRecord); err != nil {
klog.Errorf("Failed to update lock: %v", err)
return false
}
...
return true
在领导者节点获取资源锁以后,会定时(默认值为2秒)循环更新租约信息,以保持长久的领导者身份。若因网络超时而导致租约信息更新失败,则说明被候选节点抢占了领导者身份,当前节点会退出进程。代码示例如下:
func (le *LeaderElector) renew(ctx context.Context) {
...
wait.Until(func()) {
...
err := wait.PollImediate0ntil(le.config.RetryPeriod, func()
(bool,error) {
done := make(chan bool,1)W
go func() {
defer close(done)
done <- le. tryAcquireOrRenew()
} ()
...
}, timeoutCtx.Done())
...
if err == nil {
klog.V(5).Infof("successfully renewed lease tv", desc)
return
}
...
cancel()
}, le.config.RetryPeriod, ctx.Done())
if le.config.Release0nCancel {
le.release()
}
领导者节点续约的过程通过wait.PollImmediateUntil定时器定时执行,它接收一个func匿名函数(条件函数)和一个stopCh,内部会定时调用条件函数,当条件函数返回true或stopCh关闭时,该定时器才会停止并退出。执行le.tryAcquireOrRenew函数来实现领导者节点的续约,其原
理与资源锁获取过程相同。le.tryAcquireOrRenew函数返回true说明续 约 成 功 , 并 进 入 下 一 个 定 时 续 约 ; 返 回 false 则 退 出 并 执 行le.release函数且释放资源锁。