KubeVela核心控制器原理浅析

前言

在学习 KubeVela 的核心控制器之前,我们先简单了解一下 KubeVela 的相关知识。

KubeVela 本身是一个应用交付与管理控制平面,它架在 Kubernetes 集群、云平台等基础设施之上,通过开放应用模型来对组件、云服务、运维能力、交付工作流进行统一的编排和交付。

具体来说,KubeVela 本身主要由如下几个部分组成:

核心控制器 为整个系统提供核心控制逻辑,完成诸如编排应用和工作流、修订版本快照、垃圾回收等等基础逻辑。

模块化能力控制器 负责对 X-Definitions 对象进行注册和管理。

Cluster Gateway 控制器 为操作多集群提供了统一的访问接口。

插件体系 负责注册和管理 KubeVela 的扩展功能,包括 CRD 控制器和相关模块定义。比如 VelaUX、FluxCD、Workflow 等插件。

UI 控制台 和 CLI 分别为用户提供了图形化界面和命令行界面操作 API。

本文主要介绍 KubeVela 的核心控制逻辑,如未做特殊说明,以下内容均基于 KubeVela 社区版本 v1.8.2 阐述。

OAM 应用模型

在介绍 KubeVela 的核心控制逻辑之前,我们先了解一下核心控制器作用对象,OAM 应用模型(如图 1),开放应用模型允许用户把一个现代微服务应用部署所需的所有组件和各项运维动作,描述为一个统一的、与基础设施无关的"部署计划",进而实现在混合环境中进行标准化和高效率的应用交付。

图片

图1

具体来说:

  • • 通过一个叫做应用部署计划(Application)的对象来声明一个微服务应用的完整交付流程,这其中包含了待交付组件、关联的运维动作、交付流水线等内容。

  • • 所有的待交付组件、运维动作和流水线中的每一个步骤,都遵循 OAM 规范设计为独立的可插拔模块,允许用户按照自己的需求进行组合或者定制。

  • • OAM 模型也会负责规范各个模块之间的协作接口。

应用部署计划

Application 对象是 KubeVela 核心控制器作用的最小单元,也是用户唯一需要了解的 API,它表达了一个微服务应用的部署计划。遵循 OAM 规范,一个应用部署计划(Application)由"待部署组件(Component)"、"运维动作(Trait)"、"应用的执行策略(Policy)",以及"部署工作流(Workflow)"这四部分概念组成。

组件

组件(Component)是构成微服务应用的基本单元,比如一个 Bookinfo 应用可以包含 Ratings、Reviews、Details 等多个组件。

运维特征

运维特征(Trait)负责定义组件可以关联的通用运维行为,比如服务发布、访问、治理、弹性、可观测性、灰度发布等。在 OAM 规范中,一个组件可以绑定任意个运维特征。

应用的执行策略

应用的执行策略(Policy)负责定义应用级别的部署特征,比如健康检查规则、安全组、防火墙、SLO、检验等模块。

部署执行工作流

部署执行工作流(Workflow)定义了从部署开始到达到部署终态的一条完整路径,KubeVela 会按这个流水线执行工作流中定义的各个步骤来完成整个应用交付。

KubeVela 不仅内置了多种类型组件、运维特征、策略以及工作流 Step,还支持用户自定义组件、运维特征、策略及工作流 Step,帮助用户在混合环境中进行标准化和高效率的应用交付。在实际使用时,用户通过上述 Application 对象来引用预置的组件、运维特征、应用策略、以及工作流节点模块,填写这些模块暴露的用户参数即可完成一次对应用交付的建模。

核心控制器工作原理

KubeVela vela-core 的代码结构相对来说比较简单,因为它是基于 controller-runtime 框架开发实现的控制器管理器。其中 componentdefinition、traitdefinition、policydefinition、workflowstepdefinition 控制器分别用于调谐管理组件、运维特征、策略、工作流步骤类型及其版本状态信息,而 application 控制器则用于调谐应用部署计划。

application_controller 是 KubeVela 的核心控制器,它会解析应用部署计划,生成当前应用的修订版本,并与最近一次修订版本进行比较根据是否有差异来判断修订版本状态是否需要更新,然后解析工作流 Steps 中引用的外部策略生成 manifest 并执行 apply,再然后根据应用工作流 Steps 生成工作流 Instance 和待执行的任务 Runners,最后创建工作流 Executor 并执行任务 Runners。

