k8s client-go中Leader选举实现

介绍

近年来,随着对可靠系统和基础设施的需求增加,"高可用性"一词越来越流行。在分布式系统中,高可用性通常涉及最大化正常运行时间和使系统容错。

在高可用性中,一个常见的实践是使用冗余来最小化单点故障。为系统和服务准备冗余可以非常简单,只需在负载平衡器后面部署更多的冗余副本即可。尽管这样的配置可能适用于许多应用程序,但有些用例需要跨副本进行仔细的协调,以使系统正常工作。

Kubernetes控制器作为多个实例部署就是一个很好的例子。为了防止任何意想不到的行为,leader选举过程必须确保leader在副本中被选举出来,并且是唯一积极协调集群的leader。其他实例应该保持非活动状态,但在leader实例失败时准备接管。

Kubernetes的leader选举

Kubernetesleader选举过程很简单。它从创建锁对象开始,leader定期更新当前时间戳,以通知其他副本关于它的leader。这个锁对象可以是LeaseConfigMapEndpoint,也持有当前leader的身份。如果leader未能在给定的时间间隔内更新时间戳,则认为它已经崩溃,此时非活动副本通过用自己的身份更新锁来竞争获得leader。成功获得锁的pod就会成为新的leader
在这里插入图片描述

在处理任何代码之前,先看看这个过程是如何运行的! 第一步是建立一个本地Kubernetes集群。我将使用KinD,但你也可以使用你自己所使用的k8s集群。

$ kind create cluster
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.21.1) 🖼
 ✓ Preparing nodes 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Set kubectl context to "kind-kind"
You can now use your cluster with:
kubectl cluster-info --context kind-kind
Not sure what to do next? 😅  Check out https://kind.sigs.k8s.io/docs/user/quick-start/

使用的示例应用程序可以在这里找到。它使用kubernetes/client-go进行领导人选举。让我们在集群上安装应用程序:

示例:https://github.com/mayankshah1607/k8s-leader-election

client-go: https://github.com/kubernetes/client-go

# Setup required permissions for creating/getting Lease objects
$ kubectl apply -f rbac.yaml
serviceaccount/leaderelection-sa created
role.rbac.authorization.k8s.io/leaderelection-role created
rolebinding.rbac.authorization.k8s.io/leaderelection-rolebinding created

# Create deployment
$ kubectl apply -f deploy.yaml
deployment.apps/leaderelection created

这将创建一个带有3pod(副本)的部署。如果等待几秒钟,应该看到它们处于Running状态。

❯ kubectl get pods
NAME                              READY   STATUS    RESTARTS   AGE
leaderelection-6d5b456c9d-cfd2l   1/1     Running   0          19s
leaderelection-6d5b456c9d-n2kx2   1/1     Running   0          19s
leaderelection-6d5b456c9d-ph8nj   1/1     Running   0          19s

运行pod之后,尝试查看它们作为leader选举过程的一部分创建的Lease锁对象。

$ kubectl describe lease my-lease
Name:         my-lease
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  coordination.k8s.io/v1
Kind:         Lease
Metadata:
...
Spec:
  Acquire Time:            2021-10-23T06:51:50.605570Z
  Holder Identity:         leaderelection-56457b6c5c-fn725
  Lease Duration Seconds:  15
  Lease Transitions:       0
  Renew Time:              2021-10-23T06:52:45.309478Z
  

根据这个,我们目前的leader podleadership election-56457bc5c-fn725。通过查看pod日志来验证这一点。

# leader pod
$ kubectl logs leaderelection-56457b6c5c-fn725
I1023 06:51:50.605439       1 leaderelection.go:248] attempting to acquire leader lease default/my-lease...
I1023 06:51:50.630111       1 leaderelection.go:258] successfully acquired lease default/my-lease
I1023 06:51:50.630141       1 main.go:57] still the leader!
I1023 06:51:50.630245       1 main.go:36] doing stuff...

# inactive pods
$ kubectl logs leaderelection-56457b6c5c-n857k
I1023 06:51:55.400797       1 leaderelection.go:248] attempting to acquire leader lease default/my-lease...
I1023 06:51:55.412780       1 main.go:60] new leader is %sleaderelection-56457b6c5c-fn725

# inactive pod
$ kubectl logs leaderelection-56457b6c5c-s48kx
I1023 06:51:52.905451       1 leaderelection.go:248] attempting to acquire leader lease default/my-lease...
I1023 06:51:52.915618       1 main.go:60] new leader is %sleaderelection-56457b6c5c-fn725

尝试删除leader pod来模拟崩溃,如果新leader当选,检查Lease对象

代码深潜

项目的代码查看以下链接

https://github.com/mayankshah1607/k8s-leader-election

这里的基本思想是使用分布式锁定机制来决定哪个进程成为leader。获得锁的进程将执行所需的任务。主要函数是应用程序的入口。在这里,我们创建一个对锁对象的引用,并启动leader选举循环。

