client-go实战之十二:选主(leader-election)

news2024/11/26 16:58:49

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

本篇概览

  • 本文是《client-go实战》系列的第十二篇,又有一个精彩的知识点在本章呈现:选主(leader-election)
  • 在解释什么是选主之前,咱们先来看一个场景(有真实适用场景的技术,学起来才有动力),如下图所示(稍后有详细说明)
    在这里插入图片描述

上图所描述的业务场景是个普通的controller应用:

  1. 右侧是人工操作,通过kubectl命令修改了service资源
  2. 左侧的业务应用订阅了service的变化,在收到service变更的事件后,对pod进行写操作(例如将收到事件的时间写入pod的label)
  • 以上的业务应用就是个很普通的controller,很简单,运行起来也没啥问题,但是,如果这个业务应用有多个实例呢?

多实例的问题

  • 所谓多个实例,就是同样的业务应用我们运行了多个进程(例如三个),为什么多个进程?同一个应用运行多个进程不是很正常么?横向扩容不就是多进程嘛
  • 多个进程运行的时候,如果service发生变化,那么每个进程都会去修改pod的label,这不是我们想要的(只要修改一次就行了)
  • 所以,如何解决这个问题呢?三个进程都是同一套代码,都会订阅service的变化,但是最终只修改一次pod
  • 经验丰富的您应该会想到分布式锁,三个进程去抢分布式锁,抢到的负责更新,没错,这是一个正确的解法
  • 但是,分布式锁需要引入相关组件吧,redis的setnx,或者mysql的乐观锁,这样就需要维护新的组件了
  • 其实这在kubernetes是个很典型的问题,毕竟pod多实例在kubernetes是常态了,所以当然也有官方的解法,页就是本文的主题:选主(leader-election)

选主(leader-election)

  • 说到这里您应该能理解选主的含义了:多个进程竞争某个key的leader,咱们可以把特定的代码放在竞争成功后再执行,由于同一时刻只有一个进程可以竞争成功,这就相当于在不引入额外组件的情况下,只用client-go就实现了分布式锁
  • 由于选主只是个特定的小知识点,本篇就没什么多余的理论要研究了,接下来直接开始实战,编码实现一个功能来说明选主的用法
  • 实战的业务需求如下
  1. 开发一个应用,该应用同时运行多个进程
  2. 当kubernetes的指定namespace下的service发生变化时,在pod的label中记录这个service的变化时间
  3. 每次serivce变化,pod的label只能修改一次(尽管此时有多个进程)
  • 让我们少些套路,多一点真诚,不说废话,直接开始动手实战吧