图片

KubeVela 核心控制器的工作原理跟 K8s Controller 是一样的,都是通过 API Server 提供的 List & Watch 接口来实时监控集群中 Application 资源对象的状态变化,当资源对象的状态变化时,控制器会尝试将其状态调谐为期望的状态。接下来笔者将对 application_controller 的调谐逻辑进行详细说明,主要包括解析应用部署计划、创建应用修订版本、应用外部策略、解析执行工作流任务等四个过程。

1、解析应用部署计划

解析应用部署计划的过程实际上就是解析应用部署计划中的组件、运维特征、策略和工作流,然后生成应用描述文件 AppFile,在生成 AppFile 之前会先检查最新的应用修订版与应用是否具有相同的 publishVersion,如果 publishVersion 相同则直接根据最新的应用修订版生成 AppFile,否则根据应用应用部署计划生成 AppFile。AppFile 作为后续调谐逻辑的基准数据模型,不仅包含了应用部署计划中的信息,还记录了调谐过程中所必须的 PolicyWorkload 等中间产物。

func (p *Parser) GenerateAppFile(ctx context.Context, app *v1beta1.Application) (*Appfile, error) {
   if ctx, ok := ctx.(monitorContext.Context); ok {
      subCtx := ctx.Fork("generate-app-file", monitorContext.DurationMetric(func(v float64) {
         metrics.AppReconcileStageDurationHistogram.WithLabelValues("generate-appfile").Observe(v)
      }))
      defer subCtx.Commit("finish generate appFile")
   }
   if isLatest, appRev, err := p.isLatestPublishVersion(ctx, app); err != nil {
      return nil, err
   } else if isLatest {
      app.Spec = appRev.Spec.Application.Spec
      return p.GenerateAppFileFromRevision(appRev)
   }
   return p.GenerateAppFileFromApp(ctx, app)
}

2、创建应用修订版本

创建应用修订版本的过程实际上就是保存应用版本,如果是更新应用的操作,会获取应用最近一次的版本信息,并与应用当前版本比较确认是否需要更新应用状态中的最近一次版本信息。

if err := handler.PrepareCurrentAppRevision(logCtx, appFile); err != nil {
        logCtx.Error(err, "Failed to prepare app revision")
        r.Recorder.Event(app, event.Warning(velatypes.ReasonFailedRevision, err))
        return r.endWithNegativeCondition(logCtx, app, condition.ErrorCondition("Revision", err), common.ApplicationRendering)
    }
    if err := handler.FinalizeAndApplyAppRevision(logCtx); err != nil {
        logCtx.Error(err, "Failed to apply app revision")
        r.Recorder.Event(app, event.Warning(velatypes.ReasonFailedRevision, err))
        return r.endWithNegativeCondition(logCtx, app, condition.ErrorCondition("Revision", err), common.ApplicationRendering)
    }
    logCtx.Info("Successfully prepare current app revision", "revisionName", handler.currentAppRev.Name,
        "revisionHash", handler.currentRevHash, "isNewRevision", handler.isNewRevision)
    app.Status.SetConditions(condition.ReadyCondition("Revision"))
    r.Recorder.Event(app, event.Normal(velatypes.ReasonRevisoned, velatypes.MessageRevisioned))

    if err := handler.UpdateAppLatestRevisionStatus(logCtx); err != nil {
        logCtx.Error(err, "Failed to update application status")
        return r.endWithNegativeCondition(logCtx, app, condition.ReconcileError(err), common.ApplicationRendering)
    }
    logCtx.Info("Successfully apply application revision")

3、应用外部策略

外部策略指的是未在内部策略中声明的且在工作流步骤中使用的策略,这些策略是在应用的命名空间下声明的策略。AppFile 中的策略负载 PolicyWorkload 是通过加载工作流的外部策略得到的,应用外部策略会将 AppFile 中的 PolicyWorkloads 渲染成 manifest 后分发到集群。

