K8S 中 scheduler 组件的选主逻辑

概述

在 k8s 中,kube-scheduler 和 kube-controller-manager 两个组件是有 leader 选举的,这个选举机制是 k8s 对于这两个组件的高可用保障,虽然 k8s 的存储使用了 etcd,但并没有使用 etcd 来实现选主,而是对 endpoint 这个资源做抢占,谁想抢到并将自己的信息写入 endpoint的 annotation 中,谁就获得了主。因为项目中需要写一个 k8s 的插件,同样需要选主逻辑,因此复用了 k8s 中的这个方法,即kubernetes 的 tools/leaderelection包,顺便理解一下这个原理,本文主要介绍下选主逻辑,基于 etcd client 实现选主可以看另一篇文章

本文以kube-scheduler为例,kube-controller-manager同理,k8s 版本为:1.18

表现

在多副本的kube-scheduler 的日志,可以看到这样一段

Apr 25 16:04:41 instance-o24xykos-1 kube-scheduler[31111]: I0425 16:04:41.743878   31111 round_trippers.go:443] GET https://100.64.230.53:6443/api/v1/namespaces/kube-system/endpoints/kube-scheduler?timeout=10s 200 OK in 4 milliseconds

Apr 25 16:04:41 instance-o24xykos-1 kube-scheduler[31111]: I0425 16:04:41.744158   31111 leaderelection.go:350] lock is held by instance-o24xykos-3_1ad55d32-2abe-49f7-9d68-33ec5eadb906 and has not yet expired

Apr 25 16:04:41 instance-o24xykos-1 kube-scheduler[31111]: I0425 16:04:41.744176   31111 leaderelection.go:246] failed to acquire lease kube-system/kube-scheduler

这是未选到主的日志,每2秒一次(配置–leader-elect-retry-period),获取 kube-system 下的kube-scheduler的 endpoint,发现锁仍然被instance-o24xykos-3持有,抢不到锁,继续重试。

这时的 endpoint 的值为

kubectl get endpoints  kube-scheduler -n kube-system -o yaml
apiVersion: v1
kind: Endpoints
metadata:
  annotations:
    control-plane.alpha.kubernetes.io/leader: '{"holderIdentity":"instance-o24xykos-3_1ad55d32-2abe-49f7-9d68-33ec5eadb906","leaseDurationSeconds":15,"acquireTime":"2020-04-23T06:45:07Z","renewTime":"2020-04-25T07:55:58Z","leaderTransitions":1}'
  creationTimestamp: "2020-04-22T12:05:29Z"
  name: kube-scheduler
  namespace: kube-system
  resourceVersion: "467853"
  selfLink: /api/v1/namespaces/kube-system/endpoints/kube-scheduler
  uid: f3535807-0575-483f-8471-f8d4fd9eeac6

annotation 中的 key 为:control-plane.alpha.kubernetes.io/leader,value 中holderIdentity:instance-o24xykos-3即为当前锁的获得者。

在主instance-o24xykos-3上,可以看到如下日志,每隔 2s renew 一次锁,保证主的地位。

Apr 24 03:26:33 instance-o24xykos-3 kube-scheduler[2497]: I0424 03:26:33.281923    2497 leaderelection.go:282] successfully renewed lease kube-system/kube-scheduler

配置

kube-scheduler 在启动时,与 leader election 相关的启动参数有以下几个:

  • leader-elect: 是否开启选举功能,默认开启
  • leader-elect-lease-duration: 锁的失效时间,类似于 session-timeout
  • leader-elect-renew-deadline: leader 的心跳间隔,必须小于等于 lease-duration
  • leader-elect-retry-period: non-leader 每隔 retry-period 尝试获取锁

  • –leader-elect-resource-lock:用什么对象来存放选主信息,默认为 endpoint,也可以用 configmap

  • –leader-elect-resource-name:endpoint 的名称,kube-scheduler
  • –leader-elect-resource-namespace: endpoint 的命名空间,kube-system

对于 k8s 的选主来说,锁节点指的是 kube-system 命名空间下的同名 endpoint。任一 goroutine 如果能成功在该 ep 的 annotation 中留下自身记号即成为 leader。成为 leader 后会定时续约,leader 可以通过更新 RenewTime 来确保持续保有该锁。non-leader 们会定时去获取该 ep 的 annotation,若发现过期等情况则进行抢占。