源码下载

  • 如果您不想编写代码,也可以从GitHub上直接下载,地址和链接信息如下表所示(https://github.com/zq2599/blog_demos):
名称链接备注
项目主页https://github.com/zq2599/blog_demos该项目在GitHub上的主页
git仓库地址(https)https://github.com/zq2599/blog_demos.git该项目源码的仓库地址,https协议
git仓库地址(ssh)git@github.com:zq2599/blog_demos.git该项目源码的仓库地址,ssh协议
  • 这个git项目中有多个文件夹,本篇的源码在leader-tutorials文件夹下,如下图黄框所示:
    在这里插入图片描述

提前了解选主的代码

  • 接下来会开发一个完整的controller应用,以此来说明选主功能
  • 如果您觉得完整应用的代码太多,懒得看,只想了解选主部分,那就在此提前将整个工程中选主相关的代码贴出来
  • 核心代码如下所示,先创建锁对象,就像分布式锁一样,总要有个key,然后执行leaderelection.RunOrDie方法参与选主,一旦有了结果,OnNewLeader方法会被回调,这时候通过自身id和leader的id比较就知道是不是自己了,另外,当OnStartedLeading被执行的时候,就意味着当前进程就是leader,并且可以立即开始执行只有leader才能做的事情了
// startLeaderElection 选主的核心逻辑代码
func startLeaderElection(ctx context.Context, clientset *kubernetes.Clientset, stop chan struct{}) {
	klog.Infof("[%s]创建选主所需的锁对象", processIndentify)
	// 创建锁对象
	lock := &resourcelock.LeaseLock{
		LeaseMeta: metav1.ObjectMeta{
			Name:      "leader-tutorials",
			Namespace: NAMESPACE,
		},
		Client: clientset.CoordinationV1(),
		LockConfig: resourcelock.ResourceLockConfig{
			Identity: processIndentify,
		},
	}
	klog.Infof("[%s]开始选主", processIndentify)
	// 启动选主操作
	leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
		Lock:            lock,
		ReleaseOnCancel: true,
		LeaseDuration:   10 * time.Second,
		RenewDeadline:   5 * time.Second,
		RetryPeriod:     2 * time.Second,
		Callbacks: leaderelection.LeaderCallbacks{
			OnStartedLeading: func(ctx context.Context) {
				klog.Infof("[%s]当前进程是leader,只有leader才能执行的业务逻辑立即开始", processIndentify)
				// 在这里写入选主成功的代码,
				// 就像抢分布式锁一样,当前进程选举成功的时候,这的代码就会被执行,
				// 所以,在这里填写抢锁成功的业务逻辑吧,本例中就是监听service变化,然后修改pod的label
				CreateAndStartController(ctx, clientset, &v1.Service{}, "services", NAMESPACE, stop)
			},
			OnStoppedLeading: func() {
				// 失去了leader时的逻辑
				klog.Infof("[%s]失去leader身份,不再是leader了", processIndentify)
				os.Exit(0)
			},
			OnNewLeader: func(identity string) {
				// 收到通知,知道最终的选举结果
				if identity == processIndentify {
					klog.Infof("[%s]选主结果出来了,当前进程就是leader", processIndentify)
					// I just got the lock
					return
				}
				klog.Infof("[%s]选主结果出来了,leader是 : [%s]", processIndentify, identity)
			},
		},
	})
}

实战:部署service和deployment

  • 首先请准备好k8s环境,这在《client-go实战之六:时隔两年,刷新版本继续实战》里面已有详细说明
  • 然后把本次实战所需的service和deployment部署好,- 所有要部署的内容我都集中在这个名为nginx-deployment-service.yaml脚本中了
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: client-go-tutorials
  name: nginx-deployment
  labels:
    app: nginx-app
    type: front-end
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-app
      type: front-end
  template:
    metadata:
      labels:
        app: nginx-app
        type: front-end
        # 这是第一个业务自定义label,指定了mysql的语言类型是c语言
        language: c
        # 这是第二个业务自定义label,指定了这个pod属于哪一类服务,nginx属于web类
        business-service-type: web
    spec:
      containers:
        - name: nginx-container
          image: nginx:latest
          resources:
            limits:
              cpu: "0.5"
              memory: 128Mi
            requests:
              cpu: "0.1"
              memory: 64Mi
---
apiVersion: v1
kind: Service
metadata:
  namespace: client-go-tutorials
  name: nginx-service
spec:
  type: NodePort
  selector:
    app: nginx-app
    type: front-end
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30011
  • 先执行以下命令创建namespace
kubectl create namespace client-go-tutorials
  • 再执行以下命令即可完成资源的创建
kubectl apply -f nginx-deployment-service.yaml
  • 来查看一下资源情况,如下图,service和pod都创建好了,准备工作完成,可以开始编码了
    在这里插入图片描述

编码:准备工程

  • 执行命令名为go mod init leader-tutorials,新建module
  • 确保您的goproxy是正常的
  • 执行命令go get k8s.io/client-go@v0.22.8,下载client-go的指定版本
  • 现在工程已经准备好了,接着就是具体的编码

编码:梳理

  • 咱们按照开发顺序开始写代码,如果您看过欣宸的《client-go实战》系列,此刻对使用client-go开发简易版controller应该很熟悉了,这里再简单提一下开发的流程
  1. 将controller完整的写出来,功能是监听service,一旦有变化就更新pod的label
  2. 在主控逻辑中,根据选主结果决定是否启动步骤1中的controller
  • 下面开始写代码