func LoadExternalPoliciesForWorkflow(ctx context.Context, cli client.Client, appNs string, steps []workflowv1alpha1.WorkflowStep, internalPolicies []v1beta1.AppPolicy) ([]v1beta1.AppPolicy, error) {
   policies := internalPolicies
   policyMap := map[string]struct{}{}
   for _, policy := range policies {
      policyMap[policy.Name] = struct{}{}
   }
   // Load extra used policies declared in the workflow step
   for _, _step := range steps {
      if _step.Type == DeployWorkflowStep && _step.Properties != nil {
         props := DeployWorkflowStepSpec{}
         if err := utils.StrictUnmarshal(_step.Properties.Raw, &props); err != nil {
            return nil, errors.Wrapf(err, "invalid WorkflowStep %s", _step.Name)
         }
         for _, policyName := range props.Policies {
            if _, found := policyMap[policyName]; !found {
               po := &v1alpha1.Policy{}
               if err := cli.Get(ctx, types2.NamespacedName{Namespace: appNs, Name: policyName}, po); err != nil {
                  if kerrors.IsNotFound(err) {
                     return nil, errors.Errorf("external policy %s not found", policyName)
                  }
                  return nil, errors.Wrapf(err, "failed to load external policy %s in namespace %s", policyName, appNs)
               }
               policies = append(policies, v1beta1.AppPolicy{Name: policyName, Type: po.Type, Properties: po.Properties})
               policyMap[policyName] = struct{}{}
            }
         }
      }
   }
   return policies, nil
}


func (h *AppHandler) ApplyPolicies(ctx context.Context, af *appfile.Appfile) error {
   if ctx, ok := ctx.(monitorContext.Context); ok {
      subCtx := ctx.Fork("apply-policies", monitorContext.DurationMetric(func(v float64) {
         metrics.AppReconcileStageDurationHistogram.WithLabelValues("apply-policies").Observe(v)
      }))
      defer subCtx.Commit("finish apply policies")
   }
   policyManifests, err := af.GeneratePolicyManifests(ctx)
   if err != nil {
      return errors.Wrapf(err, "failed to render policy manifests")
   }
   if len(policyManifests) > 0 {
      for _, policyManifest := range policyManifests {
         util.AddLabels(policyManifest, map[string]string{
            oam.LabelAppName:      h.app.GetName(),
            oam.LabelAppNamespace: h.app.GetNamespace(),
         })
      }
      if err = h.Dispatch(ctx, "", common.PolicyResourceCreator, policyManifests...); err != nil {
         return errors.Wrapf(err, "failed to dispatch policy manifests")
      }
   }
   return nil
}

4、解析执行工作流任务

核心控制器中最重要的也是最难理解的部分应该就属工作流任务的解析执行了,因为该部分使用了大量的函数式编程和异步编程,对于一个 go 语言的初学者来说,捋清楚这段代码并不是一件易事。接下来笔者将从解析和执行两个方面来介绍工作流任务。

解析工作流任务的过程主要包含三个步骤,第一步是注册 handlers 到 providers 中,每个 provider 中都包含了多个 handlers 处理程序,这些 handlers 主要供后续执行工作流任务使用,不管是内置的还是自定义工作流步骤,在声明 Step 的过程中可以调用特定 provider 中的 handler 来完成工作任务的执行。第二步是初始化工作流实例(执行工作流任务的阶段也会根据这个工作流实例创建工作流的执行器用于执行工作流中任务 Runners)。第三步是根据工作流实例和已注册的 handlers providers 去遍历解析工作流步骤 Step 生成工作流任务运行程序 Runners。

