前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Kubernetes 如何实现组件高可用

Kubernetes 如何实现组件高可用

作者头像
CS实验室
发布于 2022-08-01 12:17:31
发布于 2022-08-01 12:17:31
71900
代码可运行
举报
文章被收录于专栏:CS实验室CS实验室
运行总次数:0
代码可运行

Kubernetes 中,Controller Manager、Scheduler 等组件以及用户实现的 Controller,都是通过多副本的方式来实现高可用。但多副本 Controller 同时工作难免会引发所监听资源的竞争条件,所以通常多副本之间只有一个副本在工作。

为了避免这种竞争条件,Kubernetes 提供了 Leader 选举的模式,多副本之间相互竞争 Leader,只有成为 Leader 才工作,否则一直等待。本文将从 Leader 选举的原理以及作为用户如何使用等方面,介绍如何在 Kubernetes 中实现组件的高可用。

Leader 选举

Leader 选举的原理主要是利用 Lease、ConfigMap、Endpoint 资源实现乐观锁,Lease 资源中定义了 Leader 的 id、抢占时间等信息;ConfigMap 和 Endpoint 在其 annotation 中定义 control-plane.alpha.kubernetes.io/leader 为 leader。是的,没错,如果我们自己实现,随便定义自己的喜欢的字段也行,这里其实是利用了 resourceVersion 来实现的乐观锁。

原理如下图所示,多个副本之间会竞争同一个资源,抢占到了锁就成为 Leader,并定期更新;抢占不到则原地等待,不断尝试抢占。

client-go 中提供了锁的工具方法,k8s 的组件也是直接通过 client-go 来使用的。接下来我们来分析 client-go 提供的工具方法如何实现 Leader 选举。

抢占锁

首先会根据定义的名称获取锁,没有则创建;随后判断当前锁有没有 Leader 以及 Leader 的租期是否到期,没有则抢占锁,否则返回并等待。

其中,抢占锁的过程势必会存在 update 资源的操作,而 k8s 通过版本号的乐观锁实现了 update 操作的原子性。在 update 资源时,ApiServer 会对比 resourceVersion,如果不一致将返回冲突错误。通过这种方式,update 操作的安全性就得到了保证。

抢占锁的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (le *LeaderElector) tryAcquireOrRenew(ctx context.Context) bool {
 now := metav1.Now()
 leaderElectionRecord := rl.LeaderElectionRecord{
  HolderIdentity:       le.config.Lock.Identity(),
  LeaseDurationSeconds: int(le.config.LeaseDuration / time.Second),
  RenewTime:            now,
  AcquireTime:          now,
 }

 // 1. obtain or create the ElectionRecord
 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
  }
  if err = le.config.Lock.Create(ctx, leaderElectionRecord); err != nil {
   klog.Errorf("error initially creating leader election record: %v", err)
   return false
  }

  le.setObservedRecord(&leaderElectionRecord)
  return true
 }

 // 2. Record obtained, check the Identity & Time
 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() {
  klog.V(4).Infof("lock is held by %v and has not yet expired", oldLeaderElectionRecord.HolderIdentity)
  return false
 }

 // 3. We're going to try to update. The leaderElectionRecord is set to it's default
 // here. Let's correct it before updating.
 if le.IsLeader() {
  leaderElectionRecord.AcquireTime = oldLeaderElectionRecord.AcquireTime
  leaderElectionRecord.LeaderTransitions = oldLeaderElectionRecord.LeaderTransitions
 } else {
  leaderElectionRecord.LeaderTransitions = oldLeaderElectionRecord.LeaderTransitions + 1
 }

 // update the lock itself
 if err = le.config.Lock.Update(ctx, leaderElectionRecord); err != nil {
  klog.Errorf("Failed to update lock: %v", err)
  return false
 }

 le.setObservedRecord(&leaderElectionRecord)
 return true
}

client-go 仓库提供了一个 example,我们启动一个进程后,可以看到其 Lease 信息如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
$ kubectl get lease demo -oyaml
apiVersion: coordination.k8s.io/v1
kind: Lease
metadata:
  ...
spec:
  acquireTime: "2022-07-23T14:28:41.381108Z"
  holderIdentity: "1"
  leaseDurationSeconds: 60
  leaseTransitions: 0
  renewTime: "2022-07-23T14:28:41.397199Z"

释放锁

释放锁的逻辑是在 Leader 退出前,也是执行 update 操作,将 Lease 的 leader 信息清空。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (le *LeaderElector) release() bool {
 if !le.IsLeader() {
  return true
 }
 now := metav1.Now()
 leaderElectionRecord := rl.LeaderElectionRecord{
  LeaderTransitions:    le.observedRecord.LeaderTransitions,
  LeaseDurationSeconds: 1,
  RenewTime:            now,
  AcquireTime:          now,
 }
 if err := le.config.Lock.Update(context.TODO(), leaderElectionRecord); err != nil {
  klog.Errorf("Failed to release lock: %v", err)
  return false
 }

 le.setObservedRecord(&leaderElectionRecord)
 return true
}