func main() {
 var (
  leaseLockName      string
  leaseLockNamespace string
  podName            = os.Getenv("POD_NAME")
 )
 flag.StringVar(&leaseLockName, "lease-name", "", "Name of lease lock")
 flag.StringVar(&leaseLockNamespace, "lease-namespace", "default", "Name of lease lock namespace")
 flag.Parse()

 if leaseLockName == "" {
  klog.Fatal("missing lease-name flag")
 }
 if leaseLockNamespace == "" {
  klog.Fatal("missing lease-namespace flag")
 }

 config, err := rest.InClusterConfig()
 client = clientset.NewForConfigOrDie(config)

 if err != nil {
  klog.Fatalf("failed to get kubeconfig")
 }

 ctx, cancel := context.WithCancel(context.Background())
 defer cancel()

 lock := getNewLock(leaseLockName, podName, leaseLockNamespace)
 runLeaderElection(lock, ctx, podName)
}

我们首先解析lease-namelease-namespace标志,以获得副本必须使用的锁对象的名称和名称空间。POD_NAME环境变量的值(在deploy.yaml manifest)将用于识别Lease对象中的leader。最后,我们使用这些参数创建一个锁对象来启动leader选举过程。

deploy.yaml: https://github.com/mayankshah1607/k8s-leader-election/blob/master/deploy.yaml#L26

runLeaderElection函数是我们通过调用RunOrDie来启动leader选举循环的地方。我们传递一个LeaderElectionConfig给它:

func runLeaderElection(lock *resourcelock.LeaseLock, ctx context.Context, id string) {
 leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
  Lock:            lock,
  ReleaseOnCancel: true,
  LeaseDuration:   15 * time.Second,
  RenewDeadline:   10 * time.Second,
  RetryPeriod:     2 * time.Second,
  Callbacks: leaderelection.LeaderCallbacks{
   OnStartedLeading: func(c context.Context) {
    doStuff()
   },
   OnStoppedLeading: func() {
    klog.Info("no longer the leader, staying inactive.")
   },
   OnNewLeader: func(current_id string) {
    if current_id == id {
     klog.Info("still the leader!")
     return
    }
    klog.Info("new leader is %s", current_id)
   },
  },
 })
}

现在,让我们看看在client-goRunOrDie的实现。

// RunOrDie starts a client with the provided config or panics if the config
// fails to validate. RunOrDie blocks until leader election loop is
// stopped by ctx or it has stopped holding the leader lease
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)
}

它使用我们传递给它的LeaderElectorConfig创建一个*LeaderElector,并调用它的Run方法:

// Run starts the leader election loop. Run will not return
// before leader election loop is stopped by ctx or it has
// stopped holding the leader lease
func (le *LeaderElector) Run(ctx context.Context) {
 defer runtime.HandleCrash()
 defer func() {
  le.config.Callbacks.OnStoppedLeading()
 }()

 if !le.acquire(ctx) {
  return // ctx signalled done
 }
 ctx, cancel := context.WithCancel(ctx)
 defer cancel()
 go le.config.Callbacks.OnStartedLeading(ctx)
 le.renew(ctx)
}

这种方法负责leader选举循环的运行。它首先尝试获取锁(使用le.acquire)。一旦成功,它将运行我们之前配置的OnStartedLeading回调,并定期更新lease。当获取锁失败时,它只是运行OnStoppedLeading回调并返回。

获取和更新方法的实现中最重要的部分是调用tryAcquireOrRenew,它持有锁定机制的核心逻辑。

acquire: https://github.com/kubernetes/client-go/blob/56656ba0e04ff501549162385908f5b7d14f5dc8/tools/leaderelection/leaderelection.go#L243

renew: https://github.com/kubernetes/client-go/blob/56656ba0e04ff501549162385908f5b7d14f5dc8/tools/leaderelection/leaderelection.go#L265

tryAcquireOrRenew: https://github.com/kubernetes/client-go/blob/56656ba0e04ff501549162385908f5b7d14f5dc8/tools/leaderelection/leaderelection.go#L317

乐观锁定(并发控制)

leader选举过程利用Kubernetes操作的原子性质来确保没有两个副本可以同时获得Lease(否则可能导致竞争条件和其他意外行为!)每当Lease被更新(更新或获得)时,Kubernetes也会更新其上的resourceVersion字段。当另一个进程同时尝试更新Lease时,Kubernetes检查被更新对象的resourceVersion字段是否与当前对象匹配——如果不匹配,更新失败,从而防止并发问题!

总结

在这篇博文中,讨论了leader选举的想法,以及为什么它对分布式系统的高可用性至关重要。研究了如何在Kubernetes中使用Lease锁实现这一点,并尝试使用Kubernetes/client-go库实现这一点。此外,我们还试图理解Kubernetes如何使用原子操作和乐观锁定方法防止并发性引起的问题。

还请注意,这篇文章中使用的代码不是一个生产可用的解决方案,而是用一种简单的方式仅仅用来演示leader选举的。

参考资料
[1]
参考: https://kubernetes.io/blog/2016/01/simple-leader-election-with-kubernetes//

[2]
参考: https://carlosbecker.com/posts/k8s-leader-election//

[3]
参考: https://taesunny.github.io/kubernetes/kubernetes-controllers-leader-election-with-go-library/

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寻花之梦~~

谢谢老板的支持和鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值