func (h *AppHandler) GenerateApplicationSteps(ctx monitorContext.Context,
   app *v1beta1.Application,
   appParser *appfile.Parser,
   af *appfile.Appfile) (*wfTypes.WorkflowInstance, []wfTypes.TaskRunner, error) {
   appRev := h.currentAppRev
   t := time.Now()
   defer func() {
      metrics.AppReconcileStageDurationHistogram.WithLabelValues("generate-app-steps").Observe(time.Since(t).Seconds())
   }()
   appLabels := map[string]string{
      oam.LabelAppName:      app.Name,
      oam.LabelAppNamespace: app.Namespace,
   }
   handlerProviders := providers.NewProviders()
   kube.Install(handlerProviders, h.r.Client, appLabels, &kube.Handlers{
      Apply:  h.Dispatch,
      Delete: h.Delete,
   })
   configprovider.Install(handlerProviders, h.r.Client, func(ctx context.Context, resources []*unstructured.Unstructured, applyOptions []apply.ApplyOption) error {
      for _, res := range resources {
         res.SetLabels(util.MergeMapOverrideWithDst(res.GetLabels(), appLabels))
      }
      return h.resourceKeeper.Dispatch(ctx, resources, applyOptions)
   })
   oamProvider.Install(handlerProviders, app, af, h.r.Client, h.applyComponentFunc(
      appParser, appRev, af), h.renderComponentFunc(appParser, appRev, af))
   pCtx := velaprocess.NewContext(generateContextDataFromApp(app, appRev.Name))
   renderer := func(ctx context.Context, comp common.ApplicationComponent) (*appfile.Workload, error) {
      return appParser.ParseWorkloadFromRevisionAndClient(ctx, comp, appRev)
   }
   multiclusterProvider.Install(handlerProviders, h.r.Client, app, af,
      h.applyComponentFunc(appParser, appRev, af),
      h.checkComponentHealth(appParser, appRev, af),
      renderer)
   terraformProvider.Install(handlerProviders, app, renderer)
   query.Install(handlerProviders, h.r.Client, nil)

   instance := generateWorkflowInstance(af, app)
   executor.InitializeWorkflowInstance(instance)
   runners, err := generator.GenerateRunners(ctx, instance, wfTypes.StepGeneratorOptions{
      Providers:       handlerProviders,
      PackageDiscover: h.r.pd,
      ProcessCtx:      pCtx,
      TemplateLoader:  template.NewWorkflowStepTemplateRevisionLoader(appRev, h.r.dm),
      Client:          h.r.Client,
      StepConvertor: map[string]func(step workflowv1alpha1.WorkflowStep) (workflowv1alpha1.WorkflowStep, error){
         wfTypes.WorkflowStepTypeApplyComponent: func(lstep workflowv1alpha1.WorkflowStep) (workflowv1alpha1.WorkflowStep, error) {
            copierStep := lstep.DeepCopy()
            if err := convertStepProperties(copierStep, app); err != nil {
               return lstep, errors.WithMessage(err, "convert [apply-component]")
            }
            copierStep.Type = wfTypes.WorkflowStepTypeBuiltinApplyComponent
            return *copierStep, nil
         },
      },
   })
   if err != nil {
      return nil, nil, err
   }
   return instance, runners, nil
}

生成任务运行程序的过程是根据应用部署计划中配置的工作流,遍历工作流步骤 Step 参数及类型,加载 Step 类型模板,然后根据 Step 参数和模板编译生成 task。taskRunner 包含三个参数,第一个参数是工作流步骤名称 wfstepName,用于标识任务名称;第二个参数是 checkPending 函数,用于检查是否挂起任务运行;第三个参数是 run 函数,也就是任务的实际运行程序,加载 Step 模板并接收 Step 配置参数完成任务编译的过程就是在这个 run 函数中完成的。

func GenerateRunners(ctx monitorContext.Context, instance *types.WorkflowInstance, options types.StepGeneratorOptions) ([]types.TaskRunner, error) {
   ctx.V(options.LogLevel)
   subCtx := ctx.Fork("generate-task-runners", monitorContext.DurationMetric(func(v float64) {
      metrics.GenerateTaskRunnersDurationHistogram.WithLabelValues("workflowrun").Observe(v)
   }))
   defer subCtx.Commit("finish generate task runners")
   options = initStepGeneratorOptions(ctx, instance, options)
   taskDiscover := tasks.NewTaskDiscover(ctx, options)
   var tasks []types.TaskRunner
   for _, step := range instance.Steps {
      opt := &types.TaskGeneratorOptions{
         ID:              generateStepID(instance.Status, step.Name),
         PackageDiscover: options.PackageDiscover,
         ProcessContext:  options.ProcessCtx,
      }
      for typ, convertor := range options.StepConvertor {
         if step.Type == typ {
            opt.StepConvertor = convertor
         }
      }
      task, err := generateTaskRunner(ctx, instance, step, taskDiscover, opt, options)
      if err != nil {
         return nil, err
      }
      tasks = append(tasks, task)
   }
   return tasks, nil
}

