client-go实战之十二:选主(leader-election)
欢迎访问我的GitHub
这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
本篇概览
- 本文是《client-go实战》系列的第十二篇,又有一个精彩的知识点在本章呈现:选主(leader-election)
- 在解释什么是选主之前,咱们先来看一个场景(有真实适用场景的技术,学起来才有动力),如下图所示(稍后有详细说明)
上图所描述的业务场景是个普通的controller应用:
- 右侧是人工操作,通过kubectl命令修改了service资源
- 左侧的业务应用订阅了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就实现了分布式锁
- 由于选主只是个特定的小知识点,本篇就没什么多余的理论要研究了,接下来直接开始实战,编码实现一个功能来说明选主的用法
- 实战的业务需求如下
- 开发一个应用,该应用同时运行多个进程
- 当kubernetes的指定namespace下的service发生变化时,在pod的label中记录这个service的变化时间
- 每次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的labelCreateAndStartController(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 lockreturn}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-tutorialsname: nginx-deploymentlabels:app: nginx-apptype: front-end
spec:replicas: 3selector:matchLabels:app: nginx-apptype: front-endtemplate:metadata:labels:app: nginx-apptype: front-end# 这是第一个业务自定义label,指定了mysql的语言类型是c语言language: c# 这是第二个业务自定义label,指定了这个pod属于哪一类服务,nginx属于web类business-service-type: webspec:containers:- name: nginx-containerimage: nginx:latestresources:limits:cpu: "0.5"memory: 128Mirequests:cpu: "0.1"memory: 64Mi
---
apiVersion: v1
kind: Service
metadata:namespace: client-go-tutorialsname: nginx-service
spec:type: NodePortselector:app: nginx-apptype: front-endports:- port: 80targetPort: 80nodePort: 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应该很熟悉了,这里再简单提一下开发的流程
- 将controller完整的写出来,功能是监听service,一旦有变化就更新pod的label
- 在主控逻辑中,根据选主结果决定是否启动步骤1中的controller
- 下面开始写代码
编码:controller
- 新建controller.go文件
- 在controller.go中增加常量和数据结构的定义
package mainimport ("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.Contextclientset *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 donedefer 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)}<-stopChklog.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,逐个更新labelfor _, pod := range pods.Items {podName := pod.Nameklog.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 mainimport ("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 *stringvar 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的labelCreateAndStartController(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 lockreturn}klog.Infof("[%s]选主结果出来了,leader是 : [%s]", processIndentify, identity)},},})
}
- 上述代码中,请注意LeaderElectionConfig对象的几个重要字段,例如LeaseDuration、RenewDeadline、RetryPeriod这些,是和选主时候的续租、超时、重试相关,需要按照您的实际网络情况进行调整
- 现在代码写完了,可以开始验证了
验证
- 这里捋一下验证的步骤
- 构建项目,生产二进制文件
- 执行此二进制文件,启动三个进程
- 观察日志,应该有一个进程选举成功,另外两个只会在日志输出选主结果
- 修改service资源,再去观察日志,发现leader进程会输出日志,再检查pod的label,发现已经修改
- 用ctrl+C命令将leader进程退出,可见另外两个进程会有一个成为新的leader
- 再次修改service资源,新的leader会负责更新pod的label
- 接下来开始操作
- 执行命令go build,对当前工程进行编译构建,得到二进制文件leader-tutorials
- 打开三个终端窗口,输入同样的命令./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原生的分布式锁方案,希望本篇能给您一些参考
你不孤单,欣宸原创一路相伴
- Java系列
- Spring系列
- Docker系列
- kubernetes系列
- 数据库+中间件系列
- DevOps系列
相关文章:

client-go实战之十二:选主(leader-election)
欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 本文是《client-go实战》系列的第十二篇,又有一个精彩的知识点在本章呈现:选主(leader-election)在解释什么是选主之前&…...

2023年即将推出的CSS特性对你影响大不大?
Google开发者大会每年都会提出有关于 Web UI 和 CSS 方面的新特性,今年又上新了许多新功能,今天就从中找出了影响最大的几个功能给大家介绍一下 :has :has() 可以通过检查父元素是否包含特定子元素或这些子元素是否处于特定状态来改变样式,也…...

