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

Kubernetes 如何实现组件高可用

作者头像
CS实验室
发布2022-08-01 20:17:31
6800
发布2022-08-01 20:17:31
举报
文章被收录于专栏:CS实验室

在 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
复制
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
复制
$ 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
复制
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
复制
$ 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
复制
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
复制
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
复制
 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 删除。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Leader 选举
    • 抢占锁
      • 释放锁
      • Controller 中如何使用
      • 总结
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档