执行工作流任务的过程也包含三个步骤,第一步是根据工作流实例创建工作流执行器。第二步是调用执行器的 ExecuteRunners 方法按顺序执行工作流任务运行程序。第三步则是根据工作流任务运行程序的执行结果(即工作流执行状态)和工作流实例状态的 EndTime 来调谐应用状态或 gc ResourceTrackers,其中 ResourceTrackers 主要是用来跟踪和维护应用管理的资源,会在转发应用管理的资源清单之前在 HubCluster 中进行创建,可以确保在删除应用程序时能真正删除所有托管的资源。

func (w *workflowExecutor) ExecuteRunners(ctx monitorContext.Context, taskRunners []types.TaskRunner) (v1alpha1.WorkflowRunPhase, error) {
   InitializeWorkflowInstance(w.instance)
   status := &w.instance.Status
   dagMode := status.Mode.Steps == v1alpha1.WorkflowModeDAG
   cacheKey := fmt.Sprintf("%s-%s", w.instance.Name, w.instance.Namespace)

   allRunnersDone, allRunnersSucceeded := checkRunners(taskRunners, w.instance.Status)
   if status.Finished {
      StepStatusCache.Delete(cacheKey)
   }
   if checkWorkflowTerminated(status, allRunnersDone) {
      if isTerminatedManually(status) {
         return v1alpha1.WorkflowStateTerminated, nil
      }
      return v1alpha1.WorkflowStateFailed, nil
   }
   if checkWorkflowSuspended(status) {
      return v1alpha1.WorkflowStateSuspending, nil
   }
   if allRunnersSucceeded {
      return v1alpha1.WorkflowStateSucceeded, nil
   }

   wfCtx, err := w.makeContext(ctx, w.instance.Name)
   if err != nil {
      ctx.Error(err, "make context")
      return v1alpha1.WorkflowStateExecuting, err
   }
   w.wfCtx = wfCtx

   if cacheValue, ok := StepStatusCache.Load(cacheKey); ok {
      // handle cache resource
      if len(status.Steps) < cacheValue.(int) {
         return v1alpha1.WorkflowStateSkipped, nil
      }
   }

   e := newEngine(ctx, wfCtx, w, status, taskRunners)

   err = e.Run(ctx, taskRunners, dagMode)
   if err != nil {
      ctx.Error(err, "run steps")
      StepStatusCache.Store(cacheKey, len(status.Steps))
      return v1alpha1.WorkflowStateExecuting, err
   }

   StepStatusCache.Store(cacheKey, len(status.Steps))
   if feature.DefaultMutableFeatureGate.Enabled(features.EnablePatchStatusAtOnce) {
      return e.status.Phase, nil
   }
   return e.checkWorkflowPhase(), nil
}

执行工作流任务,首先会创建工作流的执行引擎,然后调用引擎的 Run 方法顺序执行或并行执行 taskRunner,默认 steps 以 StepByStep 顺序执行,subSteps 以 DAG 并行执行。顺序执行会遍历 taskRunners,并依次调用 taskRunner 的 run 方法,run 方法的内容就是上文提到的生成 taskRunner 时的 run 函数,根据 Step 参数配置和加载的 Step 模板完成工作流步骤任务编译后,会执行 Step CUE Template 中调用的 provider handlers(即上文提到的在解析工作流任务阶段注册的各类型 providers handlers),从而完成 taskRunner 执行。应用部署计划中用到最多的工作流步骤类型是 deploy,deploy 是一个的功能强大的组件部署步骤,使用策略进行多集群交付。另外使用最多的应用策略是 topology,topology 描述了组件应该部署到的集群和命名空间。 

deploy.cue

import (
 "vela/op"
)

"deploy": {
 type: "workflow-step"
 annotations: {
  "category": "Application Delivery"
 }
 labels: {
  "scope": "Application"
 }
 description: "A powerful and unified deploy step for components multi-cluster delivery with policies."
}
template: {
 deploy: op.#Deploy & {
  policies:                 parameter.policies
  parallelism:              parameter.parallelism
  ignoreTerraformComponent: parameter.ignoreTerraformComponent
 }
 parameter: {
  //+usage=If set to false, the workflow will suspend automatically before this step, default to be true.
  auto: *true | bool
  //+usage=Declare the policies that used for this deployment. If not specified, the components will be deployed to the hub cluster.
  policies: *[] | [...string]
  //+usage=Maximum number of concurrent delivered components.
  parallelism: *5 | int
  //+usage=If set false, this step will apply the components with the terraform workload.
  ignoreTerraformComponent: *true | bool
 }
}