opencv实战项目-停车位计数
手势识别系列文章目录 手势识别是一种人机交互技术,通过识别人的手势动作,从而实现对计算机、智能手机、智能电视等设备的操作和控制。 1. opencv实现手部追踪(定位手部关键点) 2.opencv实战项目 实现手势跟踪并返回位置信息&a…...

NLP文本匹配任务Text Matching [无监督训练]:SimCSE、ESimCSE、DiffCSE 项目实践
NLP文本匹配任务Text Matching [无监督训练]:SimCSE、ESimCSE、DiffCSE 项目实践 文本匹配多用于计算两个文本之间的相似度,该示例会基于 ESimCSE 实现一个无监督的文本匹配模型的训练流程。文本匹配多用于计算两段「自然文本」之间的「相似度」。 例如…...
复习vue3,简简单单记录
这里的知识是结合视频以及其他文章一起学习,仅用于个人复习记录 ref 和reactive ref 用于基本类型 reactive 用于引用类型 如果使用ref 传递对象,修改值时候需要写为obj.value.attr 方式修改属性值 如果使用reactive 处理对象,直接obj.att…...

【自用】云服务器 docker 环境下 HomeAssistant 安装 HACS 教程
一、进入 docker 中的 HomeAssistant 1.查找 HomeAssistant 的 CONTAINER ID 连接上云服务器(宿主机)后,终端内进入 root ,输入: docker ps找到了 docker 的 container ID 2.config HomeAssistant 输入下面的命令&…...

使用dockerfile手动构建JDK11镜像运行容器并校验
Docker官方维护镜像的公共仓库网站 Docker Hub 国内无法访问了,大部分镜像无法下载,准备逐步构建自己的镜像库。【转载aliyun官方-容器镜像服务 ACR】Docker常见问题 阿里云容器镜像服务ACR(Alibaba Cloud Container Registry)是面…...

编程语言学习笔记-架构师和工程师的区别,PHP架构师之路
🏆作者简介,黑夜开发者,全栈领域新星创作者✌,CSDN博客专家,阿里云社区专家博主,2023年6月CSDN上海赛道top4。 🏆数年电商行业从业经验,历任核心研发工程师,项目技术负责…...

Streamlit 讲解专栏(十):数据可视化-图表绘制详解(上)
文章目录 1 前言2 st.line_chart:绘制线状图3 st.area_chart:绘制面积图4 st.bar_chart:绘制柱状图5 st.pyplot:绘制自定义图表6 结语 1 前言 在数据可视化的世界中,绘制清晰、易于理解的图表是非常关键的。Streamlit…...

其他行业跳槽转入计算机领域简单看法
其他行业跳槽转入计算机领域简单看法 本人选择从以下几个方向谈谈自己的想法和观点。 先看一下总体图,下面会详细分析 如何规划才能实现转码 自我评估和目标设定:首先,你需要评估自己的技能和兴趣,确定你希望在计算机领域从事…...

Unity制作一个简单的登入注册页面
1.创建Canvas组件 首先我们创建一个Canvas画布,我们再在Canvas画布底下创建一个空物体,取名为Resgister。把空物体的锚点设置为全屏撑开。 2.我们在Resgister空物体底下创建一个Image组件,改名为bg。我们也把它 的锚点设置为全屏撑开状态。接…...
常用游戏运营指标DAU、LTV及参考范围
文章目录 前言运营指标指标范围参考值留存指标的意义总结 前言 作为游戏人免不了听到 DAU 、UP值、留存 等名词,并且有些名词听起来还很像,特别是一款上线的游戏,这些游戏运营指标是衡量游戏业务绩效和用户参与度的重要数据,想做…...
标准模板库STL——deque和list
deque概述 deque属于顺序容器,称为双端队列容器 底层数据结构是动态二维数组,从整体上看,deque的内存不连续 初始数组第一维数量为2,必要时进行2倍扩容 每次第一维扩容后,原来数组第二维元素从新数组下标为OldSize/2的…...

