Глубокий анализ механизма выборов в kubernetes


Обзор

В Kubernetes, kube-controller-manager, kube-scheduler, и базовая реализация controller используют Operator<##code -rumtime все поддерживают выборы лидера в высокодоступной системе, в этой статье мы рассмотрим, как выборы лидера в controller-rumtime (базовая реализация - client-go) реализованы в kubernetes. контроллер.

Справочная информация

При запуске kube-controller-manager существует ряд параметров, которые предоставляются cm для выборов лидера, см. официальную документацию по параметрам.

--leader-elect                               Default: true
--leader-elect-renew-deadline duration       Default: 10s
--leader-elect-resource-lock string          Default: "leases"
--leader-elect-resource-name string          Default: "kube-controller-manager"
--leader-elect-resource-namespace string     Default: "kube-system"
--leader-elect-retry-period duration         Default: 2s
...
Войдите в полноэкранный режим Выход из полноэкранного режима

Я думал, что выборы этих компонентов осуществляются через etcd, но когда я изучил controller-runtime, я обнаружил, что параметры, связанные с etcd, не были настроены, что заставило меня задуматься о механизме выборов. Я искал информацию о выборах в kubernetes и нашел это на официальном сайте, вот общее резюме официальных инструкций. простые выборы лидера с kubernetes

Как видно из статьи, API kubernetes предоставляет механизм выборов, который может быть реализован для любого контейнера, запущенного в кластере.

API Kubernetes делает это, предоставляя два свойства

  • ResourceVersions: Каждый объект API имеет уникальную ResourceVersion.
  • Аннотации: каждый объект API может быть аннотирован с помощью этих ключей

Примечание: Эти выборы увеличат нагрузку на APIServer. Это также окажет влияние на etcd

Итак, имея на руках эту информацию, давайте посмотрим, кто является лидером cm в кластере Kubernetes (мы предоставляем кластер с одним узлом, поэтому этот узел является лидером).

Все сервисы в Kubernetes, у которых включено избрание лидера, будут генерировать EndPoint, и в этом EndPoint будут метки (Annotations), упомянутые выше, чтобы определить, кто является лидером.

$ kubectl get ep -n kube-system
NAME                      ENDPOINTS   AGE
kube-controller-manager   <none>      3d4h
kube-dns                              3d4h
kube-scheduler            <none>      3d4h
Войдите в полноэкранный режим Выход из полноэкранного режима

Вот пример того, что kube-controller-manager может сказать о EndPoint