总结

本文主要介绍了 KubeVela 核心控制器的工作原理,包括核心控制逻辑中解析应用部署计划、创建应用修订版本、应用外部策略、解析执行工作流任务等四个部分,本篇作为综述帮助大家初步了解 KubeVela 核心控制器的技术要点和运行机制,后续我们将分别从上述四个部分进行详细解读。

参考文献

应用管理平台 kubevela:https://qiankunli.github.io/2022/10/23/kubevela.html

kubevela 源码分析:https://qiankunli.github.io/2022/11/06/kubevela_source.html

KubeVela 源码仓库:https://github.com/kubevela/kubevela

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

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

相关文章

PWM(PulseWidthModulation)控制

PWM&#xff08;Pulse Width Modulation&#xff09;控制就是对脉冲的宽度进行调制的技术&#xff0c;即通过对一系列脉冲的宽度进行调制&#xff0c;来等效的获得所需要的波形&#xff08;含形状和幅值&#xff09;&#xff1b;面积等效原理是PWM技术的重要基础理论&#xff1…

【LabVIEW学习】3.labview制作安装程序

一。生成exe文件 1.创建可执行文件 &#xff08;1&#xff09;创建项目 注意&#xff1a; 1.创建.exe文件&#xff0c;这个文件在labview环境下才可以运行&#xff0c;如果直接传递给其他电脑&#xff08;没有labview环境&#xff09;&#xff0c;他是不可以运行的。 2.如果已…

原生实现底部弹窗效果 h5 小程序

<template><div class"home"><div class"btn" click"showPopupshow">弹出底部蒙层</div><div class"popup " catchtouchmove"true" :class"showPopup" ><div class"mask&q…

springframe工程导入

