Kubernetes Operator开发实践

Operator 介绍

Operator 可以看成是 CRD + Controller 的一种组合资源。Kubernetes 中的基础资源类型有 Pod、Service、Job、Deployment 等表达能力有限,CRD 则提供了创建新的资源类型方式;Controller 监听 CRD 对象实例的增、删、改事件,然后执行相应的业务逻辑。

为解决开发难得问题,社区推出了简单易用的 Operator 框架,比较主流的是 kubebuilder 和 Operator Framework,这两个框架的使用基本上差别不大,我们可以根据自己习惯选择一个即可,我们这里先使用 Operator Framework 来给大家简要说明下 Operator 的开发。

Operator Framework

Operator Framework 是 CoreOS 开源的一个用于快速开发 Operator 的工具包,该框架包含两个主要的部分:

Operator SDK: 无需了解复杂的 Kubernetes API 特性,即可让你根据你自己的专业知识构建一个 Operator 应用。 Operator Lifecycle Manager(OLM): 帮助你安装、更新和管理跨集群的运行中的所有 Operator(以及他们的相关服务) 

Operator SDK 提供了用于开发 Go、Ansible 以及 Helm 中的 Operator 的工作流,下面的工作流适用于 Golang 的 Operator:

  • 使用 SDK 创建一个新的 Operator 项目

  • 通过添加自定义资源(CRD)定义新的资源 API

  • 指定使用 SDK API 来 watch 的资源

  • 定义 Operator 的协调(reconcile)逻辑

  • 使用 Operator SDK 构建并生成 Operator 部署清单文件

每种 Operator 类型都有不同的功能集,在选择项目的类型时,重要的是要了解每种项目类型的功能和局限性以及 Operator 的用例。

示例

我们平时在部署一个简单的 Webserver 到 Kubernetes 集群中的时候,都需要先编写一个 Deployment 的控制器,然后创建一个 Service 对象,通过 Pod 的 label 标签进行关联,最后通过 Ingress 或者 type=NodePort 类型的 Service 来暴露服务,每次都需要这样操作,是不是略显麻烦,我们就可以创建一个自定义的资源对象,通过我们的 CRD 来描述我们要部署的应用信息,比如镜像、服务端口、环境变量等等,然后创建我们的自定义类型的资源对象的时候,通过控制器去创建对应的 Deployment 和 Service,是不是就方便很多了,相当于我们用一个资源清单去描述了 Deployment 和 Service 要做的两件事情。

这里我们将创建一个名为 AppService 的 CRD 资源对象,然后定义如下的资源清单进行应用部署:

apiVersion: app.example.com/v1
kind: AppService
metadata:name: nginx-app
spec:replicas: 2image: nginx:1.7.9ports:- port: 80targetPort: 80nodePort: 30002

通过这里自定义的 AppService 资源对象去创建副本数为2的 Pod,然后通过nodePort=30002的端口去暴露服务,接下来我们就来实现这个简单的 Operator 应用。

开发环境

依赖服务版本:

  • Kubernetes:v1.18.8
  • operator-sdk:v1.17.0
  • golang:1.17.3
  • docker:20.10.12

安装Operator,其他服务安装略。

wget -c https://github.com/operator-framework/operator-sdk/releases/download/v1.17.0/operator-sdk_linux_amd64
mv operator-sdk_linux_amd64 /usr/bin/operator-sdk
chmod +x /usr/bin/operator-sdk

还需要安装gcc

yum -y install gcc

Operator开发

环境准备好了,接下来就可以使用 operator-sdk 直接创建一个新的项目了,命令格式为:operator-sdk init

按照上面我们预先定义的 CRD 资源清单,我们这里可以这样创建:

# 创建项目目录
mkdir -p my-operator && cd my-operator
# 使用gomodules包管理工具
export GO111MODULE=on  
# 使用代理加速
export GOPROXY="https://goproxy.cn" 
# 使用 sdk 创建一个名为 my-operator 的 operator 项目
go mod init my-operator# 使用下面的命令初始化项目
operator-sdk init --domain example.com --license apache2 --owner "developer"

