Kubernetes高可用部署下组件选主的机制分析

选举的原因

在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仍未选主,并重复上述问题,选举就会失败,所有组件都会挂掉。因此合理的考虑自身项目的服务器条件,配置合理的时间是很重要的。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容