美文网首页
Kubernetes高可用部署下组件选主的机制分析

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

作者: Andy_Yao | 来源:发表于2021-08-27 15:29 被阅读0次

    选举的原因

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

    相关文章

      网友评论

          本文标题:Kubernetes高可用部署下组件选主的机制分析

          本文链接:https://www.haomeiwen.com/subject/ewymiltx.html