深入解析kubernetes中的选举机制

article/2023/10/1 3:48:15

Overview

在 Kubernetes的 kube-controller-manager , kube-scheduler, 以及使用 Operator 的底层实现 controller-rumtime 都支持高可用系统中的leader选举,本文将以理解 controller-rumtime (底层的实现是 client-go) 中的leader选举以在kubernetes controller中是如何实现的。

Background

在运行 kube-controller-manager 时,是有一些参数提供给cm进行leader选举使用的,可以参考官方文档提供的 参数 来了解相关参数。

--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的选举,发现官网是这么介绍的,下面是对官方的说明进行一个通俗总结。simple leader election with kubernetes

通过阅读文章得知,kubernetes API 提供了一中选举机制,只要运行在集群内的容器,都是可以实现选举功能的。

Kubernetes API通过提供了两个属性来完成选举动作的

  • ResourceVersions:每个API对象唯一一个ResourceVersion
  • Annotations:每个API对象都可以对这些key进行注释

注:这种选举会增加APIServer的压力。也就对etcd会产生影响

那么有了这些信息之后,我们来看一下,在Kubernetes集群中,谁是cm的leader(我们提供的集群只有一个节点,所以本节点就是leader)

在Kubernetes中所有启用了leader选举的服务都会生成一个 EndPoint ,在这个 EndPoint 中会有上面提到的label(Annotations)来标识谁是leader。

$ 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 leaderNormal  LeaderElection  9m     kube-controller-manager  master-machine_06730140-a503-487d-850b-1fe1619f1fe1 became leader

可以看出 Annotations: control-plane.alpha.kubernetes.io/leader: 标出了哪个node是leader。

election in controller-runtime

controller-runtime 有关leader选举的部分在 pkg/leaderelection 下面,总共100行代码,我们来看下做了些什么?

可以看到,这里只提供了创建资源锁的一些选项

type Options struct {// 在manager启动时,决定是否进行选举LeaderElection bool// 使用那种资源锁 默认为租用 leaseLeaderElectionResourceLock string// 选举发生的名称空间LeaderElectionNamespace string// 该属性将决定持有leader锁资源的名称LeaderElectionID string
}

通过 NewResourceLock 可以看到,这里是走的 client-go/tools/leaderelection下面,而这个leaderelection也有一个 example 来学习如何使用它。

通过 example 可以看到,进入选举的入口是一个 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 coderun(ctx)},OnStoppedLeading: func() {// 这里清理你的leaseklog.Infof("leader lost: %s", id)os.Exit(0)},OnNewLeader: func(identity string) {// we're notified when new leader electedif identity == id {// I just got the lockreturn}klog.Infof("new leader elected: %s", identity)},},
})

到这里,我们了解了锁的概念和如何启动一个锁,下面看下,client-go都提供了那些锁。

在代码 tools/leaderelection/resourcelock/interface.go 定义了一个锁抽象,interface提供了一个通用接口,用于锁定leader选举中使用的资源。

type Interface interface {// Get 返回选举记录Get(ctx context.Context) (*LeaderElectionRecord, []byte, error)// Create 创建一个LeaderElectionRecordCreate(ctx context.Context, ler LeaderElectionRecord) error// Update will update and existing LeaderElectionRecordUpdate(ctx context.Context, ler LeaderElectionRecord) error// RecordEvent is used to record eventsRecordEvent(string)// Identity 返回锁的标识Identity() string// Describe is used to convert details on current resource lock into a stringDescribe() string
}

那么实现这个抽象接口的就是,实现的资源锁,我们可以看到,client-go提供了四种资源锁

  • leaselock
  • configmaplock
  • multilock
  • endpointlock

leaselock

Lease是kubernetes控制平面中的通过ETCD来实现的一个Leases的资源,主要为了提供分布式租约的一种控制机制。相关对这个API的描述可以参考于:Lease 。

在Kubernetes集群中,我们可以使用如下命令来查看对应的lease

$ 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:51ZManaged Fields:API Version:  coordination.k8s.io/v1Fields Type:  FieldsV1fieldsV1:f:spec:f:acquireTime:f:holderIdentity:f:leaseDurationSeconds:f:leaseTransitions:f:renewTime:Manager:         kube-controller-managerOperation:       UpdateTime:            2022-06-24T11:01:51ZResource Version:  56012Self Link:         /apis/coordination.k8s.io/v1/namespaces/kube-system/leases/kube-controller-managerUID:               851a32d2-25dc-49b6-a3f7-7a76f152f071
Spec:Acquire Time:            2022-06-27T15:30:46.000000ZHolder Identity:         master-machine_06730140-a503-487d-850b-1fe1619f1fe1Lease Duration Seconds:  15Lease Transitions:       2Renew Time:              2022-06-28T06:09:26.837773Z
Events:                    <none>