配置gradle工程 init.d 目录下新建init.gradle allprojects {repositories {mavenLocal()maven {allowInsecureProtocol trueurl https://maven.aliyun.com/nexus/content/repositories/central/}} } 报错Plugin [id: org.jetbrains.dokka, version: 0.10.1, apply: false] w…

单片机学习5——外部中断程序

#include<reg52.h>unsigned char a; sbit lcden P3^4;void main() {lcden0;EA1;EX01;IT00;a0xF0; //点亮4位小灯while(1){P1a;} }//中断服务程序 void ext0() interrupt 0 // 0 表示的是外部中断源0 {a0x0f; // 中断处理完&#xff0c;再返回主…

公司注册资金认缴的好处有哪些

公司注册资金认缴的好处 1、减少投资项目审批&#xff0c;最大限度地缩小审批、核准、备案范围&#xff0c;切实落实企业和个人投资自主权。对确需审批、核准、备案的项目&#xff0c;要简化程序、限时办结。同时&#xff0c;为避免重复投资和无序竞争&#xff0c;强调要加强土…

坚鹏:广州银行清华大学消费金融发展趋势与创新培训圆满结束

广州银行自1996年9月成立以来&#xff0c;依托中国经济腾飞的大好形势&#xff0c;成为国内具有一定知名度与地方特色的商业银行。截至2022年12月末&#xff0c;已开业机构174家&#xff0c;包括总行1家&#xff0c;分行级机构15家(含信用卡中心)、支行152家、信用卡分中心6家&…

系统安全测试要怎么做?

进行系统安全测试时&#xff0c;可以按照以下详细的步骤进行&#xff1a; 1、信息收集和分析&#xff1a; 收集系统的相关信息&#xff0c;包括架构、部署环境、使用的框架和技术等。 分析系统的安全需求、威胁模型和安全策略等文档。 2、威胁建模和风险评估&#xff1a; …

三菱GX WORRKS3 下载与安装

目录 下载 安装 准备好安装包 对电脑系统要求 安装 因为小编公司需要&#xff0c;所以开始了三菱plc软件的学习&#xff0c;并从今天开始记录学习&#xff0c;希望小编的内容能帮到你&#xff0c;对你的学习有帮助&#xff01; 下载 三菱电机官网 当然了&#xff0c;需要…

乘法原理 LeetCode 828. 统计子串中的唯一字符

我们定义了一个函数 countUniqueChars(s) 来统计字符串 s 中的唯一字符&#xff0c;并返回唯一字符的个数。 例如&#xff1a;s "LEETCODE" &#xff0c;则其中 "L", "T","C","O","D" 都是唯一字符&#xff0c;…

代码随想录算法训练营第四十八天|121. 买卖股票的最佳时机、122. 买卖股票的最佳时机 II

LeetCode 121. 买卖股票的最佳时机 题目链接&#xff1a;121. 买卖股票的最佳时机 - 力扣&#xff08;LeetCode&#xff09; 直觉告诉我要贪心算法&#xff0c;章节告诉我得用DP来做&#xff0c;行&#xff0c;都做一下&#xff01; 贪心&#xff1a;只能买一次&#xff0c;所…

好物周刊#31:在线格式转换

https://github.com/cunyu1943/JavaPark https://yuque.com/cunyu1943 村雨遥的好物周刊&#xff0c;记录每周看到的有价值的信息&#xff0c;主要针对计算机领域&#xff0c;每周五发布。 一、项目 1. Soybean Admin 一个基于 Vue3、Vite3、TypeScript、NaiveUI、Pinia 和…

性能测试【二】:nmon的常用操作

性能测试【二】&#xff1a;nmon的常用操作 1、nmon介绍说明2、软件下载2.1、Nmon下载地址2.2、Nmonanalyser下载地址 3、nmon使用3.1、将nmon上传至/usr/local/src目录下&#xff0c;并解压3.2、解压后根据自己系统的实际版本查找相应的使用命令&#xff0c;并给命令赋予可执行…

Arduio开发STM32所面临的风险

据说micro_ros用到了arduino,然后用arduino搞stm32需要用到这个Arduino STM32的东西&#xff0c;然后这里申明了&#xff1a;这些代码没有经过严格测试&#xff0c;如果是向心脏起搏器&#xff0c;自动驾驶这样要求严格的的情况下&#xff0c;这个东西不能保证100%不发生问题&a…

怎样提升伦敦银买卖技巧?

如果投资者想提升伦敦银的买卖技巧&#xff0c;可以学习一些有用的技术分析方法。所谓技术分析&#xff0c;就是通过对行情过往价格和相关交易数据进行收集&#xff0c;用图表的方式解读白银市场&#xff0c;进而预测行情未来主线走势、判断价格细节变化、寻找重要支撑点阻力点…

BGP综合实验

一、实验拓扑图 二、实验需求 1、AS1存在两个环回&#xff0c;一个地址为192.168.1.0/24&#xff0c;该地址不能在任何协议中宣告&#xff0c;AS3存在两个环回&#xff0c;一个地址为192.168.2.0/24&#xff0c;该地址不能在任何协议中宣告&#xff0c;最终要求这两个环回可以…

Rust的异步编程与Futures

欢迎关注我的公众号lincyang新自媒体&#xff0c;回复关键字【程序员经典书单】&#xff0c;领取程序员的100本经典书单 大家好&#xff01;我是lincyang。 今天&#xff0c;我们来探讨Rust中的异步编程和Futures。Rust的异步编程是一个强大的特性&#xff0c;它允许开发者编写…

学习grdecl文件格式

一、初步了解 最近在学习grdecl文件格式&#xff0c;文档不多。查找资料发现&#xff0c;这个格式的文件是由斯伦贝谢公司的ECLIPSE专业软件生成的。 搜到一些文档&#xff0c;都是2010年之前的&#xff0c;似乎有些用处。文档也交代了这个文件格式分为二进制和文本格式…

卷积神经网络(CNN)车牌识别

文章目录 一、前言二、前期工作1. 设置GPU&#xff08;如果使用的是CPU可以忽略这步&#xff09;2. 导入数据3. 查看数据3.数据可视化4.标签数字化 二、构建一个tf.data.Dataset1.预处理函数2.加载数据3.配置数据 三、搭建网络模型四、设置动态学习率五、编译六、训练八、保存和…