编码:controller

  • 新建controller.go文件
  • 在controller.go中增加常量和数据结构的定义
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"time"

	"k8s.io/klog/v2"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/fields"
	objectruntime "k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/cache"
	"k8s.io/client-go/util/workqueue"
)

const (
	LABLE_SERVICE_UPDATE_TIME = "service-update-time" // 这个label用来记录service的更新时间
)

// 自定义controller数据结构,嵌入了真实的控制器
type Controller struct {
	ctx       context.Context
	clientset *kubernetes.Clientset
	// 本地缓存,关注的对象都会同步到这里
	indexer cache.Indexer
	// 消息队列,用来触发对真实对象的处理事件
	queue workqueue.RateLimitingInterface
	// 实际运行运行的控制器
	informer cache.Controller
}
  • 然后是controller的套路代码,主要是从队列中不断获取数据并处理的逻辑

// processNextItem 不间断从队列中取得数据并处理
func (c *Controller) processNextItem() bool {
	// 注意,队列里面不是对象,而是key,这是个阻塞队列,会一直等待
	key, quit := c.queue.Get()
	if quit {
		return false
	}
	// Tell the queue that we are done with processing this key. This unblocks the key for other workers
	// This allows safe parallel processing because two pods with the same key are never processed in
	// parallel.
	defer c.queue.Done(key)

	// 注意,这里的syncToStdout应该是业务代码,处理对象变化的事件
	err := c.updatePodsLabel(key.(string))

	// 如果前面的业务逻辑遇到了错误,就在此处理
	c.handleErr(err, key)

	// 外面的调用逻辑是:返回true就继续调用processNextItem方法
	return true
}

// runWorker 这是个无限循环,不断地从队列取出数据处理
func (c *Controller) runWorker() {
	for c.processNextItem() {
	}
}

// handleErr 如果前面的业务逻辑执行出现错误,就在此集中处理错误,本例中主要是重试次数的控制
func (c *Controller) handleErr(err error, key interface{}) {
	if err == nil {
		// Forget about the #AddRateLimited history of the key on every successful synchronization.
		// This ensures that future processing of updates for this key is not delayed because of
		// an outdated error history.
		c.queue.Forget(key)
		return
	}

	// 如果重试次数未超过5次,就继续重试
	if c.queue.NumRequeues(key) < 5 {
		klog.Infof("Error syncing pod %v: %v", key, err)

		// Re-enqueue the key rate limited. Based on the rate limiter on the
		// queue and the re-enqueue history, the key will be processed later again.
		c.queue.AddRateLimited(key)
		return
	}

	// 代码走到这里,意味着有错误并且重试超过了5次,应该立即丢弃
	c.queue.Forget(key)
	// 这种连续五次重试还未成功的错误,交给全局处理逻辑
	runtime.HandleError(err)
	klog.Infof("Dropping pod %q out of the queue: %v", key, err)
}

// Run 开始常规的控制器模式(持续响应资源变化事件)
func (c *Controller) Run(threadiness int, stopCh chan struct{}) {
	defer runtime.HandleCrash()

	// Let the workers stop when we are done
	defer c.queue.ShutDown()
	klog.Info("Starting Pod controller")

	go c.informer.Run(stopCh)

	// Wait for all involved caches to be synced, before processing items from the queue is started
	// 刚开始启动,从api-server一次性全量同步所有数据
	if !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) {
		runtime.HandleError(fmt.Errorf("timed out waiting for caches to sync"))
		return
	}

	// 支持多个线程并行从队列中取得数据进行处理
	for i := 0; i < threadiness; i++ {
		go wait.Until(c.runWorker, time.Second, stopCh)
	}

	<-stopCh
	klog.Info("Stopping Pod controller")
}

  • 从上述代码可见,监听的资源发生变化时,调用的是updatePodsLabel方法,此方法的作用就是查找该namespace下的所有pod,依次用patch的方式更新pod的label