[root@master-machine ~]# kubectl describe ep kube-controller-manager -n kube-system
Name:         kube-controller-manager
Namespace:    kube-system
Labels:       <none>
Annotations:  control-plane.alpha.kubernetes.io/leader:
                {"holderIdentity":"master-machine_06730140-a503-487d-850b-1fe1619f1fe1","leaseDurationSeconds":15,"acquireTime":"2022-06-27T15:30:46Z","re...
Subsets:
Events:
  Type    Reason          Age    From                     Message
  ----    ------          ----   ----                     -------
  Normal  LeaderElection  2d22h  kube-controller-manager  master-machine_76aabcb5-49ff-45ff-bd18-4afa61fbc5af became leader
  Normal  LeaderElection  9m     kube-controller-manager  master-machine_06730140-a503-487d-850b-1fe1619f1fe1 became leader
Войдите в полноэкранный режим Выход из полноэкранного режима

Вы можете видеть, что Аннотации: control-plane.alpha.kubernetes.io/leader: определяет, какой узел является лидером.

выборы в controller-runtime

controller-runtime Раздел о выборах лидера находится в разделе pkg/leaderelection, всего 100 строк кода, поэтому давайте посмотрим, что делается.

Как видите, здесь предусмотрено всего несколько вариантов создания блокировок ресурсов

type Options struct {
    // 在manager启动时,决定是否进行选举
    LeaderElection bool
    // 使用那种资源锁 默认为租用 lease
    LeaderElectionResourceLock string
    // 选举发生的名称空间
    LeaderElectionNamespace string
    // 该属性将决定持有leader锁资源的名称
    LeaderElectionID string
}
Войдите в полноэкранный режим Выход из полноэкранного режима

На примере NewResourceLock вы можете увидеть, что он находится в разделе client-go/tools/leaderelection, и в этом разделе также есть пример, чтобы узнать, как его использовать.

Как видно из примера, точкой входа в выборы является функция RunOrDie()

// 这里使用了一个lease锁,注释中说愿意为集群中存在lease的监听较少
lock := &resourcelock.LeaseLock{
    LeaseMeta: metav1.ObjectMeta{
        Name:      leaseLockName,
        Namespace: leaseLockNamespace,
    },
    Client: client.CoordinationV1(),
    LockConfig: resourcelock.ResourceLockConfig{
        Identity: id,
    },
}

// 开启选举循环
leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
    Lock: lock,
    // 这里必须保证拥有的租约在调用cancel()前终止,否则会仍有一个loop在运行
    ReleaseOnCancel: true,
    LeaseDuration:   60 * time.Second,
    RenewDeadline:   15 * time.Second,
    RetryPeriod:     5 * time.Second,
    Callbacks: leaderelection.LeaderCallbacks{
        OnStartedLeading: func(ctx context.Context) {
            // 这里填写你的代码,
            // usually put your code
            run(ctx)
        },
        OnStoppedLeading: func() {
            // 这里清理你的lease
            klog.Infof("leader lost: %s", id)
            os.Exit(0)
        },
        OnNewLeader: func(identity string) {
            // we're notified when new leader elected
            if identity == id {
                // I just got the lock
                return
            }
            klog.Infof("new leader elected: %s", identity)
        },
    },
})
Войдите в полноэкранный режим Выход из полноэкранного режима

Теперь, когда мы поняли концепцию блокировки и как ее запустить, давайте рассмотрим блокировки, которые предоставляет client-go.

Абстракция блокировки определена в коде tools/leaderelection/resourcelock/interface.go. Интерфейс предоставляет общий интерфейс для блокировки ресурсов, используемых в выборах лидера.