将上一步启动的进程 kill 后,再看其 Lease 信息:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
$ kubectl get lease demo -oyaml
apiVersion: coordination.k8s.io/v1
kind: Lease
metadata:
  ...
spec:
  acquireTime: "2022-07-23T14:29:26.557658Z"
  holderIdentity: ""
  leaseDurationSeconds: 1
  leaseTransitions: 0
  renewTime: "2022-07-23T14:29:26.557658Z"

Controller 中如何使用

我们在实现自己的 Controller 的时候,通常是使用 controller runtime 工具,而 controller runtime 早已将 Leader 选举的逻辑做好了封装。

主要逻辑在两处,一是 Lease 基础信息的定义,根据用户的定义补充基础信息,如当前运行的 namespace 作为 leader 的 namespace、根据 host 生成随机的 id 等。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func NewResourceLock(config *rest.Config, recorderProvider recorder.Provider, options Options) (resourcelock.Interface, error) {
 if options.LeaderElectionResourceLock == "" {
  options.LeaderElectionResourceLock = resourcelock.LeasesResourceLock
 }

 // LeaderElectionID must be provided to prevent clashes
 if options.LeaderElectionID == "" {
  return nil, errors.New("LeaderElectionID must be configured")
 }

 // Default the namespace (if running in cluster)
 if options.LeaderElectionNamespace == "" {
  var err error
  options.LeaderElectionNamespace, err = getInClusterNamespace()
  if err != nil {
   return nil, fmt.Errorf("unable to find leader election namespace: %w", err)
  }
 }

 // Leader id, needs to be unique
 id, err := os.Hostname()
 if err != nil {
  return nil, err
 }
 id = id + "_" + string(uuid.NewUUID())

 // Construct clients for leader election
 rest.AddUserAgent(config, "leader-election")
 corev1Client, err := corev1client.NewForConfig(config)
 if err != nil {
  return nil, err
 }

 coordinationClient, err := coordinationv1client.NewForConfig(config)
 if err != nil {
  return nil, err
 }

 return resourcelock.New(options.LeaderElectionResourceLock,
  options.LeaderElectionNamespace,
  options.LeaderElectionID,
  corev1Client,
  coordinationClient,
  resourcelock.ResourceLockConfig{
   Identity:      id,
   EventRecorder: recorderProvider.GetEventRecorderFor(id),
  })
}

二是启动 leader 选举,注册 lock 信息、租期时间、callback 函数等信息,再启动选举进程:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (cm *controllerManager) startLeaderElection(ctx context.Context) (err error) {
 l, err := leaderelection.NewLeaderElector(leaderelection.LeaderElectionConfig{
  Lock:          cm.resourceLock,
  LeaseDuration: cm.leaseDuration,
  RenewDeadline: cm.renewDeadline,
  RetryPeriod:   cm.retryPeriod,
  Callbacks: leaderelection.LeaderCallbacks{
   OnStartedLeading: func(_ context.Context) {
    if err := cm.startLeaderElectionRunnables(); err != nil {
     cm.errChan <- err
     return
    }
    close(cm.elected)
   },
   OnStoppedLeading: func() {
    if cm.onStoppedLeading != nil {
     cm.onStoppedLeading()
    }
    cm.gracefulShutdownTimeout = time.Duration(0)
    cm.errChan <- errors.New("leader election lost")
   },
  },
  ReleaseOnCancel: cm.leaderElectionReleaseOnCancel,
 })
 if err != nil {
  return err
 }

 // Start the leader elector process
 go func() {
  l.Run(ctx)
  <-ctx.Done()
  close(cm.leaderElectionStopped)
 }()
 return nil
}

有了 controller runtime 对选举逻辑的包装,我们在使用的时候,就方便很多。根据 Controller Runtime 的使用姿势 一文的介绍,我们可以在初始化 Controller 的时候,定义 Lease 的信息:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 scheme := runtime.NewScheme()
 _ = corev1.AddToScheme(scheme)
 // 1. init Manager
 mgr, _ := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
  Scheme: scheme,
  Port:   9443,
  LeaderElection:     true,
  LeaderElectionID:   "demo.xxx",
 })
 // 2. init Reconciler(Controller)
 _ = ctrl.NewControllerManagedBy(mgr).
  For(&corev1.Pod{}).
  Complete(&ApplicationReconciler{})
...

只要在初始化时,加入 LeaderElection: true,以及 LeaderElectionID,即 Lease 的 name,保证集群内唯一即可。其他的信息 controller runtime 都会帮你填充。