// updatePodsLabel 这是业务逻辑代码,一旦service发生变化,就修改pod的label,将service的变化事件记录进去
func (c *Controller) updatePodsLabel(key string) error {
	// 开始进入controller的业务逻辑
	klog.Infof("[%s]这里是controller的业务逻辑,key [%s]", processIndentify, key)
	// 从本地缓存中取出完整的对象
	_, exists, err := c.indexer.GetByKey(key)
	if err != nil {
		klog.Errorf("[%s]根据key[%s]从本地缓存获取对象失败 : %v", processIndentify, key, err)
		return err
	}

	if !exists {
		klog.Infof("[%s]对象不存在,key [%s],这是个删除事件", processIndentify, key)
	} else {
		klog.Infof("[%s]对象存在,key [%s],这是个新增或修改事件", processIndentify, key)
	}

	// 代码走到这里,表示监听的对象发生了变化,
	// 按照业务设定,需要修改pod的指定label,
	// 准备好操作pod的接口
	podInterface := c.clientset.CoreV1().Pods(NAMESPACE)

	// 远程取得最新的pod列表
	pods, err := podInterface.List(c.ctx, metav1.ListOptions{})

	if err != nil {
		klog.Errorf("[%s]远程获取pod列表失败 : %v", processIndentify, err)
		return err
	}

	// 将service的变化时间写入pod的指定label,这里先获取当前时间
	updateTime := time.Now().Format("20060102150405")
	// 准备patch对象
	patchData := map[string]interface{}{
		"metadata": map[string]interface{}{
			"labels": map[string]interface{}{
				LABLE_SERVICE_UPDATE_TIME: updateTime,
			},
		},
	}

	// 转为byte数组,稍后更新pod的时候,就用这个数组进行patch更新
	patchByte, _ := json.Marshal(patchData)

	// 遍历所有pod,逐个更新label
	for _, pod := range pods.Items {
		podName := pod.Name
		klog.Infof("[%s]正在更新pod [%s]", processIndentify, podName)

		_, err := podInterface.Patch(c.ctx, podName, types.MergePatchType, patchByte, metav1.PatchOptions{})

		// 失败就返回,会导致整体重试
		if err != nil {
			klog.Infof("[%s]更新pod [%s]失败, %v", processIndentify, podName, err)
			return err
		}

		klog.Infof("[%s]更新pod [%s]成功", processIndentify, podName)
	}

	return nil
}
  • 到这里,controller的代码已经写得七七八八了,还剩创建controller对象以及运行informer的代码,这里将它们集中封装在一个方法中,一旦这个方法被调用,就意味着controller会被创建,然后监听service变化再更新pod的label的逻辑就会被执行
// CreateAndStartController 为了便于外部使用,这里将controller的创建和启动封装在一起
func CreateAndStartController(ctx context.Context, clientset *kubernetes.Clientset, objType objectruntime.Object, resource string, namespace string, stopCh chan struct{}) {
	// ListWatcher用于获取数据并监听资源的事件
	podListWatcher := cache.NewListWatchFromClient(clientset.CoreV1().RESTClient(), resource, NAMESPACE, fields.Everything())

	// 限速队列,里面存的是有事件发生的对象的身份信息,而非对象本身
	queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())

	// 创建本地缓存并对指定类型的资源开始监听
	// 注意,如果业务上有必要,其实可以将新增、修改、删除等事件放入不同队列,然后分别做针对性处理,
	// 但是,controller对应的模式,主要是让status与spec达成一致,也就是说增删改等事件,对应的都是查到实际情况,令其与期望情况保持一致,
	// 因此,多数情况下增删改用一个队列即可,里面放入变化的对象的身份,至于处理方式只有一种:查到实际情况,令其与期望情况保持一致
	indexer, informer := cache.NewIndexerInformer(podListWatcher, objType, 0, cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) {
			key, err := cache.MetaNamespaceKeyFunc(obj)
			if err == nil {
				// 再次注意:这里放入队列的并非对象,而是对象的身份,作用是仅仅告知消费方,该对象有变化,
				// 至于有什么变化,需要消费方自行判断,然后再做针对性处理
				queue.Add(key)
			}
		},
		UpdateFunc: func(old interface{}, new interface{}) {
			key, err := cache.MetaNamespaceKeyFunc(new)
			if err == nil {
				queue.Add(key)
			}
		},
		DeleteFunc: func(obj interface{}) {
			key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
			if err == nil {
				queue.Add(key)
			}
		},
	}, cache.Indexers{})

	controller := &Controller{
		ctx:       ctx,
		clientset: clientset,
		informer:  informer,
		indexer:   indexer,
		queue:     queue,
	}

	go controller.Run(1, stopCh)
}