初始化完成后的项目结构如下所示:

$ tree -L 2
.
├── config
│   ├── default
│   ├── manager
│   ├── manifests
│   ├── prometheus
│   ├── rbac
│   └── scorecard
├── Dockerfile
├── go.mod
├── go.sum
├── hack
│   └── boilerplate.go.txt
├── main.go
├── Makefile
└── PROJECT8 directories, 7 files

到这里一个全新的 Operator 项目就新建完成了。

项目结构

使用 operator-sdk init 命令创建新的 Operator 项目后,项目目录就包含了很多生成的文件夹和文件。

  • go.mod/go.sum  - Go Modules 包管理清单,用来描述当前 Operator 的依赖包。
  • main.go 文件,使用 operator-sdk API 初始化和启动当前 Operator 的入口。
  • deploy - 包含一组用于在 Kubernetes 集群上进行部署的通用的 Kubernetes 资源清单文件。
  • pkg/apis - 包含定义的 API 和自定义资源(CRD)的目录树,这些文件允许 sdk 为 CRD 生成代码并注册对应的类型,以便正确解码自定义资源对象。
  • pkg/controller - 用于编写所有的操作业务逻辑的地方
  • version - 版本定义
  • build - Dockerfile 定义目录

我们主要需要编写的是 pkg 目录下面的 api 定义以及对应的 controller 实现。

添加 API

接下来为我们的自定义资源添加一个新的 API,按照上面我们预定义的资源清单文件,在 Operator 相关根目录下面执行如下命令:

$ operator-sdk create api --resource=true --controller=true --group app --version v1 --kind AppService
$ go mod tidy

这里我们添加了一个 group 为 app,版本为 v1的 AppService 的资源对象。

自定义 API

打开源文件 api/v1beta1/appservice_types.go,需要我们根据我们的需求去自定义结构体 AppServiceSpec,我们最上面预定义的资源清单中就有 replicas、image、ports 这些属性,所有我们需要用到的属性都需要在这个结构体中进行定义:

type AppServiceSpec struct {Replicas  *int32                        `json:"replicas"`Image     string                        `json:"image"`Resources corev1.ResourceRequirements   `json:"resources,omitempty"`Envs      []corev1.EnvVar               `json:"envs,omitempty"`Ports     []corev1.ServicePort          `json:"ports,omitempty"`
}

代码中会涉及到一些包名的导入,由于包名较多,所以我们会使用一些别名进行区分,主要的包含下面几个:

