选举的原因
在k8s的组件中,其中有kube-scheduler和kube-manager-controller两个组件是有leader选举的,这个选举机制是k8s对于这两个组件的高可用保障。即正常情况下kube-scheduler或kube-manager-controller组件的多个副本只有一个是处于业务逻辑运行状态,其它副本则不断的尝试去获取锁,去竞争leader,直到自己成为leader。如果正在运行的leader因某种原因导致当前进程退出,或者锁丢失,则由其它副本去竞争新的leader,获取leader继而执行业务逻辑。
选举的配置
- leader-elect-resource-namespace:选举过程中用于锁定的资源所在的namespace名称,默认为“kube-system”
- leader-elect-resource-name:选举过程中用于锁定的资源对象名称。
- leader-elect:true为开启选举
- leader-elect-lease-duration:资源锁租约观察时间,如果其它竞争者在该时间间隔过后发现leader没更新获取锁时间,则其它副本可以认为leader已经挂掉不参与工作了,将重新选举leader。
- leader-elect-renew-deadline:选举过程中在停止leading角色之前再次renew的时间间隔,既在该时间内没有更新则失去leader身份。
- leader-elect-retry-period:选举过程中获取leader角色和renew之间的时间间隔,既为其它副本获取锁的时间间隔(竞争leader)和leader更新间隔;默认是2s。
- leader-elect-resource-lock:选根据过程中使用哪种资源对象进行锁定操作。
选举的逻辑
所有节点上的组件请求各自apiserver,apiserver从etcd中抢占锁资源,抢到锁的节点组件会将自己标记成为锁的持有者。 leader 则可以通过更新RenewTime来确保持续保有该锁。同时其它节点上的组件也会请求各节点上的apiserver,来查询加锁对象的更新时间来判断自己是否成为新的leader。当leader在配置的时间内未能成功更新锁资源的时间,立即会失去leader身份。
选主核心逻辑
选主核心逻辑:tryAcquireOrRenew
tryAcquireOrRenew 函数尝试获取租约,如果获取不到或者得到的租约已过期则尝试抢占,否则 leader 不变。函数返回 True 说明本 goroutine 已成功抢占到锁,获得租约合同,成为 leader。
func (le *LeaderElector) tryAcquireOrRenew() bool {
// 创建 leader election 租约
now := metav1.Now()
leaderElectionRecord := rl.LeaderElectionRecord{
HolderIdentity: le.config.Lock.Identity(),
LeaseDurationSeconds: int(le.config.LeaseDuration / time.Second),
RenewTime: now,
AcquireTime: now,
}
// 1\. 从 endpointslock 上获取 leader election 租约,也就是上边 endpoint 的 get 方法的实现
oldLeaderElectionRecord, err := le.config.Lock.Get()
if err != nil {
if !errors.IsNotFound(err) {
klog.Errorf("error retrieving resource lock %v: %v", le.config.Lock.Describe(), err)
return false
}
// 租约存在:于是将函数一开始创建的 leader election 租约放入同名 endpoint 的 annotation 中
if err = le.config.Lock.Create(leaderElectionRecord); err != nil {
klog.Errorf("error initially creating leader election record: %v", err)
return false
}
// 创建成功,成为 leader,函数返回 true
le.observedRecord = leaderElectionRecord
le.observedTime = le.clock.Now()
return true
}
// 2\. 更新本地缓存的租约,并更新观察时间戳,用来判断租约是否到期
if !reflect.DeepEqual(le.observedRecord, *oldLeaderElectionRecord) {
le.observedRecord = *oldLeaderElectionRecord
le.observedTime = le.clock.Now()
}
// leader 的租约尚未到期,自己暂时不能抢占它,函数返回 false
if len(oldLeaderElectionRecord.HolderIdentity) > 0 &&
le.observedTime.Add(le.config.LeaseDuration).After(now.Time) &&
!le.IsLeader() {
klog.V(4).Infof("lock is held by %v and has not yet expired", oldLeaderElectionRecord.HolderIdentity)
return false
}
// 3\. 租约到期,而 leader 身份不变,因此获得租约的时间戳 AcquireTime 保持不变
if le.IsLeader() {
leaderElectionRecord.AcquireTime = oldLeaderElectionRecord.AcquireTime
leaderElectionRecord.LeaderTransitions = oldLeaderElectionRecord.LeaderTransitions
} else {
// 租约到期,leader 易主,transtions+1 说明 leader 更替了
leaderElectionRecord.LeaderTransitions = oldLeaderElectionRecord.LeaderTransitions + 1
}
// 尝试去更新租约记录
if err = le.config.Lock.Update(leaderElectionRecord); err != nil {
// 更新失败,函数返回 false
klog.Errorf("Failed to update lock: %v", err)
return false
}
// 更新成功,函数返回 true
le.observedRecord = leaderElectionRecord
le.observedTime = le.clock.Now()
return true
}
发起选主
以scheduler为例, 在启动时就会发起选主,代码位于:cmd/kube-scheduler/app/server.go
// If leader election is enabled, runCommand via LeaderElector until done and exit.
if cc.LeaderElection != nil {
cc.LeaderElection.Callbacks = leaderelection.LeaderCallbacks{
OnStartedLeading: run,
OnStoppedLeading: func() {
klog.Fatalf("leaderelection lost")
},
}
leaderElector, err := leaderelection.NewLeaderElector(*cc.LeaderElection)
if err != nil {
return fmt.Errorf("couldn't create leader elector: %v", err)
}
leaderElector.Run(ctx)
return fmt.Errorf("lost lease")
}
// Leader election is disabled, so runCommand inline until done.
run(ctx)
return fmt.Errorf("finished without leader elect")
更新锁
renew方法,只有在获取锁之后才会调用,它会通过持续更新资源锁的数据,来确保继续持有已获得的锁,保持自己的leader 状态。
// renew loops calling tryAcquireOrRenew and returns immediately when tryAcquireOrRenew fails or ctx signals done.
func (le *LeaderElector) renew(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
wait.Until(func() {
timeoutCtx, timeoutCancel := context.WithTimeout(ctx, le.config.RenewDeadline)
defer timeoutCancel()
//
err := wait.PollImmediateUntil(le.config.RetryPeriod, func() (bool, error) {
done := make(chan bool, 1)
go func() {
defer close(done)
done <- le.tryAcquireOrRenew()
}()
// 超时返回error, 否则返回更新结果
select {
case <-timeoutCtx.Done():
return false, fmt.Errorf("failed to tryAcquireOrRenew %s", timeoutCtx.Err())
case result := <-done:
return result, nil
}
}, timeoutCtx.Done())
le.maybeReportTransition()
desc := le.config.Lock.Describe()
if err == nil {
klog.V(5).Infof("successfully renewed lease %v", desc)
return
}
le.config.Lock.RecordEvent("stopped leading")
le.metrics.leaderOff(le.config.Name)
klog.Infof("failed to renew lease %v: %v", desc, err)
cancel()
}, le.config.RetryPeriod, ctx.Done())
// if we hold the lease, give it up
if le.config.ReleaseOnCancel {
le.release()
}
}
选主失败的后果
controller manager 和 scheduler 都是通过连接 apiserver 去读写数据,假如 apiserver 出现异常无法访问,将会影响 controller manager 和 scheduler 运行。而apiserver 运行依赖etcd服务,如果etcd不可访问或者不可读写,那么apiserver也无法向 controller manager 和 scheduler 或者其他连接apiserver的应用提供服务。
然而有很多因素会导致 ETCD 服务不可访问或者不可读写,比如:
- 网络断开或者网络闪断;
- 三个 ETCD 节点丢失三个,最后一个节点将变成只读模式;
- 多个 ETCD 实例之间会通过 2380 端口通信来选举 leader,并且彼此保持心跳检测。如果节点负载增加导致 ETCD 心跳检测响应延迟,超过预定的心跳超时时间后会进行 leader 的重新选举,选举时候将会出现 ETCD 服务不可用。
最常见的问题就是在etcd选主时,组件访问apiserver去抢占锁资源,然而此时apiserver无法回应组件,组件会抛出异常并停止服务,而锁资源的现持有者在锁过期时间之后,也会放弃leader角色,接着重新选举,若此时etcd仍未选主,并重复上述问题,选举就会失败,所有组件都会挂掉。因此合理的考虑自身项目的服务器条件,配置合理的时间是很重要的。