编码:主控程序(选主逻辑也在里面)

  • 本文是讲选主(leader-election)的,前面做了这么多铺垫,主角该上场了,新建main.go文件
  • 定义常量,以及全局变量
package main

import (
	"context"
	"flag"
	"os"
	"path/filepath"
	"time"

	"github.com/google/uuid"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/tools/leaderelection"
	"k8s.io/client-go/tools/leaderelection/resourcelock"
	"k8s.io/client-go/util/homedir"
	"k8s.io/klog/v2"
)

const (
	NAMESPACE = "client-go-tutorials"
)

// 用于表明当前进程身份的全局变量,目前用的是uuid
var processIndentify string
  • 先把套路的代码写了,就是client-go初始化的那部分,以及main方法,里面是整个程序的启动和业务调用流程,可见选主有关的代码都放在名为startLeaderElection的方法中
// initOrDie client有关的初始化操作
func initOrDie() *kubernetes.Clientset {
	klog.Infof("[%s]开始初始化kubernetes客户端相关对象", processIndentify)
	var kubeconfig *string
	var master string

	// 试图取到当前账号的家目录
	if home := homedir.HomeDir(); home != "" {
		// 如果能取到,就把家目录下的.kube/config作为默认配置文件
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
		master = ""
	} else {
		// 如果取不到,就没有默认配置文件,必须通过kubeconfig参数来指定
		flag.StringVar(kubeconfig, "kubeconfig", "", "absolute path to the kubeconfig file")
		flag.StringVar(&master, "master", "", "master url")
		flag.Parse()
	}

	config, err := clientcmd.BuildConfigFromFlags(master, *kubeconfig)
	if err != nil {
		klog.Fatal(err)
	}

	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		klog.Fatal(err)
	}
	klog.Infof("[%s]kubernetes客户端相关对象创建成功", processIndentify)
	return clientset
}


func main() {
	// 一次性确定当前进程身份
	processIndentify = uuid.New().String()

	// 准备一个带cancel的context,这样在主程序退出的时候,可以将停止的信号传递给业务
	ctx, cancel := context.WithCancel(context.Background())
	// 这个是用来停止controller的
	stop := make(chan struct{})

	// 主程序结束的时候,下面的操作可以将业务逻辑都停掉
	defer func() {
		close(stop)
		cancel()
	}()

	// 初始化clientSet配置,因为是启动阶段,所以必须初始化成功,否则进程退出
	clientset := initOrDie()

	// 在一个新的协程中执行选主逻辑,以及选主成功的后的逻辑
	go startLeaderElection(ctx, clientset, stop)

	// 这里可以继续做其他事情
	klog.Infof("选主的协程已经在运行,接下来可以执行其他业务 [%s]", processIndentify)

	select {}
}
  • 最后是选主的代码,如下所示,先创建锁对象,就像分布式锁一样,总要有个key,然后执行leaderelection.RunOrDie方法参与选主,一旦有了结果,OnNewLeader方法会被回调,这时候通过自身id和leader的id比较就知道是不是自己了,另外,当OnStartedLeading被执行的时候,就意味着当前进程就是leader,并且可以立即开始执行只有leader才能做的事情了
