Tim Wang Tech Blog

使用client-go在Kubernetes中进行leader election

本文是 leader-election-in-kubernetes-using-client-go的中文翻译版本,内容有删减

如果您想了解 Kubernetes 中leader election的工作原理,那么希望本文能对您有所帮助。在本文中,我们将讨论高可用系统中leader election的概念,并探讨kubernetes/client-go库,以了解其在 Kubernetes 控制器中的应用。

近年来,“高可用性”一词因可靠系统和基础设施需求的增加而变得流行起来。在分布式系统中,高可用性通常涉及最大化运行时间和系统容错。高可用性中通常采用的一种做法是使用冗余来避免单点故障。为冗余做好系统和服务的准备工作可能只需要在负载均衡器后面部署更多的副本。虽然这样的配置对许多应用程序来说可能有效,但有些用例需要在副本之间进行仔细的协调才能使系统正确运行。

一个很好的例子是当一个 Kubernetes 控制器被部署为多个实例时。为了防止任何意外的行为,leader election过程必须确保在副本之间选出一个leader,并且该leader是唯一主动协调集群的实例。其他实例应该保持不活动,但随时准备接管leader实例的工作,以防其失败。

在 Kubernetes 中,leader election的过程很简单。它始于创建一个锁对象,leader会定期更新当前时间戳,以通知其他副本其领导权。这个锁对象可以是一个LeaseConfigMap或者Endpoint,它还保存了当前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-kindNot sure what to do next? 😅  Check out https://kind.sigs.k8s.io/docs/user/quick-start/

我们将使用的示例应用程序可以在k8s-leader-election,它使用kubernetes/client-go实现leader election。让我们在我们的集群上运行应用程序:

# 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

这将创建一个包含3个pod(副本)的deployment。如果您等待几秒钟,您应该会看到它们处于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 election过程的一部分创建的Lease锁对象。

$ kubectl describe lease my-leaseName:         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 pod是leaderelection-56457bc5c-fn725。让我们通过查看我们的pod日志来验证这一点。

# leader pod
$ kubectl logs leaderelection-56457b6c5c-fn725I1023 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来模拟崩溃,并检查Lease对象的内容来判断是否选举出了新的leader

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

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 election过程。

The runLeaderElection function is where we initiate the leader election loop by calling RunOrDie . We pass a LeaderElectionConfig to it:

runLeaderElection函数是我们通过调用RunOrDie来启动leader election循环的地方。我们向它传递一个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-go中RunOrDie的实现。

https://github.com/kubernetes/client-go/blob/master/tools/leaderelection/leaderelection.go#L218-L227

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

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

这个方法负责运行leader election循环。它首先尝试获取锁(使用le.acquire)。在成功后,它运行我们之前配置的OnStartedLeading回调并定期更新租约。在无法获取锁时,它只是运行OnStoppedLeading回调并返回。

这里最重要的部分是acquirerenew方法中对tryAcquireOrRenew的调用,它包含了锁机制的核心逻辑。

优化锁(并发控制)[Optimistic locking (concurrency control)]

leader election 过程利用Kubernetes的原子性,确保不会有两个实例同时获取到Lease中的锁,每次更新 Lease(续订或获取),Kubernetes 也会更新其上的 resourceVersion 字段。当另一个进程尝试同时更新 Lease 时,Kubernetes 会检查更新对象的 resourceVersion 字段是否与当前对象匹配——如果不匹配,则更新失败,从而防止并发问题!

在这篇文章中,我们介绍了leader election的概念以及它对分布式系统的高可用性的重要性。我们看了看Kubernetes是如何使用Lease锁来实现的,并尝试使用kubernetes/client-go库。此外,我们还尝试了解Kubernetes如何使用原子操作和乐观锁方法来防止并发问题。