k8s client-go中Leader选举实现
文章目录介绍Kubernetes的leader选举代码深潜乐观锁定(并发控制)总结介绍近年来,随着对可靠系统和基础设施的需求增加,"高可用性"一词越来越流行。在分布式系统中,高可用性通常涉及最大化正常运行时间和使系统容错。在高可用性中,一个常见的实践是使用冗余来最小化单点故障。为系统和服务准备冗余可以非常简单,只需在负载平衡器后面部署更多的冗余副本即可。尽管这样的配置可能适用于许多应用程序,但有些
介绍
近年来,随着对可靠系统和基础设施的需求增加,"高可用性"一词越来越流行。在分布式系统中,高可用性通常涉及最大化正常运行时间和使系统容错。
在高可用性中,一个常见的实践是使用冗余来最小化单点故障。为系统和服务准备冗余可以非常简单,只需在负载平衡器后面部署更多的冗余副本即可。尽管这样的配置可能适用于许多应用程序,但有些用例需要跨副本进行仔细的协调,以使系统正常工作。
Kubernetes控制器作为多个实例部署就是一个很好的例子。为了防止任何意想不到的行为,leader选举过程必须确保leader在副本中被选举出来,并且是唯一积极协调集群的leader。其他实例应该保持非活动状态,但在leader实例失败时准备接管。
Kubernetes的leader选举
Kubernetes
的leader
选举过程很简单。它从创建锁对象开始,leader
定期更新当前时间戳,以通知其他副本关于它的leader
。这个锁对象可以是Lease
、ConfigMap
或Endpoint
,也持有当前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
这将创建一个带有3
个pod
(副本)的部署。如果等待几秒钟,应该看到它们处于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 pod
是leadership 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-name
和lease-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-go
中RunOrDie
的实现。
// 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/
更多推荐
所有评论(0)