// startLeaderElection 选主的核心逻辑代码
func startLeaderElection(ctx context.Context, clientset *kubernetes.Clientset, stop chan struct{}) {
	klog.Infof("[%s]创建选主所需的锁对象", processIndentify)
	// 创建锁对象
	lock := &resourcelock.LeaseLock{
		LeaseMeta: metav1.ObjectMeta{
			Name:      "leader-tutorials",
			Namespace: NAMESPACE,
		},
		Client: clientset.CoordinationV1(),
		LockConfig: resourcelock.ResourceLockConfig{
			Identity: processIndentify,
		},
	}
	klog.Infof("[%s]开始选主", processIndentify)
	// 启动选主操作
	leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
		Lock:            lock,
		ReleaseOnCancel: true,
		LeaseDuration:   10 * time.Second,
		RenewDeadline:   5 * time.Second,
		RetryPeriod:     2 * time.Second,
		Callbacks: leaderelection.LeaderCallbacks{
			OnStartedLeading: func(ctx context.Context) {
				klog.Infof("[%s]当前进程是leader,只有leader才能执行的业务逻辑立即开始", processIndentify)
				// 在这里写入选主成功的代码,
				// 就像抢分布式锁一样,当前进程选举成功的时候,这的代码就会被执行,
				// 所以,在这里填写抢锁成功的业务逻辑吧,本例中就是监听service变化,然后修改pod的label
				CreateAndStartController(ctx, clientset, &v1.Service{}, "services", NAMESPACE, stop)
			},
			OnStoppedLeading: func() {
				// 失去了leader时的逻辑
				klog.Infof("[%s]失去leader身份,不再是leader了", processIndentify)
				os.Exit(0)
			},
			OnNewLeader: func(identity string) {
				// 收到通知,知道最终的选举结果
				if identity == processIndentify {
					klog.Infof("[%s]选主结果出来了,当前进程就是leader", processIndentify)
					// I just got the lock
					return
				}
				klog.Infof("[%s]选主结果出来了,leader是 : [%s]", processIndentify, identity)
			},
		},
	})
}
  • 上述代码中,请注意LeaderElectionConfig对象的几个重要字段,例如LeaseDuration、RenewDeadline、RetryPeriod这些,是和选主时候的续租、超时、重试相关,需要按照您的实际网络情况进行调整
  • 现在代码写完了,可以开始验证了

验证

  • 这里捋一下验证的步骤
  1. 构建项目,生产二进制文件
  2. 执行此二进制文件,启动三个进程
  3. 观察日志,应该有一个进程选举成功,另外两个只会在日志输出选主结果
  4. 修改service资源,再去观察日志,发现leader进程会输出日志,再检查pod的label,发现已经修改
  5. 用ctrl+C命令将leader进程退出,可见另外两个进程会有一个成为新的leader
  6. 再次修改service资源,新的leader会负责更新pod的label
  • 接下来开始操作
  1. 执行命令go build,对当前工程进行编译构建,得到二进制文件leader-tutorials
  2. 打开三个终端窗口,输入同样的命令./leader-tutorials,选主成功的进程日志如下,之前操作过的残留,所以没有一开始就选主成功,而是等了几秒后才成为leader,一旦成为leader,全量同步service会触发一次pod的更新操作
    在这里插入图片描述
  • 再去看另外两个进程的日志,可见已经识别到leader的身份,于是就没有执行controller的逻辑
    在这里插入图片描述
  • 现在去修改service,用命令kubectl edit service nginx-service -n client-go-tutorials编辑,我这里是给service增加了一个label,如下图所示
    在这里插入图片描述
  • 此刻,leader进程会监听到service变化,下图黄色箭头以下的内容就是处理pod的日志
    在这里插入图片描述
  • 去看另外两个进程的日志,不会有任何变化,因为controller都没有
  • 执行以下命令查看pod的修改情况(注意pod的名字要从您自己的环境复制)
kubectl describe pod nginx-deployment-78f6b696d9-cr47w -n client-go-tutorials
  • 可以看到pod的label有变化,如下图黄色箭头所示,这和上面的leader日志的时间是一致的
    在这里插入图片描述
  • 目前leader进程工作正常,再来试试leader进程退出后的情况,用ctrl+C终止leader进程
  • 再去看另外两个进程的日志,发现其中一个成功成为新的leader
    在这里插入图片描述
  • 验证完成,都符合预期
  • 至此,client-go的选主功能实战就完成了,如果您在寻找kubernetes原生的分布式锁方案,希望本篇能给您一些参考