分类预测 | MATLAB实现WOA-CNN-BiGRU-Attention数据分类预测
分类预测 | MATLAB实现WOA-CNN-BiGRU-Attention数据分类预测 目录 分类预测 | MATLAB实现WOA-CNN-BiGRU-Attention数据分类预测分类效果基本描述模型描述程序设计参考资料 分类效果 基本描述 1.Matlab实现WOA-CNN-BiGRU-Attention多特征分类预测,多特征输入模型&…...
C++ Primer Plus 第6版 读书笔记(10) 第十章 类与对象
第十章 类与对象 在面向对象编程中,类和对象是两个重要的概念。 类(Class)是一种用户自定义的数据类型,用于封装数据和操作。它是对象的模板或蓝图,描述了对象的属性(成员变量)和行为…...
基于C++ 的OpenCV绘制多边形,多边形多条边用不用的颜色绘制
使用基于C的OpenCV库来绘制多边形,并且为多边形的不同边使用不同的颜色,可以按照以下步骤进行操作: 首先,确保你已经安装了OpenCV库并配置好了你的开发环境。 导入必要的头文件: #include <opencv2/opencv.hpp&g…...

(六)、深度学习框架中的算子
1、深度学习框架算子的基本概念 深度学习框架中的算子(operator)是指用于执行各种数学运算和操作的函数或类。这些算子通常被用来构建神经网络的各个层和组件,实现数据的传递、转换和计算。 算子是深度学习模型的基本组成单元,它们…...

Redis实现共享Session
Redis实现共享Session 分布式系统中,sessiong共享有很多的解决方案,其中托管到缓存中应该是最常用的方案之一。 1、引入依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM…...

网络通信原理UDP协议(第五十课)
UDP协议:用户数据包协议,无连接、不可靠,效率高 字段长度描述Source Port2字节标识哪个应用程序发送(发送进程)。Destination Port2字节标识哪个应用程序接收(接收进程)。Length2字节UDP首部加上UDP数据的字节数,最小为8。Checksum2字节覆盖UDP首部和UDP数据,是可…...

43、TCP报文(一)
本节内容开始,我们正式学习TCP协议中具体的一些原理。首先,最重要的内容仍然是这个协议的封装结构和首部格式,因为这里面牵扯到一些环环相扣的知识点,例如ACK、SYN等等,如果这些内容不能很好的理解,那么后续…...

华为云AI开发平台ModelArts
华为云ModelArts:重塑AI开发流程的“智能引擎”与“创新加速器”! 在人工智能浪潮席卷全球的2025年,企业拥抱AI的意愿空前高涨,但技术门槛高、流程复杂、资源投入巨大的现实,却让许多创新构想止步于实验室。数据科学家…...
服务器硬防的应用场景都有哪些?
服务器硬防是指一种通过硬件设备层面的安全措施来防御服务器系统受到网络攻击的方式,避免服务器受到各种恶意攻击和网络威胁,那么,服务器硬防通常都会应用在哪些场景当中呢? 硬防服务器中一般会配备入侵检测系统和预防系统&#x…...
【git】把本地更改提交远程新分支feature_g
创建并切换新分支 git checkout -b feature_g 添加并提交更改 git add . git commit -m “实现图片上传功能” 推送到远程 git push -u origin feature_g...
C++八股 —— 单例模式
文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全(Thread Safety) 线程安全是指在多线程环境下,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性…...
Element Plus 表单(el-form)中关于正整数输入的校验规则
目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入(联动)2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...

有限自动机到正规文法转换器v1.0
1 项目简介 这是一个功能强大的有限自动机(Finite Automaton, FA)到正规文法(Regular Grammar)转换器,它配备了一个直观且完整的图形用户界面,使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...
【Java学习笔记】BigInteger 和 BigDecimal 类
BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点:传参类型必须是类对象 一、BigInteger 1. 作用:适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...

Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...

排序算法总结(C++)
目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指:同样大小的样本 **(同样大小的数据)**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...