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等等,如果这些内容不能很好的理解,那么后续…...
Ubuntu系统下交叉编译openssl
一、参考资料 OpenSSL&&libcurl库的交叉编译 - hesetone - 博客园 二、准备工作 1. 编译环境 宿主机:Ubuntu 20.04.6 LTSHost:ARM32位交叉编译器:arm-linux-gnueabihf-gcc-11.1.0 2. 设置交叉编译工具链 在交叉编译之前&#x…...
MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例
一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...

【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)
服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...
基于matlab策略迭代和值迭代法的动态规划
经典的基于策略迭代和值迭代法的动态规划matlab代码,实现机器人的最优运输 Dynamic-Programming-master/Environment.pdf , 104724 Dynamic-Programming-master/README.md , 506 Dynamic-Programming-master/generalizedPolicyIteration.m , 1970 Dynamic-Programm…...
docker 部署发现spring.profiles.active 问题
报错: org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property spring.profiles.active imported from location class path resource [application-test.yml] is invalid in a profile specific resource [origin: class path re…...
华为OD最新机试真题-数组组成的最小数字-OD统一考试(B卷)
题目描述 给定一个整型数组,请从该数组中选择3个元素 组成最小数字并输出 (如果数组长度小于3,则选择数组中所有元素来组成最小数字)。 输入描述 行用半角逗号分割的字符串记录的整型数组,0<数组长度<= 100,0<整数的取值范围<= 10000。 输出描述 由3个元素组成…...

热烈祝贺埃文科技正式加入可信数据空间发展联盟
2025年4月29日,在福州举办的第八届数字中国建设峰会“可信数据空间分论坛”上,可信数据空间发展联盟正式宣告成立。国家数据局党组书记、局长刘烈宏出席并致辞,强调该联盟是推进全国一体化数据市场建设的关键抓手。 郑州埃文科技有限公司&am…...

Windows电脑能装鸿蒙吗_Windows电脑体验鸿蒙电脑操作系统教程
鸿蒙电脑版操作系统来了,很多小伙伴想体验鸿蒙电脑版操作系统,可惜,鸿蒙系统并不支持你正在使用的传统的电脑来安装。不过可以通过可以使用华为官方提供的虚拟机,来体验大家心心念念的鸿蒙系统啦!注意:虚拟…...
Docker、Wsl 打包迁移环境
电脑需要开启wsl2 可以使用wsl -v 查看当前的版本 wsl -v WSL 版本: 2.2.4.0 内核版本: 5.15.153.1-2 WSLg 版本: 1.0.61 MSRDC 版本: 1.2.5326 Direct3D 版本: 1.611.1-81528511 DXCore 版本: 10.0.2609…...

使用python进行图像处理—图像变换(6)
图像变换是指改变图像的几何形状或空间位置的操作。常见的几何变换包括平移、旋转、缩放、剪切(shear)以及更复杂的仿射变换和透视变换。这些变换在图像配准、图像校正、创建特效等场景中非常有用。 6.1仿射变换(Affine Transformation) 仿射变换是一种…...