你不孤单,欣宸原创一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 数据库+中间件系列
  6. DevOps系列

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/872592.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Keepalived源码安装

文章目录 Keepalived源码安装安装准备缺少OpenSSL解决方法 Keepalived 源码安装 安装准备 tar zxf keepalived-2.2.8.tar.gz /root/ ll drwxrwxr-x. 10 1000 1000 4096 Aug 9 18:29 keepalived-2.2.8 #进入目录执行以下命令查看帮助 ./configure --help #重要编译参数 -…

QT学习笔记-oracle oci数据库驱动交叉编译并移植到ARM开发板

QT学习笔记-oracle oci数据库驱动交叉编译并移植到RK3568ARM开发板 0、背景1、搭建交叉编译环境2、交叉编译过程3、把数据库驱动部署到目标系统中 0、背景 在上一文《QT学习笔记-QT安装oracle oci驱动》中介绍了在Windows环境下使用QT访问oracle数据库时遇到驱动无法加载问题的…

kingbase:数据库启动状态

1 启停KingbaseES数据库 Linux下通过系统服务&#xff1a; root用户执行&#xff1a; service kingbase8d stop/start/restart ——注册服务的情况下 Linux下通过安装用户&#xff1a; 安装用户执行&#xff1a; sys_ctl stop/start/restart -D data路径 2 查看数据库当…

UI自动化环境的搭建(python+pycharm+selenium+chrome)

最近在做一些UI自动化的项目&#xff0c;为此从环境搭建来从0到1&#xff0c;希望能够帮助到你&#xff0c;同时也是自我的梳理。将按照如下进行开展&#xff1a; 1、python的下载、安装&#xff0c;python环境变量的配置。 2、pycharm开发工具的下载安装。 3、selenium的安装。…

【Java】一只小菜坤的编程题之旅【3】

文章目录 1丶判定是否互为字符重排2、杨辉三角3丶某公司的1个面试题&#xff08;字符串包含问题&#xff09; 1丶判定是否互为字符重排 这个题我们用一个非常简单的思想就能实现&#xff0c;我们先将字符串转换为字符数组&#xff0c;然后对字符数组进行排序&#xff0c;然后再…

Codeforces Round 893 (Div. 2)ABC

Codeforces Round 892 (Div. 2) 目录 A. United We Stand题目大意思路代码 B. Olya and Game with Arrays题目大意思路代码 C. Another Permutation Problem题目大意思路代码 A. United We Stand 题目大意 给你一个数组&#xff0c;把这个数组分成两个数组a和b&#xff0c;使…

03_013内存分配api以及页表详解

前言 之前文章中物理ram中的最小单位一直用页来表示 这次又描述的详细了点 物理ram的最小单位 有的地方叫 块,框,页帧 在虚拟空间中最小单位也叫页 需要好好区分 不过后来想想管你虚拟页还是物理ram页 都存在物理ram上 都能想成一 一对应的关系 所以大家都叫页好像也行 内存分…

【Unity3D】Shader Graph节点

1 前言 Shader Graph 16.0.3 中有 208 个 Node&#xff08;节点&#xff09;&#xff0c;本文梳理了 Shader Graph 中大部分 Node 的释义&#xff0c;官方介绍详见→Node-Library。 Shader Graph 通过图像的形式表达了顶点变换和片元着色流程&#xff0c;其背后都是一些列的数学…

保持城市天际线(力扣)贪心 JAVA

给你一座由 n x n 个街区组成的城市&#xff0c;每个街区都包含一座立方体建筑。给你一个下标从 0 开始的 n x n 整数矩阵 grid &#xff0c;其中 grid[r][c] 表示坐落于 r 行 c 列的建筑物的 高度 。 城市的 天际线 是从远处观察城市时&#xff0c;所有建筑物形成的外部轮廓。…

【系统架构】分布式系统架构设计

1 分布式系统是什么 分布式系统是指由多个计算机节点组成的一个系统&#xff0c;这些节点通过网络互相连接&#xff0c;并协同工作完成某个任务。 与单个计算机相比&#xff0c;分布式系统具有更高的可扩展性、可靠性和性能等优势&#xff0c;因此广泛应用于大规模数据处理、高…

