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

欢迎访问我的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系列

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【2023 华数杯全国大学生数学建模竞赛】 A题 隔热材料的结构优化控制研究 问题分析及完整论文

【2023 华数杯全国大学生数学建模竞赛】 A题 隔热材料的结构优化控制研究 问题分析及完整论文 1 题目 A 题 隔热材料的结构优化控制研究 新型隔热材料 A 具有优良的隔热特性&#xff0c;在航天、军工、石化、建筑、交通等高科技领域中有着广泛的应用。 目前&#xff0c;由单…

微服务Ribbon-负载均衡策略和饥饿加载

目录 一、负载均衡策略 1.1 负载均衡策略介绍 1.2 自定义负载均衡策略 二、饥饿加载 &#xff08;笔记整理自bilibili黑马程序员课程&#xff09; 一、负载均衡策略 1.1 负载均衡策略介绍 负载均衡的规则都定义在IRule接口中&#xff0c;而IRule有很多不同的实现类&…

使用巴特沃兹滤波器的1D零相位频率滤波研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Redis布隆过滤器的原理和应用场景,解决缓存穿透

目录 一、redis 二、布隆过滤器 三、缓存穿透问题 四、布隆过滤器解决缓存穿透 一、redis Redis&#xff08;Remote Dictionary Server&#xff09;是一种开源的内存数据存储系统&#xff0c;也是一个使用键值对&#xff08;Key-Value&#xff09;方式的高性能数据库。Red…

Scratch 之 单个角色模糊特效

想给单独一个角色用模糊特效&#xff0c;怎么办&#xff01;&#xff01;&#xff01; 完全不用慌&#xff0c;最近&#xff0c;我做出了超好用的个体模糊特效&#xff0c;用起来超级简单&#xff0c;接下来进入讲解。 1.原理 前几天&#xff0c;我闲来无事&#xff0c;摘下眼镜…

Express 实战(一):概览

在正式学习 Express 内容之前&#xff0c;我们有必要从大的方面了解一下 Node.js 。 在很长的一段时间里&#xff0c;JavaScript 一门编写浏览器中运行脚本的语言。不过近些年&#xff0c;随着互联网的发展以及技术进步&#xff0c;JavaScript 迎来了一个集中爆发的时代。一个…

DCMM数据管理成熟度之数据治理-数据治理沟通

​01 标准原文 1 概述 数据治理沟通旨在确保组织内全部利益相关者都能及时了解相关政策、标准、流程、角色、职责、计划的最新情况,开展数据管理和应用相关的培训,掌握数据管理相关的知识和技能。数据治理沟通旨在建立与提升跨部门及部门内部数据管理能力,提升数据资产意识,…

V3s uboot 通过env 修改LCD 参数信息

实际项目中我们可能使用各种参数的LCD 显示器&#xff0c;有7吋&#xff0c;4.3 寸等等&#xff0c;我这里使用的uboot 版本是U-Boot 2017.01-rc2 &#xff0c;在make menuconfig 时候会填入lcd 配置信息&#xff0c;如下&#xff1a; 所以这里使用起来很不方便&#xff0c;查看…

【Vue2.0源码学习】生命周期篇-初始化阶段(new Vue)

文章目录 1.综述1.1 前言1.2 生命周期流程图1.3 总结 2.初始化阶段2.1 前言2.2 new Vue()都干了什么2.3 合并属性2.4 callHook函数如何触发钩子函数2.5 总结2.5 总结 1.综述 1.1 前言 在Vue中&#xff0c;每个Vue实例从被创建出来到最终被销毁都会经历一个过程&#xff0c;就…

docker搭建LNMP

docker安装 略 下载镜像 nginx:最新版php-fpm:根据自己需求而定mysql:根据自己需求定 以下是我搭建LNMP使用的镜像版本 rootVM-12-16-ubuntu:/docker/lnmp/php/etc# docker images REPOSITORY TAG IMAGE ID CREATED SIZE mysql 8.0…

微服务——数据同步