下面来看下leaselock的实现,leaselock会实现了作为资源锁的抽象

type LeaseLock struct {// LeaseMeta 就是类似于其他资源类型的属性,包含name ns 以及其他关于lease的属性LeaseMeta  metav1.ObjectMetaClient     coordinationv1client.LeasesGetter // Client 就是提供了informer中的功能// lockconfig包含上面通过 describe 看到的 Identity与recoder用于记录资源锁的更改LockConfig ResourceLockConfig// lease 就是 API中的Lease资源,可以参考下上面给出的这个API的使用lease      *coordinationv1.Lease
}

下面来看下leaselock实现了那些方法?

Get

Get 是从spec中返回选举的记录

func (ll *LeaseLock) Get(ctx context.Context) (*LeaderElectionRecord, []byte, error) {var err errorll.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 LeaderElectionRecordif 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

Create 是在kubernetes集群中尝试去创建一个租约,可以看到,Client就是API提供的对应资源的REST客户端,结果会在Kubernetes集群中创建这个Lease

func (ll *LeaseLock) Create(ctx context.Context, ler LeaderElectionRecord) error {var err errorll.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
}

Update

Update 是更新Lease的spec

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 = leasereturn nil
}

RecordEvent

RecordEvent 是记录选举时出现的事件,这时候我们回到上部分 在kubernetes集群中查看 ep 的信息时可以看到的event中存在 became leader 的事件,这里就是将产生的这个event添加到 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 schemasubject.Kind = "Lease"subject.APIVersion = coordinationv1.SchemeGroupVersion.String()ll.LockConfig.EventRecorder.Eventf(subject, corev1.EventTypeNormal, "LeaderElection", events)
}

到这里大致上了解了资源锁究竟是什么了,其他种类的资源锁也是相同的实现的方式,这里就不过多阐述了;下面的我们来看看选举的过程。

election workflow

选举的代码入口是在 leaderelection.go ,这里会继续上面的 example 向下分析整个选举的过程。

前面我们看到了进入选举的入口是一个 RunOrDie() 的函数,那么就继续从这里开始来了解。进入 RunOrDie,看到其实只有几行而已,大致上了解到了RunOrDie会使用提供的配置来启动选举的客户端,之后会阻塞,直到 ctx 退出,或停止持有leader的租约。

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.LeaderElectionRecordobservedRawRecord []byteobservedTime      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 testingclock clock.Clock// 锁定 observedRecordobservedRecordLock sync.Mutexmetrics leaderMetricsAdapter
}

可以看到 Run 实现的选举逻辑就是在初始化客户端时传入的 三个 callback