大数据-玩转数据-Flink 自定义Sink(Mysql)

一、说明 如果Flink没有提供给我们可以直接使用的连接器&#xff0c;那我们如果想将数据存储到我们自己的存储设备中&#xff0c;mysql 的安装使用请参考 mysql-玩转数据-centos7下mysql的安装 创建表 CREATE TABLE sensor (id int(10) ) ENGINEInnoDB DEFAULT CHARSETutf8二…

使用日志来监控应用

根据提取规则运行的位置可以分为两类做法&#xff0c;一个是在中心端&#xff0c;一个是在日志端。 中心端就是把要处理的所有机器的日志都统一传到中心&#xff0c;比如通过 Kafka 传输&#xff0c;最终落到 Elasticsearch&#xff0c;指标提取规则可以作为流计算任务插到 Ka…

3.解构赋值

解构赋值是一种快速为变量赋值的简洁语法&#xff0c;本质上仍然是为变量赋值。 3.1数组解构 数组解构是 将数组的单元值快速批量赋值给一系列变量 的简洁语法 1.基本语法: &#xff08;1&#xff09;赋值运算符左侧的[ ]用于批量声明变量&#xff0c;右侧数组的单元值将被赋…

免费开源的多种人工智能项目,比如:训练一个模型,让人工智能玩王者荣耀

免费开源的多种人工智能项目&#xff0c;比如&#xff1a;训练一个模型&#xff0c;让人工智能玩王者荣耀。 全文大纲 PULSE - 该开源项目可以通过给图片增加像素点来实现去马赛克或高清化。 Depix - 给打了马赛克的文字去码。 TecoGAN - 给视频去马赛克或者进行超分辨率。 Sk…

python -- 函数闭包

1. LEGB规则 L: local 是局部作用域 E: Enclosed 是嵌套函数的外层函数作用域 G: Global 全局作用域 B:Build-In 内置作用域 变量的使用权重&#xff1a;局部变量 > 外层作用域变量 > 全局变量 > 内置变量 下面代码执行后&#xff0c;x变量的值分别为多少&#xff1…

【JavaEE基础学习打卡03】Java EE 平台有哪些内容?

目录 前言一、Java EE平台说明二、Java EE平台容器及组件1.平台容器2.平台组件 三、JavaEE平台API服务1.API服务概览2.平台API 总结 前言 &#x1f4dc; 本系列教程适用于Java Web初学者、爱好者&#xff0c;小白白。我们的天赋并不高&#xff0c;可贵在努力&#xff0c;坚持不…

Opencv特征检测之ORB算法原理及应用详解

Opencv特征检测之ORB算法原理及应用详解 特征是图像信息的另一种数字表达形式。一组好的特征对于在指定 任务上的最终表现至关重要。视觉里程 &#xff08;VO&#xff09; 的主要问题是如何根据图像特征来估计相机运动。但是&#xff0c;整幅图像用来计算分析通常比较耗时&…

算法通过村第三关-数组基础笔记|爱不起的数组

文章目录 前言线性表的概念什么是线性表从语言实现的角度看从存储的角度看从访问限制的角度看从扩容的角度看数组的概念数组元素的特征 数组的基本操作数组的创建和初始化查找一个元素增加一个元素删除一个元素 总结 前言 提示&#xff1a;孩子们有时候挺伤人的&#xff0c;他…

两个数组的交集-C语言/Java

描述 给定两个数组 nums1 和 nums2 &#xff0c;返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序。&#xff08;1 < nums1.length, nums2.length < 1000&#xff0c;0 < nums1[i], nums2[i] < 1000&#xff09; 示例1 输入…

Linux源码剖析匿名共享内存shmem原理

如下问题如果都清楚了就不用看本文了&#xff1a; 1. shmem ram文件系统的初始化流程是怎样的 2. shmem思想上想复用基于文件的操作流程&#xff0c;实现上shmem也引入了一个文件&#xff0c;那么类似文件open会生成struct file&#xff0c;shmem的struct file怎么生成的 3.…