目前主节点的标识用的是hostname

原理

k8s 中选主逻辑位于:staging/src/k8s.io/client-go/tools/leaderelection

锁结构

type LeaderElectionRecord struct {
  // leader 标识,通常为 hostname
    HolderIdentity       string      `json:"holderIdentity"`
    // 同启动参数 --leader-elect-lease-duration
    LeaseDurationSeconds int         `json:"leaseDurationSeconds"`
    // Leader 第一次成功获得租约时的时间戳
    AcquireTime          metav1.Time `json:"acquireTime"`
    // leader 定时 renew 的时间戳
    RenewTime            metav1.Time `json:"renewTime"`
    // leader 更换
    LeaderTransitions    int         `json:"leaderTransitions"`
}

Interface 接口

k8s 中的锁需实现 如下Interface 接口, 当前实现有三种,EndpointsResourceLock、ConfigMapsResourceLock、LeaseLock。LeaseLock是一种新的方式,会创建一种专门的含租期字段的Lease对象。

type Interface interface {
    // Get returns the LeaderElectionRecord
    Get() (*LeaderElectionRecord, error)

    // Create attempts to create a LeaderElectionRecord
    Create(ler LeaderElectionRecord) error

    // Update will update and existing LeaderElectionRecord
    Update(ler LeaderElectionRecord) error

    // RecordEvent is used to record events
    RecordEvent(string)

    // Identity will return the locks Identity
    Identity() string

    // Describe is used to convert details on current resource lock
    // into a string
    Describe() string
}

以EndpointsResourceLock的选举过程为例:


// 创建 func (el *EndpointsLock) Create(ler LeaderElectionRecord) error { recordBytes, err := json.Marshal(ler) if err != nil { return err } el.e, err = el.Client.Endpoints(el.EndpointsMeta.Namespace).Create(&v1.Endpoints{ ObjectMeta: metav1.ObjectMeta{ Name: el.EndpointsMeta.Name, Namespace: el.EndpointsMeta.Namespace, Annotations: map[string]string{ LeaderElectionRecordAnnotationKey: string(recordBytes), }, }, }) return err } // 更新 func (el *EndpointsLock) Update(ler LeaderElectionRecord) error { if el.e == nil { return errors.New("endpoint not initialized, call get or create first") } recordBytes, err := json.Marshal(ler) if err != nil { return err } el.e.Annotations[LeaderElectionRecordAnnotationKey] = string(recordBytes) el.e, err = el.Client.Endpoints(el.EndpointsMeta.Namespace).Update(el.e) return err }

选主核心逻辑: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")

update的原子性

在抢锁的过程中,势必会存在同时 update endpoint 的操作,而解决这种竞争,Kubernetes 是通过版本号的乐观锁来实现的。它对比了 resourceVersion,而resourceVersion的取值最终又来源于etcd的modifiedindex,当key对应的val改变时,modifiedindex的值发生改变。

kubernetes 的 update 是原子的、安全的,通过resourceVersion字段判断对象是否已经被修改。当包含 ResourceVersion 的更新请求到达 Apiserver,服务器端将对比请求数据与服务器中数据的资源版本号,如果不一致,则表明在本次更新提交时,服务端对象已被修改,此时 Apiserver 将返回冲突错误(409),客户端需重新获取服务端数据,重新修改后再次提交到服务器端。

ResourceVersion 字段在 Kubernetes 中除了用在update的并发控制机制外,还用在 Kubernetes 的 list-watch 机制中。Client 端的 list-watch 分为两个步骤,先 list 取回所有对象,再以增量的方式 watch 后续对象。Client 端在list取回所有对象后,将会把最新对象的 ResourceVersion 作为下一步 watch 操作的起点参数,也即 Kube-Apiserver 以收到的 ResourceVersion 为起始点返回后续数据,保证了 list-watch 中数据的连续性与完整性。

Written by

说点什么

3 评论 在 "K8S 中 scheduler 组件的选主逻辑"

avatar

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

  Subscribe  
最新 最旧 得票最多
提醒
trackback

[…] k8s 中 scheduler 的选主逻辑可以看这篇文章 […]

trackback

[…] k8s 中 scheduler 的选主逻辑可以看这篇 文章 […]

兜兜里有都是糖
游客
兜兜里有都是糖

高质量的博文,学习到了,点赞点赞👍