func (le *LeaderElector) Run(ctx context.Context) {defer runtime.HandleCrash()defer func() { // 退出时执行callbacke的OnStoppedLeadingle.config.Callbacks.OnStoppedLeading()}()if !le.acquire(ctx) {return}ctx, cancel := context.WithCancel(ctx)defer cancel()go le.config.Callbacks.OnStartedLeading(ctx) // 选举时,执行 OnStartedLeadingle.renew(ctx)
}

在 Run 中调用了 acquire,这个是 通过一个loop去调用 tryAcquireOrRenew,直到ctx传递过来结束信号

func (le *LeaderElector) acquire(ctx context.Context) bool {ctx, cancel := context.WithCancel(ctx)defer cancel()succeeded := falsedesc := 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 是尝试获得一个leader租约,如果已经获得到了,则更新租约;否则可以得到租约则为true,反之false

func (le *LeaderElector) tryAcquireOrRenew(ctx context.Context) bool {now := metav1.Now() // 时间leaderElectionRecord := rl.LeaderElectionRecord{ // 构建一个选举recordHolderIdentity:       le.config.Lock.Identity(), // 选举人的身份特征,ep与主机名有关LeaseDurationSeconds: int(le.config.LeaseDuration / time.Second), // 默认15sRenewTime:            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}// 到了这里就已经拿到或者创建了租约,然后记录其一些属性,LeaderElectionRecordle.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.AcquireTimeleaderElectionRecord.LeaderTransitions = oldLeaderElectionRecord.LeaderTransitions} else { // LeaderTransitions 就是指leader调整(转变为其他)了几次,如果是,// 则为发生转变,保持原有值// 反之,则+1leaderElectionRecord.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
}

summary

到这里,已经完整知道利用kubernetes进行选举的流程都是什么了;下面简单回顾下,上述leader选举所有的步骤:

  • 首选创建的服务就是该服务的leader,锁可以为 lease , endpoint 等资源进行上锁
  • 已经是leader的实例会不断续租,租约的默认值是15秒 (leaseDuration);leader在租约满时更新租约时间(renewTime)。
  • 其他的follower,会不断检查对应资源锁的存在,如果已经有leader,那么则检查 renewTime,如果超过了租用时间(),则表明leader存在问题需要重新启动选举,直到有follower提升为leader。
  • 而为了避免资源被抢占,Kubernetes API使用了 ResourceVersion 来避免被重复修改(如果版本号与请求版本号不一致,则表示已经被修改了,那么APIServer将返回错误)

Reference

Kubernetes 并发控制与数据一致性的实现原理

Controller manager 的高可用实现方式

deep dive into kubernetes simple leader election


http://www.ngui.cc/article/show-458856.html

相关文章

curl: (56) Recv failure: Connection reset by peer

Docker 重定向问题:curl: (56) Recv failure: Connection reset by peer解决思路☺ 以下考虑情况,针对的是docker部署springboot项目,我在springboot的配置文件 application.properties 设置了端口为 88481、考虑是不是端口冲突了 ,比如当前运行的项目的端口是8848,然而在项目…

使用.NetCore自带的后台作业,出入队简单模拟生产者消费者处理请求响应的数据

环境:Core:3.1的项目 说明:由于该方案为个人测试项目,重启时队列中的部分数据很可能会丢失, 对数据有要求的该方案不适用,不能照搬需要持久化处理, 另外发布到Linux Docker中通常不会自动回收,但是发布到IIS中需要简单设置不回收即可!!! 如下截图:在IIS中找到这个站…

用WindowsAppSDK(WASDK)优雅的开发上位机应用

C#开发上位机应用的一些选择 如果你不想看介绍,可以直接跳到优雅开发示例那里。 1. WASDK(WinUI 3)Windows 应用 SDK 是一组新的开发人员组件和工具,它们代表着 Windows 应用开发平台的下一步发展。 Windows 应用 SDK 提供一组统一的 API 和工具,可供从 Windows 11 到 Win…

Spring Data JPA系列5:让IDEA自动帮你写JPA实体定义代码

正所谓“工欲善其事、必先利其器”,面对一个优秀的框架,如果再结合一些外部的工具,其实可以让我们的开发效率与程序员开发过程的体验更上一层楼的。大家好,又见面了。 这是本系列的最后一篇文档啦,先来回顾下前面4篇:在第1篇《Spring Data JPA系列1:JDBC、ORM、JPA、Spr…

如何制作精美的图片

当我们在日常浏览视频网站、查看公众号文章时,精美的图片封面往往更容易吸引我们去点击。像我们的技术类文章如果能在文中穿插一些有意思、简明扼要或有动感的图片,就看起来特别的有层次感。一、什么样的图片适合我们 支持CC0免费商用协议的图片,是没有版权风险的,当然我们…

PotPlayer播放百度云盘视频

需要的工具 PotPlayer、油猴tampermonkey、坚果(这个不用下载,有个账号就行) 下载地址:百度网盘 步骤 安装油猴tampermonkey拖拽Tampermonkey_4.14.crx过程chrome浏览器上,如果提示安装失败,则在扩展程序中打开开发者模式安装PotPlayer云盘脚本复制PotPlayer播放云盘视频的…

FS2K人脸素描属性识别

根据FS2K数据集,利用VGG16,DenseNet,ResNet进行训练和测试,实现输入一张图片(简笔画和普通照片),输出该图片的属性特征信息人脸素描属性识别 代码:https://github.com/linkcao/FS2K_extract 问题分析需要根据FS2K数据集进行训练和测试,实现输入一张图片,输出该图片的属…

从Hadder看蛋白质分子中的加氢算法

本文主要介绍了开源加氢软件Hadder中用到的一些常规的补氢算法。在存储和优化蛋白质结构的过程中,人们更多的关注于蛋白质本身的骨架的变化,而单个原子的细微变化,对整体产生的性质是微乎其微的。但是我们在建立力场以及做能量最小化的过程中,需要用到氢原子。而氢原子的初…

CODING DevOps 助力中化信息打造新一代研效平台,驱动“线上中化”新未来

中化信息技术有限公司,简称“中化信息”,是世界 500 强企业中国中化控股有限责任公司(简称“中国中化”)的全资直属公司,依托于中国中化的信息化建设实践,建立起从咨询、设计到研发、交付及运维的服务价值链,形成涵盖生命科学、材料科学、基础化工、环境科学、轮胎橡胶、…

使用Karmada实现Helm应用的跨集群部署

摘要:借助Karmada原生API的支持能力,Karmada可以借助Flux轻松实现Helm应用的跨集群部署。 本文分享自华为云社区《 使用Karmada实现Helm应用的跨集群部署【云原生开源】》,作者:华为云云原生开源团队。 背景 通过使用 Kubernetes 原生 API 并提供高级调度功能,Karmada已经…