import (appsv1 "k8s.io/api/apps/v1"corev1 "k8s.io/api/core/v1"metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

这里的 resources、envs、ports 的定义都是直接引用的 "k8s.io/api/core/v1"中定义的结构体,而且需要注意的是我们这里使用的是 ServicePort,而不是像传统的 Pod 中定义的 ContanerPort,这是因为我们的资源清单中不仅要描述容器的 Port,还要描述 Service 的 Port。

然后一个比较重要的结构体 AppServiceStatus 用来描述资源的状态,当然我们可以根据需要去自定义状态的描述,我这里就偷懒直接使用 Deployment 的状态了:

type AppServiceStatus struct {appsv1.DeploymentStatus `json:",inline"`
}

定义完成后,在项目根目录下面执行如下命令:

$ make
/Users/ych/devs/projects/go/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go build -o bin/manager main.go

该命令会使用我们更新后的资源对象结构重新自动生成一些代码,这样我们就算完成了对自定义资源对象的 API 的声明。

实现业务逻辑

上面 API 描述声明完成了,接下来就需要我们来进行具体的业务逻辑实现了,编写具体的 controller 实现,打开源文件controllers/appservice_controller.go,需要我们去更改的地方也不是很多,核心的就是Reconcile 方法,该方法就是去不断的 watch 资源的状态,然后根据状态的不同去实现各种操作逻辑。

首先 sdk 为我们搭建了一个基本的 reconciler 结构,几乎每一个调谐器器都需要记录日志,并且能够获取对象,所以可以直接使用。

type AppServiceReconciler struct {client.ClientLog    logr.LoggerScheme *runtime.Scheme
}

在appservice_controller.go文件中定义一个全局变量,用于后面的annotations

var oldSpecAnnotation = "old/spec"

Reconcile 实际上是对单个对象进行调谐,我们的 Request 只是有一个名字,但我们可以使用 client 从缓存中获取这个对象。我们返回一个空的结果,没有错误,这就向 controller-runtime 表明我们已经成功地对这个对象进行了调谐,在有一些变化之前不需要再尝试调谐。

大多数控制器需要一个日志句柄和一个上下文,所以我们在 Reconcile 中将他们初始化。上下文是用来允许取消请求的,它是所有 client 方法的第一个参数。

Reconcile协调业务逻辑代码如下。

func (r *AppServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {ctx = context.Background()log := r.Log.WithValues("appservice", req.NamespacedName)// 获取appService crd资源appService := &appv1.AppService{}if err := r.Client.Get(ctx, req.NamespacedName, appService); err != nil {if errors.IsNotFound(err) {return ctrl.Result{}, nil}return ctrl.Result{}, err}// crd 资源标记为删除if appService.DeletionTimestamp != nil {return ctrl.Result{}, nil}log.Info("fetch appservice objects", "appservice", appService)// 如果不存在,则创建关联资源; 如果存在,判断是否需要更新// 如果需要更新,则直接更新; 如果不需要更新,则正常返回oldDeploy := &appsv1.Deployment{}if err := r.Client.Get(ctx, req.NamespacedName, oldDeploy); err != nil {// deployment 不存在,创建if errors.IsNotFound(err) {// 创建deploymentif err := r.Client.Create(ctx, resources.NewDeploy(appService)); err != nil {return ctrl.Result{}, err}// 创建serviceif err := r.Client.Create(ctx, resources.NewService(appService)); err != nil {return ctrl.Result{}, err}// 更新 crd 资源的 Annotationsdata, _ := json.Marshal(appService.Spec)if appService.Annotations != nil {appService.Annotations["spec"] = string(data)} else {appService.Annotations = map[string]string{"spec": string(data)}}if err := r.Client.Update(ctx, appService); err != nil {return ctrl.Result{}, err}} else {return ctrl.Result{}, err}} else {// deployment 存在,更新oldSpec := appv1.AppServiceSpec{}if err := json.Unmarshal([]byte(appService.Annotations["spec"]), &oldSpec); err != nil {return ctrl.Result{}, err}if !reflect.DeepEqual(appService.Spec, oldSpec) {// 更新deploymentnewDeploy := resources.NewDeploy(appService)oldDeploy.Spec = newDeploy.Specif err := r.Client.Update(ctx, oldDeploy); err != nil {return ctrl.Result{}, err}// 更新servicenewService := resources.NewService(appService)oldService := &corev1.Service{}if err := r.Client.Get(ctx, req.NamespacedName, oldService); err != nil {return ctrl.Result{}, err}// 更新 service 必须设置老的 clusterIPclusterIP := oldService.Spec.ClusterIPoldService.Spec = newService.SpecoldService.Spec.ClusterIP = clusterIPif err := r.Client.Update(ctx, oldService); err != nil {return ctrl.Result{}, err}// 更新 crd 资源的 Annotationsdata, _ := json.Marshal(appService.Spec)if appService.Annotations != nil {appService.Annotations["spec"] = string(data)} else {appService.Annotations = map[string]string{"spec": string(data)}}if err := r.Client.Update(ctx, appService); err != nil {return ctrl.Result{}, err}}}return ctrl.Result{}, nil
}

上面就是业务逻辑实现的核心代码,逻辑很简单,就是去判断资源是否存在,不存在,则直接创建新的资源,创建新的资源除了需要创建 Deployment 资源外,还需要创建 Service 资源对象,因为这就是我们的需求,当然你还可以自己去扩展,比如再创建一个 Ingress 对象。更新也是一样的,去对比新旧对象的声明是否一致,如果不一致则需要更新,同样的,两种资源都需要更新的。

另外两个核心的方法就是上面的 resources.NewDeploy(instance) 和 resources.NewService(instance) 方法,这两个方法实现逻辑也很简单,就是根据 CRD 中的声明去填充 Deployment 和 Service 资源对象的 Spec 对象即可。

在groupversion_info.go文件里增加一个Kind全局变量

var (GroupVersion = schema.GroupVersion{Group: "app.example.com", Version: "v1"}Kind         = "AppService"SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}AddToScheme = SchemeBuilder.AddToScheme
)

NewDeploy 方法实现如下:

/resources/deployment.go

NewService 对应的方法实现如下:

/resources/service.go

这样我们就实现了 AppService 这种资源对象的业务逻辑。

调试

测试访问 Kubernetes 集群是否可以:

$ kubectl cluster-info

首先,需要在集群中安装 CRD 对象:

$ make install $ kubectl get crd |grep appservice
appservices.app.example.com             2022-02-27T07:31:10Z

当我们通过 kubectl get crd 命令获取到我们定义的 CRD 资源对象,就证明我们定义的 CRD 安装成功了。其实现在只是 CRD 的这个声明安装成功了,但是我们这个 CRD 的具体业务逻辑实现方式还在我们本地,并没有部署到集群之中,我们可以通过下面的命令来在本地项目中启动 Operator 的调试:

$ make run ENABLE_WEBHOOKS=false

上面的命令会在本地运行 Operator 应用,通过 ~/.kube/config 去关联集群信息,现在我们去添加一个 AppService 类型的资源然后观察本地 Operator 的变化情况,资源清单文件就是我们上面预定义的(config/samples/app_v1_appservice.yaml):

apiVersion: app.ydzs.io/v1beta1
kind: AppService
metadata:name: nginx
spec:replicas: 2image: nginx:1.7.9ports:- port: 80targetPort: 80nodePort: 30002

直接创建这个资源对象:

$ kubectl apply -f config/samples/app_v1_appservice.yaml

我们可以看到我们的应用创建成功了,这个时候查看 Operator 的调试窗口会有如下的信息出现:

......
1.646208501373661e+09	INFO	controllers.AppService	fetch appservice objects	{"appservice": "default/nginx", "appservice": {"apiVersion": "app.example.com/v1", "kind": "AppService", "namespace": "default", "name": "nginx"}}
1.6462085017729385e+09	INFO	controllers.AppService	fetch appservice objects	{"appservice": "default/nginx", "appservice": {"apiVersion": "app.example.com/v1", "kind": "AppService", "namespace": "default", "name": "nginx"}}
......

然后我们可以去查看集群中是否有符合我们预期的资源出现:

$ kubectl get AppService
NAME        AGE
nginx       2m8s$ kubectl get deploy
NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
nginx   p                2/2     2            2           2m20s$ kubectl get svc   
NAME             TYPE           CLUSTER-IP       EXTERNAL-IP             PORT(S)          AGE
nginx            NodePort       10.111.179.0     <none>                  80:30002/TCP     2m23s

看到了吧,我们定义了两个副本(replicas=2),这里就出现了两个 Pod,还有一个 NodePort=30002 的 Service 对象.

如果应用在安装过程中出现了任何问题,我们都可以通过本地的 Operator 调试窗口找到有用的信息,然后调试修改即可。

清理:

$ kubectl delete -f config/samples/app_v1_appservice.yaml
$ make uninstall

部署

自定义的资源对象现在测试通过了,但是如果我们将本地的调试控制器终止掉,我们可以猜想到就没办法处理 AppService 资源对象的一些操作了,所以我们需要将我们的业务逻辑实现部署到集群中去。

执行下面的命令构建 Operator 应用打包成 Docker 镜像:

Dockerfile 需要更改为这样:

# Build the manager binary
FROM golang:1.17 as builderWORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN export GOPROXY=https://goproxy.cn && go mod download# Copy the go source
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/
COPY resources/ resources/# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager main.go# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM golang:1.17
WORKDIR /
COPY --from=builder /workspace/manager .
#USER 65532:65532ENTRYPOINT ["/manager"]

执行命令构建

$ make docker-build IMG=registry.cn-hangzhou.aliyuncs.com/test-operator/my-operator:v1.0.0

镜像构建成功后,推送到自己的registry

$ make docker-push IMG=registry.cn-hangzhou.aliyuncs.com/test-operator/my-operator:v1.0.0

config/manager/manager.yaml里的镜像名改为你打包后的镜像名

apiVersion: v1
kind: Namespace
metadata:labels:control-plane: controller-managername: system
---
apiVersion: apps/v1
kind: Deployment
metadata:name: controller-managernamespace: systemlabels:control-plane: controller-manager
spec:selector:matchLabels:control-plane: controller-managerreplicas: 1template:metadata:annotations:kubectl.kubernetes.io/default-container: managerlabels:control-plane: controller-managerspec:securityContext:runAsNonRoot: truecontainers:- command:- /managerargs:- --leader-electimage: registry.cn-hangzhou.aliyuncs.com/test-operator/my-operator:v1.0.0name: managersecurityContext:allowPrivilegeEscalation: falselivenessProbe:httpGet:path: /healthzport: 8081initialDelaySeconds: 15periodSeconds: 20readinessProbe:httpGet:path: /readyzport: 8081initialDelaySeconds: 5periodSeconds: 10# TODO(user): Configure the resources accordingly based on the project requirements.# More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/resources:limits:cpu: 500mmemory: 128Mirequests:cpu: 10mmemory: 64MiserviceAccountName: controller-managerterminationGracePeriodSeconds: 10

修改config/default/manager_auth_proxy_patch.yaml文件,将kube-rbac镜像地址改成国内的。

containers:
- name: kube-rbac-proxyimage: kubesphere/kube-rbac-proxy:v0.8.0

部署operator

编辑config/rbac/role_binding.yaml文件,绑定controller 到 cluster-admin 集群管理员角色。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:name: manager-rolebinding
roleRef:apiGroup: rbac.authorization.k8s.iokind: ClusterRole#name: manager-rolename: cluster-admin
subjects:- kind: ServiceAccountname: controller-managernamespace: system

现在,部署 Operator,本质上用的是kustomize工具构建:

$ touch config/rbac/role.yaml   # 创建一个空文件,否则部署会报错$ make deploy IMG=registry.cn-hangzhou.aliyuncs.com/test-operator/my-operator:v1.0.0
/root/my-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
cd config/manager && /root/my-operator/bin/kustomize edit set image controller=registry.cn-hangzhou.aliyuncs.com/test-operator/my-operator:v1.0.0
/root/my-operator/bin/kustomize build config/default | kubectl apply -f -
namespace/my-operator-system created
customresourcedefinition.apiextensions.k8s.io/appservices.app.example.com created
serviceaccount/my-operator-controller-manager created
role.rbac.authorization.k8s.io/my-operator-leader-election-role created
clusterrole.rbac.authorization.k8s.io/my-operator-metrics-reader created
clusterrole.rbac.authorization.k8s.io/my-operator-proxy-role created
rolebinding.rbac.authorization.k8s.io/my-operator-leader-election-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/my-operator-manager-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/my-operator-proxy-rolebinding created
configmap/my-operator-manager-config created
service/my-operator-controller-manager-metrics-service created
deployment.apps/my-operator-controller-manager created

查看operator部署是否成功:

# kubectl get po -A | grep my-operator
my-operator-system   my-operator-controller-manager-b5f8bcb58-7nqkv         2/2     Running   0          11h# kubectl get svc -A | grep my-operator
my-operator-system   my-operator-controller-manager-metrics-service   ClusterIP      172.21.2.239    <none>            8443/TCP                                                                  11h

部署 crd资源测试。可以看到我们自定义的deployment、service资源已经按照需求创建成功了:

$ kubectl apply -f config/samples/app_v1_appservice.yaml$ kubectl get deploy |grep nginx
nginx            2/2     2            2           46s$ kubectl get pod |grep nginx
nginx-5bf87f5f59-knbdw                           1/1     Running   0          51s
nginx-5bf87f5f59-lblx6                           1/1     Running   0          51s$ kubectl get svc |grep nginx
nginx        NodePort    172.21.0.149   <none>        80:30002/TCP   57s

其他信息

删除 CRD 自定义资源

$ kubectl delete -f config/samples/app_v1_appservice.yaml

删除 CRD 定义

$ make uninstall

删除 controller

$ make undeploy

到这里我们的 CRD 和 Operator 实现都已经安装成功了。

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

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

相关文章

GIS在地质灾害危险性评估与灾后重建中的应用

地质灾害是指全球地壳自然地质演化过程中&#xff0c;由于地球内动力、外动力或者人为地质动力作用下导致的自然地质和人类的自然灾害突发事件。由于降水、地震等自然作用下&#xff0c;地质灾害在世界范围内频繁发生。我国除滑坡灾害外&#xff0c;还包括崩塌、泥石流、地面沉…

GSA、GSEA、ssGSEA、GSVA用到的统计学知识点

文章目录 概率密度函数&#xff08;probability density function&#xff0c;PDF&#xff09;分布函数&#xff08;Cumulative Distribution Function&#xff0c;CDF&#xff09;核密度估计&#xff08;KDE&#xff09;经验累计分布函数&#xff08;Empirical Cumulative Dis…

source not found for数组a的引用Exception

identityHashCode(数组名&#xff09; adressidentityHashCode(a)//将得到数组a的哈希值。在同一个数据类型下&#xff0c;同一对象的哈希值是一样的&#xff0c;同一哈希值对应同一对象。由于不同数据类型的变量哈希值生成算法不同&#xff0c;所以在数据类型不同的情况下&am…

关于esp8266的一些经验汇总,新手必看

说实话&#xff0c;esp8266的nodemcu 已经使用了2年多了&#xff0c;各种问题遇到过&#xff0c;就尝试各种解决&#xff0c;而现在回头来看真的是稀里糊涂的在用&#xff0c;当然这个问题也同样涉及到esp32. 因为最近打算自己打一块esp8266的板&#xff0c;之前打的比较多的是…

Redis冲冲冲——Redis分布式锁如何实现

目录 引出Redis分布式锁如何实现Redis入门1.Redis是什么&#xff1f;2.Redis里面存Java对象 Redis进阶1.雪崩/ 击穿 / 穿透2.Redis高可用-主从哨兵3.持久化RDB和AOF4.Redis未授权访问漏洞5.Redis里面安装BloomFilte Redis的应用1.验证码2.Redis高并发抢购3.缓存预热用户注册验证…

从根到叶:深入理解二叉搜索树

我们的心永远向前憧憬 尽管活在阴沉的现在 一切都是暂时的,转瞬即逝, 而那逝去的将变为可爱 &#x1f31d;(俄) 普希金 <假如生活欺骗了你> 1.二叉搜索树的概念 概念:搜索树&#xff08;Search Tree&#xff09;是一种有序的数据结构&#xff0c;用于存储和组…

Facebook商城号为什么被封?防封养号技巧

由于Facebook商城的高利润空间&#xff0c;越来越多的跨境电商商家注意到它的存在。Facebook作为全球最大、用户量最大的社媒平台&#xff0c;同时也孕育了一个巨大的商业生态&#xff0c;包括广告投放、商城交易等。依托背后的大流量&#xff0c;Facebook商城起号较快&#xf…

优思学院|拉丁方实验设计是什么?

今天&#xff0c;我要给大家带来一个六西格玛实验设计的小窍门——拉丁方设计。这是一种巧妙的方式&#xff0c;帮助我们探索不同因素&#xff08;输入&#xff09;对结果&#xff08;输出&#xff09;的影响&#xff0c;同时巧妙地处理那些我们不想要的“噪音因素”。 想象一…

HEUFT电源维修x-ray发生器维修HBE211226

HEUFT电源维修x-ray发生器维修HBE211253;海富HEUFT在线液位检测X射线发生器维修&#xff0c;不限型型号系列。 德国海富推出HEUFT在线液位检测装置,满瓶检测系统HEUFT有着强大的功能,它的模块机构能整合很多程序,并依据不同的产品及其包装特性,照相技术,高频技术或X-ray技术。…

LInux-多线程基础概念

文章目录 前言预备页表详解缺页中断页表的映射 一、多线程是什么&#xff1f;轻量级进程 二、Pthread库pthread_create 前言 从本章的多线程开始&#xff0c;我们开始进入Linux系统的尾声&#xff0c;所以&#xff0c;在学习多线程的过程中&#xff0c;我们也会逐步对之前的内…

邮件营销新手必读指南?怎样做好邮件营销?

邮件营销的全流程及步骤&#xff1f;做好邮件营销有哪些注意点&#xff1f; 邮件营销作为一种传统却依然高效的推广手段&#xff0c;被众多企业所青睐。对于新手来说&#xff0c;如何开展邮件营销&#xff0c;却是一个值得探讨的话题。AokSend将为你提供一份邮件营销新手必读指…

国家妇女节放假是法定的假日

在这个充满活力和希望的春天&#xff0c;我们迎来了一个特殊的节日——国家妇女节。这是一个属于所有女性的节日&#xff0c;是一个庆祝女性成就、关爱女性权益的时刻。在这个特殊的日子里&#xff0c;我们不禁要问&#xff1a;国家妇女节放假是法定假日吗&#xff1f;让我们一…

Ps:辅助类工具组

工具箱里的辅助类工具为图像编辑和设计提供了重要的支持&#xff0c;能帮助用户更准确地测量、选取颜色或添加注释等。 快捷键&#xff1a;I 吸管工具 Eyedropper Tool 用于从图像中采样颜色。 用户可以通过点击屏幕上的任何点来选取那里的颜色&#xff0c;该颜色随即成为当前的…

Java+SpringBoot+Vue+MySQL实战:打造智能餐厅点餐系统

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

消息队列以及Kafka的使用

什么是消息队列 消息队列&#xff1a;一般我们会简称它为MQ(Message Queue)。其主要目的是通讯。 ps&#xff1a;消息队列是以日志的形式将数据顺序存储到磁盘当中。通常我们说从内存中IO读写数据的速度要快于从硬盘中IO读写的速度是对于随机的写入和读取。但是对于这种顺序存…

2024年:AI领航研发新纪元

导言&#xff1a; 在21世纪的科技巨浪中&#xff0c;人工智能&#xff08;AI&#xff09;已经崭露头角&#xff0c;成为研发领域的核心变革者。其强大的潜力和无所不在的应用正在改变人类解决问题的方式&#xff0c;为未来的发展开启了无限可能。随着机器学习、自然语言处理、计…

周立功USBCAN-E-mini分析仪的安装测试笔记

一、介绍 USBCAN-E-mini 智能 CAN 接口卡是系列 USBCAN 便携版本&#xff0c;与 USBCAN—E-U 单路智能 CAN 接口卡完全兼容。USBCAN-E-mini 智能 CAN 接口卡与 USB1.1 总线兼容的&#xff0c;集成 1 路 CAN 接口的智能型 CAN-bus 总线通讯接口卡。采用 USBCAN-E-mini 智能 CAN …

6、string字符串拼接

#include <iostream> using namespace std;void test01 () {string s1 "我";s1 "爱玩游戏";cout << s1 << endl;s1 :;string s2 "lol dnf";s1 s2;cout << s1 << endl;string s3 "i";s3.append(&q…

Vue 使用@别名

1. 安装 types/node types/node 包允许您在TypeScript项目中使用Node.js的核心模块和API&#xff0c;并提供了对它们的类型检查和智能提示的支持。 npm install types/node --save-dev 比如安装之后&#xff0c;就可以导入nodejs的 path模块&#xff0c;在下面代码 import pat…

【ICCV】AIGC时代下的SOTA人脸表征提取器TransFace,FaceChain团队出品

一、论文 本文介绍被计算机视觉顶级国际会议ICCV 2023接收的论文 "TransFace: Calibrating Transformer Training for Face Recognition from a Data-Centric Perspective" 论文链接&#xff1a;https://arxiv.org/abs/2308.10133 开源代码&#xff1a;https://an…