type Interface interface {
    // Get 返回选举记录
    Get(ctx context.Context) (*LeaderElectionRecord, []byte, error)

    // Create 创建一个LeaderElectionRecord
    Create(ctx context.Context, ler LeaderElectionRecord) error

    // Update will update and existing LeaderElectionRecord
    Update(ctx context.Context, ler LeaderElectionRecord) error

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

    // Identity 返回锁的标识
    Identity() string

    // Describe is used to convert details on current resource lock into a string
    Describe() string
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Реализация этого абстрактного интерфейса является реализацией блокировки ресурса, и мы видим, что client-go предоставляет четыре типа блокировок ресурса

  • leaselock
  • configmaplock
  • multilock
  • конечная точка

leaselock

Lease — это ресурс для реализации аренды в плоскости управления kubernetes через ETCD, в первую очередь для обеспечения механизма управления распределенной арендой. Описание этого API можно найти на сайте Lease.

В кластере Kubernetes мы можем использовать следующую команду для просмотра соответствующей аренды

$ kubectl get leases -A
NAMESPACE         NAME                      HOLDER                                                AGE
kube-node-lease   master-machine            master-machine                                        3d19h
kube-system       kube-controller-manager   master-machine_06730140-a503-487d-850b-1fe1619f1fe1   3d19h
kube-system       kube-scheduler            master-machine_1724e2d9-c19c-48d7-ae47-ee4217b27073   3d19h

$ kubectl describe leases kube-controller-manager -n kube-system
Name:         kube-controller-manager
Namespace:    kube-system
Labels:       <none>
Annotations:  <none>
API Version:  coordination.k8s.io/v1
Kind:         Lease
Metadata:
  Creation Timestamp:  2022-06-24T11:01:51Z
  Managed Fields:
    API Version:  coordination.k8s.io/v1
    Fields Type:  FieldsV1
    fieldsV1:
      f:spec:
        f:acquireTime:
        f:holderIdentity:
        f:leaseDurationSeconds:
        f:leaseTransitions:
        f:renewTime:
    Manager:         kube-controller-manager
    Operation:       Update
    Time:            2022-06-24T11:01:51Z
  Resource Version:  56012
  Self Link:         /apis/coordination.k8s.io/v1/namespaces/kube-system/leases/kube-controller-manager
  UID:               851a32d2-25dc-49b6-a3f7-7a76f152f071
Spec:
  Acquire Time:            2022-06-27T15:30:46.000000Z
  Holder Identity:         master-machine_06730140-a503-487d-850b-1fe1619f1fe1
  Lease Duration Seconds:  15
  Lease Transitions:       2
  Renew Time:              2022-06-28T06:09:26.837773Z
Events:                    <none>
Войдите в полноэкранный режим Выход из полноэкранного режима

Вот взгляд на реализацию leaselock, которая реализует абстракцию блокировки ресурсов

type LeaseLock struct {
    // LeaseMeta 就是类似于其他资源类型的属性,包含name ns 以及其他关于lease的属性
    LeaseMeta  metav1.ObjectMeta
    Client     coordinationv1client.LeasesGetter // Client 就是提供了informer中的功能
    // lockconfig包含上面通过 describe 看到的 Identity与recoder用于记录资源锁的更改
    LockConfig ResourceLockConfig
    // lease 就是 API中的Lease资源,可以参考下上面给出的这个API的使用
    lease      *coordinationv1.Lease
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Какие методы реализует lexelock?

Получить

Получение — это возвращение записи о выборах из спецификации

func (ll *LeaseLock) Get(ctx context.Context) (*LeaderElectionRecord, []byte, error) {
    var err error
    ll.lease, err = ll.Client.Leases(ll.LeaseMeta.Namespace).Get(ctx, ll.LeaseMeta.Name, metav1.GetOptions{})
    if err != nil {
        return nil, nil, err
    }
    record := LeaseSpecToLeaderElectionRecord(&ll.lease.Spec)
    recordByte, err := json.Marshal(*record)
    if err != nil {
        return nil, nil, err
    }
    return record, recordByte, nil
}

// 可以看出是返回这个资源spec里面填充的值
func LeaseSpecToLeaderElectionRecord(spec *coordinationv1.LeaseSpec) *LeaderElectionRecord {
    var r LeaderElectionRecord
    if spec.HolderIdentity != nil {
        r.HolderIdentity = *spec.HolderIdentity
    }
    if spec.LeaseDurationSeconds != nil {
        r.LeaseDurationSeconds = int(*spec.LeaseDurationSeconds)
    }
    if spec.LeaseTransitions != nil {
        r.LeaderTransitions = int(*spec.LeaseTransitions)
    }
    if spec.AcquireTime != nil {
        r.AcquireTime = metav1.Time{spec.AcquireTime.Time}
    }
    if spec.RenewTime != nil {
        r.RenewTime = metav1.Time{spec.RenewTime.Time}
    }
    return &r
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Создать

Create — попытка создания аренды в кластере kubernetes. Как вы можете видеть, Client является REST-клиентом соответствующего ресурса, предоставляемого API, что приводит к созданию аренды в кластере Kubernetes

func (ll *LeaseLock) Create(ctx context.Context, ler LeaderElectionRecord) error {
    var err error
    ll.lease, err = ll.Client.Leases(ll.LeaseMeta.Namespace).Create(ctx, &coordinationv1.Lease{
        ObjectMeta: metav1.ObjectMeta{
            Name:      ll.LeaseMeta.Name,
            Namespace: ll.LeaseMeta.Namespace,
        },
        Spec: LeaderElectionRecordToLeaseSpec(&ler),
    }, metav1.CreateOptions{})
    return err
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Обновление

Обновление — это обновление спецификации аренды

func (ll *LeaseLock) Update(ctx context.Context, ler LeaderElectionRecord) error {
    if ll.lease == nil {
        return errors.New("lease not initialized, call get or create first")
    }
    ll.lease.Spec = LeaderElectionRecordToLeaseSpec(&ler)

    lease, err := ll.Client.Leases(ll.LeaseMeta.Namespace).Update(ctx, ll.lease, metav1.UpdateOptions{})
    if err != nil {
        return err
    }

    ll.lease = lease
    return nil
}
Войдите в полноэкранный режим Выход из полноэкранного режима

RecordEvent

RecordEvent — это запись событий, произошедших во время выборов, и здесь мы возвращаемся к предыдущему разделу. Когда мы смотрим на информацию ep в кластере kubernetes, мы видим, что в ней есть событие became leader, и здесь мы добавляем полученное событие в meta-. data.

func (ll *LeaseLock) RecordEvent(s string) {
   if ll.LockConfig.EventRecorder == nil {
      return
   }
   events := fmt.Sprintf("%v %v", ll.LockConfig.Identity, s)
   subject := &coordinationv1.Lease{ObjectMeta: ll.lease.ObjectMeta}
   // Populate the type meta, so we don't have to get it from the schema
   subject.Kind = "Lease"
   subject.APIVersion = coordinationv1.SchemeGroupVersion.String()
   ll.LockConfig.EventRecorder.Eventf(subject, corev1.EventTypeNormal, "LeaderElection", events)
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Другие типы блокировок ресурсов реализуются аналогичным образом, поэтому я не буду слишком подробно останавливаться на этом; давайте рассмотрим процесс выбора.

Рабочий процесс выборов

Точка входа для кода выборов находится в файле leaderelection.go, и мы продолжим на примере выше, чтобы проанализировать весь процесс выборов.

Ранее мы видели, что точкой входа в выборы является функция RunOrDie(), поэтому продолжим с этого места. Когда вы вводите RunOrDie, вы видите, что на самом деле здесь всего несколько строк, и вы узнаете в общих чертах, что RunOrDie будет использовать предоставленную конфигурацию для запуска клиента выборов, а затем будет блокировать до тех пор, пока ctx не выйдет или не перестанет удерживать аренду лидера.

func RunOrDie(ctx context.Context, lec LeaderElectionConfig) {
    le, err := NewLeaderElector(lec)
    if err != nil {
        panic(err)
    }
    if lec.WatchDog != nil {
        lec.WatchDog.SetLeaderElection(le)
    }
    le.Run(ctx)
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Вот взгляд на то, что делает NewLeaderElector. Как вы можете видеть, LeaderElector — это структура, которая только что создана здесь, и эта структура предоставляет все необходимое для выборов (LeaderElector — это клиент выборов, созданный RunOrDie).

func NewLeaderElector(lec LeaderElectionConfig) (*LeaderElector, error) {
    if lec.LeaseDuration <= lec.RenewDeadline {
        return nil, fmt.Errorf("leaseDuration must be greater than renewDeadline")
    }
    if lec.RenewDeadline <= time.Duration(JitterFactor*float64(lec.RetryPeriod)) {
        return nil, fmt.Errorf("renewDeadline must be greater than retryPeriod*JitterFactor")
    }
    if lec.LeaseDuration < 1 {
        return nil, fmt.Errorf("leaseDuration must be greater than zero")
    }
    if lec.RenewDeadline < 1 {
        return nil, fmt.Errorf("renewDeadline must be greater than zero")
    }
    if lec.RetryPeriod < 1 {
        return nil, fmt.Errorf("retryPeriod must be greater than zero")
    }
    if lec.Callbacks.OnStartedLeading == nil {
        return nil, fmt.Errorf("OnStartedLeading callback must not be nil")
    }
    if lec.Callbacks.OnStoppedLeading == nil {
        return nil, fmt.Errorf("OnStoppedLeading callback must not be nil")
    }

    if lec.Lock == nil {
        return nil, fmt.Errorf("Lock must not be nil.")
    }
    le := LeaderElector{
        config:  lec,
        clock:   clock.RealClock{},
        metrics: globalMetricsFactory.newLeaderMetrics(),
    }
    le.metrics.leaderOff(le.config.Name)
    return &le, nil
}
Войдите в полноэкранный режим Выход из полноэкранного режима

LeaderElector — это клиент для выборов, созданный

type LeaderElector struct {
    config LeaderElectionConfig // 这个的配置,包含一些时间参数,健康检查
    // recoder相关属性
    observedRecord    rl.LeaderElectionRecord
    observedRawRecord []byte
    observedTime      time.Time
    // used to implement OnNewLeader(), may lag slightly from the
    // value observedRecord.HolderIdentity if the transition has
    // not yet been reported.
    reportedLeader string
    // clock is wrapper around time to allow for less flaky testing
    clock clock.Clock
    // 锁定 observedRecord
    observedRecordLock sync.Mutex
    metrics leaderMetricsAdapter
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Вы можете видеть, что логика выборов, реализованная в Run, — это три обратных вызова, которые передаются при инициализации клиента

func (le *LeaderElector) Run(ctx context.Context) {
    defer runtime.HandleCrash()
    defer func() { // 退出时执行callbacke的OnStoppedLeading
        le.config.Callbacks.OnStoppedLeading()
    }()

    if !le.acquire(ctx) {
        return
    }
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()
    go le.config.Callbacks.OnStartedLeading(ctx) // 选举时,执行 OnStartedLeading
    le.renew(ctx)
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Это вызывается в Run, который вызывается через цикл для tryAcquireOrRenew, пока сигнал end не будет передан ctx

func (le *LeaderElector) acquire(ctx context.Context) bool {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()
    succeeded := false
    desc := le.config.Lock.Describe()
    klog.Infof("attempting to acquire leader lease %v...", desc)
    // jitterUntil是执行定时的函数 func() 是定时任务的逻辑
    // RetryPeriod是周期间隔
    // JitterFactor 是重试系数,类似于延迟队列中的系数 (duration + maxFactor * duration)
    // sliding 逻辑是否计算在时间内
    // 上下文传递
    wait.JitterUntil(func() {
        succeeded = le.tryAcquireOrRenew(ctx)
        le.maybeReportTransition()
        if !succeeded {
            klog.V(4).Infof("failed to acquire lease %v", desc)
            return
        }
        le.config.Lock.RecordEvent("became leader")
        le.metrics.leaderOn(le.config.Name)
        klog.Infof("successfully acquired lease %v", desc)
        cancel()
    }, le.config.RetryPeriod, JitterFactor, true, ctx.Done())
    return succeeded
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Фактическое действие выборов здесь происходит в tryAcquireOrRenew, вот взгляд на tryAcquireOrRenew; tryAcquireOrRenew пытается получить аренду лидера, если она получена, аренда обновляется; в противном случае это true, если аренда доступна, false, если нет.

func (le *LeaderElector) tryAcquireOrRenew(ctx context.Context) bool {
    now := metav1.Now() // 时间
    leaderElectionRecord := rl.LeaderElectionRecord{ // 构建一个选举record
        HolderIdentity:       le.config.Lock.Identity(), // 选举人的身份特征,ep与主机名有关
        LeaseDurationSeconds: int(le.config.LeaseDuration / time.Second), // 默认15s
        RenewTime:            now, // 重新获取时间
        AcquireTime:          now, // 获得时间
    }

    // 1. 从API获取或创建一个recode,如果可以拿到则已经有租约,反之创建新租约
    oldLeaderElectionRecord, oldLeaderElectionRawRecord, err := le.config.Lock.Get(ctx)
    if err != nil {
        if !errors.IsNotFound(err) {
            klog.Errorf("error retrieving resource lock %v: %v", le.config.Lock.Describe(), err)
            return false
        }
        // 创建租约的动作就是新建一个对应的resource,这个lock就是leaderelection提供的四种锁,
        // 看你在runOrDie中初始化传入了什么锁
        if err = le.config.Lock.Create(ctx, leaderElectionRecord); err != nil {
            klog.Errorf("error initially creating leader election record: %v", err)
            return false
        }
        // 到了这里就已经拿到或者创建了租约,然后记录其一些属性,LeaderElectionRecord
        le.setObservedRecord(&leaderElectionRecord)

        return true
    }

    // 2. 获取记录检查身份和时间
    if !bytes.Equal(le.observedRawRecord, oldLeaderElectionRawRecord) {
        le.setObservedRecord(oldLeaderElectionRecord)

        le.observedRawRecord = oldLeaderElectionRawRecord
    }
    if len(oldLeaderElectionRecord.HolderIdentity) > 0 &&
        le.observedTime.Add(le.config.LeaseDuration).After(now.Time) &&
        !le.IsLeader() { // 不是leader,进行HolderIdentity比较,再加上时间,这个时候没有到竞选其,跳出
        klog.V(4).Infof("lock is held by %v and has not yet expired", oldLeaderElectionRecord.HolderIdentity)
        return false
    }

    // 3.我们将尝试更新。 在这里leaderElectionRecord设置为默认值。让我们在更新之前更正它。
    if le.IsLeader() { // 到这就说明是leader,修正他的时间
        leaderElectionRecord.AcquireTime = oldLeaderElectionRecord.AcquireTime
        leaderElectionRecord.LeaderTransitions = oldLeaderElectionRecord.LeaderTransitions
    } else { // LeaderTransitions 就是指leader调整(转变为其他)了几次,如果是,
        // 则为发生转变,保持原有值
        // 反之,则+1
        leaderElectionRecord.LeaderTransitions = oldLeaderElectionRecord.LeaderTransitions + 1
    }
    // 完事之后更新APIServer中的锁资源,也就是更新对应的资源的属性信息
    if err = le.config.Lock.Update(ctx, leaderElectionRecord); err != nil {
        klog.Errorf("Failed to update lock: %v", err)
        return false
    }
    // setObservedRecord 是通过一个新的record来更新这个锁中的record
    // 操作是安全的,会上锁保证临界区仅可以被一个线程/进程操作
    le.setObservedRecord(&leaderElectionRecord)
    return true
}
Войдите в полноэкранный режим Выход из полноэкранного режима

резюме

На данный момент вы имеете полное представление о процессе выборов с использованием kubernetes; здесь приводится краткий обзор всех шагов, связанных с вышеупомянутыми выборами лидера.

  • Предпочтительная служба является лидером службы, и блокировки могут быть размещены на таких ресурсах, как lease , endpoint и т.д.
  • Экземпляры, которые уже являются лидерами, постоянно обновляются, значение аренды по умолчанию составляет 15 секунд (leaseDuration); лидер обновляет время аренды, когда оно заполняется (renewTime).
  • Для другого последователя постоянно проверяется существование соответствующей блокировки ресурса, и если уже есть лидер, то проверяется renewTime, и если время аренды () превышено, то это говорит о том, что есть проблема с лидером и выборы должны быть перезапущены, пока последователь не будет продвинут до лидера.
  • А чтобы избежать загромождения ресурсов, Kubernetes API использует ResourceVersion, чтобы избежать дублирования модификаций (если номер версии не совпадает с номером запрашиваемой версии, значит, она уже была изменена, и APIServer вернет ошибку).

Ссылка

Как реализован контроль параллелизма и согласованность данных в Kubernetes

Высокодоступная реализация менеджера контроллера

Глубокое погружение в простые выборы лидера в kubernetes

Оцените статью
devanswers.ru
Добавить комментарий