问题分析 mysql和redis之间有数据同步问题&#xff0c;ES和mysql之间也有数据同步问题。 单体项目可以在crud时就直接去修改&#xff0c;但在微服务里面不同的服务不行。 方案一 方案二 方案三 总结 导入酒店管理项目 倒入完成功启动后可以看见数据成功获取到了 声明队列和…

模拟实现消息队列(以 RabbitMQ 为蓝本)

目录 1. 需求分析1.1 介绍一些核心概念核心概念1核心概念2 1.2 消息队列服务器&#xff08;Broker Server&#xff09;要提供的核心 API1.3 交换机类型1.3.1 类型介绍1.3.2 转发规则&#xff1a; 1.4 持久化1.5 关于网络通信1.5.1 客户端与服务器提供的对应方法1.5.2 客户端额外…

网络基础-PosixAPI

文章目录 一.网络常用接口Linux协议栈与posix api的关系 背景1.1 socket1.8 close服务端posix接口1.2 bind1.3 listen1.4 accept1.5 connect1.6 send1.7 recv总结 二. 修改句柄属性2.1 fctl2.2 特殊作用2.2.1 设置阻塞非阻塞2.2.1 实例2.2.2设置获取记录锁2.2.2 实例 三、文件操…

数据库操作不再困难,MyBatis动态Sql标签解析

系列文章目录 MyBatis缓存原理 Mybatis的CachingExecutor与二级缓存 Mybatis plugin 的使用及原理 MyBatis四大组件Executor、StatementHandler、ParameterHandler、ResultSetHandler 详解 MyBatisSpringboot 启动到SQL执行全流程 数据库操作不再困难&#xff0c;MyBatis动态S…

n-皇后问题

希望这篇题解对你有用&#xff0c;麻烦动动手指点个赞或关注&#xff0c;感谢您的关注 不清楚蓝桥杯考什么的点点下方&#x1f447; 考点秘籍 想背纯享模版的伙伴们点点下方&#x1f447; 蓝桥杯省一你一定不能错过的模板大全(第一期) 蓝桥杯省一你一定不能错过的模板大全…

Python教程(8)——一文弄懂Python字符串操作(下)

Python字符串操作 字符串常用方法字符串更多方法介绍 字符串常用方法 字符串在编程中是一种不可或缺的数据类型&#xff0c;它在文本和字符数据时提供了丰富而强大的功能。掌握了字符串的使用方法&#xff0c;你能够更加便捷地进行文本处理、数据操作、用户交互等任务&#xf…

opencv图片换背景色

#include <iostream> #include<opencv2/opencv.hpp> //引入头文件using namespace cv; //命名空间 using namespace std;//opencv这个机器视觉库&#xff0c;它提供了很多功能&#xff0c;都是以函数的形式提供给我们 //我们只需要会调用函数即可in…

文献阅读:AnnoLLM: Making Large Language Models to Be Better Crowdsourced Annotators

文献阅读&#xff1a;AnnoLLM: Making Large Language Models to Be Better Crowdsourced Annotators 1. 文章简介2. 方法介绍3. 实验考察 1. 实验结果2. 消解实验3. Consistency & Stability 4. 结论 & 思考 文献链接&#xff1a;https://arxiv.org/abs/2303.16854 …

GrapeCity Documents for Excel, Java Edition Crack

GrapeCity Documents for Excel, Java Edition Crack 增加了对SpreadJS.sjs文件格式的支持&#xff1a; 更快地将大型Microsoft Excel文件转换为.sjs格式。 使用较小的占用空间保存导出的文件。 将Excel/SpreadJS功能导入SpreadJS/从SpreadJS导出。 从.sjs文件中压缩的JSON文件…

@Param详解

文章目录 背景什么是ParamParam的使用方法使用方法&#xff1a;遇到的问题及因Param解决了什么问题使用与不使用对比 Param是如何进行映射的总结 背景 最近在开发过程中&#xff0c;在写mapper接口是在参数前加了Param注解&#xff0c;但是在运行的时候就会报错&#xff0c;说…