总结

在生产环境中,高可用是一个很重要的功能,没有高可用的服务没人敢上生产。Kubernetes 基于 etcd 的 modifiedindex 实现了 resourceVersion 的乐观锁,通过这个乐观锁,Leader 选举机制才能够被多副本使用,避免竞争条件。我们在实现自己的 Controller 的时候只需要巧妙利用这一机制,就可以轻松实现高可用。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-07-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 CS实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
client-go实战之十二:选主(leader-election)
程序员欣宸
2023/08/14
1.2K0
client-go实战之十二:选主(leader-election)
【K8s源码品读】012:Phase 1 - kube-controller-manager - 了解控制管理中心
由于我们的示例是创建一个nginx的pod,涉及到kube-controller-manager的内容很少。
junedayday
2021/08/05
3560
k8s源码-scheduler流程深度剖析
入口函数里NewSchdulerCommand, kubernetes所有组件都使用common cli的形式,可参考cobra,NewSchedulerCommand后面会介绍,返回cobra.Command, 然后Execute该command。
ascehuang
2019/11/24
3.4K0
k8s源码-scheduler流程深度剖析
编写一个operator扩展kubernetes能力
Operator 是 CoreOS 推出的旨在简化复杂有状态应用管理,它是一个感知应用状态的控制器,通过扩展 Kubernetes API 来自动创建、管理和配置应用实例。 Operator 基于 CRD 扩展资源对象,并通过控制器来保证应用处于预期状态。
我的小碗汤
2019/04/25
2.6K0
编写一个operator扩展kubernetes能力
kubernetes 之 master高可用集群搭建
1、k8s的node默认已经有高可用了,因为在pod会随机分配到各个node上,如果有pod挂了,就会分配到其他node上,所以这里主要是做一下master的高可用。
kubernetes中文社区
2019/06/24
6.2K0
kubernetes 之 master高可用集群搭建
Kubernetes 调度器详解
在 Kubernetes 中,调度 (scheduling) 指的是确保 Pod 匹配到合适的节点, 以便 kubelet 能够运行它们。 调度的工作由调度器和控制器协调完成。
thierryzhou
2022/11/01
1.5K0
Kubernetes 调度器详解
controller-manager学习三部曲之二:源码学习
程序员欣宸
2024/05/26
1740
controller-manager学习三部曲之二:源码学习
Kubernetes Controller 机制详解(二)
在上一篇文章 Kubernetes Controller 机制详解(一)中,我们学习了 Kubernetes API List/Watch 机制,以及如何采用 Kubernetes client-go 中的 Informer 机制来创建 Controller。该方法需要用户了解 Kubernetes client-go 的实现原理,并在 Controller 的编码中处理较多 Informer 实现相关的细节。包括启动 InformerFactory,将 Watch 到的消息加入到队列,重试等等逻辑。如果有多个副本,还需要加入 Leader Election 的相关代码。如果需如果你创建了自定义的 CRD,可能还希望在创建资源时采用 webhook 对资源进行校验。这些功能都需要用户编写较多的代码。
赵化冰
2023/04/08
1K0
kubelet 状态上报的方式
分布式系统中服务端会通过心跳机制确认客户端是否存活,在 k8s 中,kubelet 也会定时上报心跳到 apiserver,以此判断该 node 是否存活,若 node 超过一定时间没有上报心跳,其状态会被置为 NotReady,宿主上容器的状态也会被置为 Nodelost 或者 Unknown 状态。kubelet 自身会定期更新状态到 apiserver,通过参数 --node-status-update-frequency 指定上报频率,默认是 10s 上报一次,kubelet 不止上报心跳信息还会上报自身的一些数据信息。
田飞雨
2019/12/18
3.2K0
Kubernetes存储详解
由于容器默认情况下rootfs系统树没有一个与之对应的存储设备,因此可以认为容器中任何文件操作都是临时性的,这样的设计带来了两个问题,其一是如果容器因为某些原因被kubelet重启后,会丢失这些文件;其二是不同的容器之间无法共享文件。为了解决上述问题,Kubernetes 设计了卷(Volume) 这一抽象来管理容器中所有需要使用到的外部文件。
thierryzhou
2022/11/17
8410
Kubernetes存储详解
k8s调度器启动流程分析 | 视频文字稿
本文主要对 Kubernetes 调度器(v1.19.3版本)初始化启动过程进行分析。kube-scheduler 组件有很多可以配置的启动参数,其核心也是通过 cobra 开发的一个 CLI 工具,所以要掌握 kube-scheduler 的启动配置,需要我们对 cobra 有一个基本的了解,kube-scheduler 主要有两种类型的配置参数:
我是阳明
2021/03/17
7380
k8s实践(十五):Centos7.6部署k8s v1.16.4高可用集群(主备模式)
本文采用kubeadm方式搭建高可用k8s集群,k8s集群的高可用实际是k8s各核心组件的高可用,这里使用主备模式,架构如下:
loong576
2020/01/14
1.9K0
k8s实践(十五):Centos7.6部署k8s v1.16.4高可用集群(主备模式)
部署高可用 kubernetes 集群
kubernetes 虽然具有故障自愈和容错能力,但某些组件的异常会导致整个集群不可用,生产环境中将其部署为高可用还是非常有必要的,本文会介绍如何构建一个高可用的 Kubernetes 集群。kuber-controller-manager 和 kube-scheduler 的高可用官方已经实现了,都是通过 etcd 全局锁进行选举实现的,etcd 是一个分布式,强一致的(满足 CAP 的 CP)KV 存储系统,其天然具备高可用。而 apiserver 作为整个系统的核心,所有对数据的修改操作都是通过 apiserver 间接操作 etcd 的,所以 apiserver 的高可用实现是比较关键的。
田飞雨
2019/12/18
1.3K0
k8s代码走读---kube-controller-manager
今天开始走读 k8s 的代码,走读代码还是非常有意思的。首先选择的是 controller-manager 这个组件。这几天也看了《kubernetes源码剖析》的前两章,这本书还是不错的,推荐大家阅读。前面两章主要讲解什么是 kubernets,它的来历,它的架构,它的代码结构,以及它的编译过程。读代码首先要了解它的架构,其次要知道它的代码结构。代码结构就是整个代码目录的组成,那个目录中的文件是做什么的。看了这本书的前面 2 章,对它的架构和结构就会有比较清晰的认识。
黑光技术
2020/08/11
1.1K0
k8s代码走读---kube-controller-manager
如何批量删除k8s资源对象
在云平台开发、中间件容器化时,经常会遇到批量删除k8s资源对象的需求,下面记录一下kubectl和golang发送删除pvc、pv、pod请求的例子,便于后续学习查阅
我的小碗汤
2019/10/11
4.3K0
如何批量删除k8s资源对象
【K8s源码品读】008:Phase 1 - kube-scheduler - 初探调度的启动流程与算法
聚焦目标 理解kube-scheduler启动的流程 目录 kube-scheduler的启动 Scheduler的注册 了解一个最简单的算法NodeName run // kube-scheduler 类似于kube-apiserver,是个常驻进程,查看其对应的Run函数 func runCommand(cmd *cobra.Command, opts *options.Options, registryOptions ...Option) error { // 根据入参,返回配置cc与调度sch
junedayday
2021/08/05
3240
Kubernetes Informer基本原理
不论是 k8s 自身组件,还是自己编写 controller,都需要通过 apiserver 监听 etcd 事件来完成自己的控制循环逻辑。
政采云前端团队
2024/01/30
5890
Kubernetes Informer基本原理
Kubernetes Controller高可用诡异的15mins超时
作者杜杨浩,腾讯云高级工程师,热衷于开源、容器和Kubernetes。目前主要从事镜像仓库以及云原生架构相关研发工作。 前言 节点宕机是生产环境无法规避的情况,生产环境必须适配这种情况,保障即便宕机后,服务依旧可用。而Kubernetes内部实现的Leader Election Mechanism很好的解决了controller高可用的问题。但是某一次生产环境的测试发现controller在节点宕机后并没有马上(在规定的分布式锁释放时间后)实现切换,本文对这个问题进行了详尽的描述,分析,并在最后给出了解决
腾讯云原生
2020/09/15
2.9K0
TiDB Operator 源码阅读 (四) 组件的控制循环
上篇文章中,我们介绍了 TiDB Operator 的组件生命周期管理的编排,以 TiDBCluster Controller 为例介绍 Controller Manager 的实现。TiDBCluster Controller 负责了 TiDB 主要组件的生命周期管理,TiDB 各个组件的 Member Manager 封装了对应具体的生命周期管理逻辑。在上篇文章中,我们描述了一个抽象的组件生命周期管理的实现,本文中,我们将以 PD 为例详细介绍组件生命周期管理的实现过程和相关代码,并且以 PD 的介绍为基础,介绍其他组件的部分差异。
PingCAP
2021/06/30
7630
kube-scheduler 源码分析
Kube-scheduler 是 kubernetes 的核心组件之一,也是所有核心组件之间功能比较单一的,其代码也相对容易理解。kube-scheduler 的目的就是为每一个 pod 选择一个合适的 node,整体流程可以概括为三步,获取未调度的 podList,通过执行一系列调度算法为 pod 选择一个合适的 node,提交数据到 apiserver,其核心则是一系列调度算法的设计与执行。
田飞雨
2019/12/15
6210
推荐阅读
相关推荐
client-go实战之十二:选主(leader-election)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验