日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Kubernetes operator 模式开发实践

發布時間:2024/8/23 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Kubernetes operator 模式开发实践 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

0. 前言

近日我們在開發符合我們業務自身需求的微服務平臺時,使用了 Kubernetes 的 Operator Pattern 來實現其中的運維系統,在本文,我們將實現過程中積累的主要知識點和技術細節做了一個整理。

讀者在閱讀完本文之后,會對 Operator Pattern 有一個基本的了解,并能將該模式應用到自己的業務中去。除此之外,我們也會分享要實現這一運維系統需要具備的一些相關知識。
注:閱讀本文內容需要對 Kubernetes 和 Go 語言有基本了解。

1. 什么是 Operator Pattern

在解釋什么是 Operator Pattern 之前,我們得先了解在我們使用一個 Kubernetes 客戶端——這里以 kubectl 舉例——向 Kubernetes 集群發出指令,直到這項指令被 Kubernetes 集群執行結束,這段時間之內到底都發生了什么。
這里以我們輸入_ kubectl create -f ns-my-workspace.yaml_ 這條命令舉例,這條命令的整條執行鏈路大致如下圖所示:

### ns-my-workspace.yaml apiVersion: v1 kind: Namespace metadata:name: my-workspace

如上圖所示,所有在 Kubernetes 集群中的組件交互都是通過 RESTful API 的形式完成的,包括第一步的控制器監聽操作,以及第二步中 kubectl 發送的指令。雖說我們執行的是 _kubectl create -f ns-my-workspace.yaml _指令,但其實 kubectl 會向「API服務器」發送一個 POST 請求:

curl --request POST \--url http://${k8s.host}:${k8s.port}/api/v1/namespaces \--header 'content-type: application/json' \--data '{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"my-workspace"} }'

如上面的 cURL 指令,Kubernetes API 服務器接受的其實是 JSON 數據類型,而并非是 YAML。
然后所有這些創建的 resources 都會持久化到 etcd 組件中,「API服務器」也是 Kubernetes 集群中與「etcd」交互的唯一一個組件。
之后被創建的 my-workspace resource 就會被發送給監聽了 namespaces resource 變更的 「Namespace控制器」中,最后就由「Namespace控制器」執行創建 my-workspace 命名空間的具體操作。那么同理,當創建 ReplicaSet resource 時就會由「ReplicaSet控制器」做具體執行,當創建 Pod 時,則會由「Pod控制器」具體執行,其他類型的 resource 與之類似,這些控制器共同組成了上圖中的「Kubernetes API 控制器集合」。

說到這里,我們不難發現,實現 Kubernetes 中某一種領域類型——如上面提到的 Namespace、ReplicaSet、Pod,也即 Kubernetes 中的 Kind——的操作邏輯,需要具備兩個因素:

  • 對該領域類型的模型抽象,如上面的 ns-my-workspace.yaml 文件描述的 YAML 數據結構,這個抽象決定了 Kubernetes client 發送到 Kubernetes API server 的 RESTful API 請求,也描述了這個領域類型本身。
  • 實際去處理這個領域類型抽象的控制器,如上面的「Namespace控制器」、「ReplicaSet控制器」、「Pod控制器」,這些控制器實現了這個抽象描述的具體業務邏輯,并通過 RESTful API 提供這些服務。
  • 而當 Kubernetes 開發者需要擴展 Kubernetes 能力時,也可以遵循這種模式,即提供一份對想要擴展的能力的抽象,和實現了這個抽象具體邏輯的控制器。前者稱作 CRD(Custom Resource Definition),后者稱作 Controller。
    Operator pattern 就是通過這種方式實現 Kubernetes 擴展性的一種模式,Operator 模式認為可以將一個領域問題的解決方案想像成是一個「操作者」,這個操作者在用戶和集群之間,通過一份份「訂單」,去操作集群的API,來達到完成這個領域各種需求的目的。這里的訂單就是 CR(Custom Resource,即 CRD 的一個實例),而操作者就是控制器,是具體邏輯的實現者。之所以強調是 operator,而不是計算機領域里傳統的 server 角色,則是因為?operator 本質上不創造和提供新的服務,他只是已有 Kubernetes API service 的組合
    而本文實踐的「運維系統」,就是一個為了解決運維領域問題,而實現出來的 operator。

    2. Operator Pattern 實戰

    在本節我們會通過使用 kubebuilder 工具,構建一個 Kubernetes Operator,在本節之后,我們會在自己的 Kubernetes 集群中獲得一個 CRD 和其對應的 Kubernetes API 控制器,用于簡單的部署一個微服務。即當我們 create 如下 YAML 時:

    apiVersion: devops.my.domain/v1 kind: DemoMicroService metadata:name: demomicroservice-sample spec:image: stefanprodan/podinfo:0.0.1

    可得到一個 Kubernetes 部署實例:

    本節所有示例代碼均提供在:https://github.com/l4wei/kubebuilder-example

    2.1 Kubebuilder 實現

    Kubebuilder(https://github.com/kubernetes-sigs/kubebuilder)是一個用 Go 語言構建 Kubernetes APIs 控制器和 CRD 的腳手架工具,通過使用 kubebuilder,用戶可以遵循一套簡單的編程框架,使用 Go 語言方便的實現一個 operator。

    2.1.1 安裝

    在安裝 kubebuilder 之前,需要先安裝 Go 語言和 kustomize,并確保可以正常使用。
    kustomize 是一個可定制化生成 Kubernetes YAML Configuration 文件的工具,你可以通過遵循一套 kustomize 的配置,批量的生成你需要的?Kubernetes YAML 配置。kubebuilder 使用了 kustomize 去生成控制器所需的一些 YAML 配置。mac 用戶可使用 brew 方便地安裝 kustomize。
    然后使用使用下面的腳本安裝 kubebuilder:

    os=$(go env GOOS) arch=$(go env GOARCH)# download kubebuilder and extract it to tmp curl -L https://go.kubebuilder.io/dl/2.2.0/${os}/${arch} | tar -xz -C /tmp/# move to a long-term location and put it on your path # (you'll need to set the KUBEBUILDER_ASSETS env var if you put it somewhere else) sudo mv /tmp/kubebuilder_2.2.0_${os}_${arch} /usr/local/kubebuilder export PATH=$PATH:/usr/local/kubebuilder/bin

    使用 kubebuilder -h 若能看到幫助文檔,則表示 kubebuilder 安裝成功。

    2.1.2 創建工程

    使用下面的腳本創建一個 kubebuilder 工程:

    mkdir example cd example go mod init my.domain/example kubebuilder init --domain my.domain

    上述命令的?my.domain?一般是你所在機構的域名,_example_ 一般是你這個 Go 語言項目的項目名。根據這樣的設定,如果這個 Go 項目作為一個模塊要被其他 Go 項目依賴,那么一般命名為?my.domain/example_。
    如果你的 example 目錄建立在 ${GOPATH} 目錄之下,那么就不需要 _go mod init my.domain/example?這條命令,Go 語言也能找到該 example 目錄下的 go pkg。
    然后確保以下兩條命令在你的開發機器上被執行過:

    export GO111MODULE=on sudo chmod -R 777 ${GOPATH}/go/pkg

    以上兩條命令的執行可以解決在開發時可能出現的cannot find package ... (from $GOROOT)這種問題。
    在創建完工程之后,你的 example 目錄結構會大致如下:

    . ├── Dockerfile ├── Makefile ├── PROJECT ├── bin │?? └── manager ├── config │?? ├── certmanager │?? │?? ├── certificate.yaml │?? │?? ├── kustomization.yaml │?? │?? └── kustomizeconfig.yaml │?? ├── default │?? │?? ├── kustomization.yaml │?? │?? ├── manager_auth_proxy_patch.yaml │?? │?? ├── manager_webhook_patch.yaml │?? │?? └── webhookcainjection_patch.yaml │?? ├── manager │?? │?? ├── kustomization.yaml │?? │?? └── manager.yaml │?? ├── prometheus │?? │?? ├── kustomization.yaml │?? │?? └── monitor.yaml │?? ├── rbac │?? │?? ├── auth_proxy_role.yaml │?? │?? ├── auth_proxy_role_binding.yaml │?? │?? ├── auth_proxy_service.yaml │?? │?? ├── kustomization.yaml │?? │?? ├── leader_election_role.yaml │?? │?? ├── leader_election_role_binding.yaml │?? │?? └── role_binding.yaml │?? └── webhook │?? ├── kustomization.yaml │?? ├── kustomizeconfig.yaml │?? └── service.yaml ├── go.mod ├── go.sum ├── hack │?? └── boilerplate.go.txt └── main.go

    上面目錄中的 bin 目錄下的 manager 即工程編譯出的二進制可執行文件,也就是這個控制器的可執行文件。
    config 目錄下都是 kustomize 的配置,例如 config/manager 目錄下面的文件即生成控制器部署 YAML 配置文件的 kustomize 配置,如果你執行下面的指令:

    kustomize build config/manager

    就能看到 kustomize 生成的 YAML 配置:

    apiVersion: v1 kind: Namespace metadata:labels:control-plane: controller-managername: system --- apiVersion: apps/v1 kind: Deployment metadata:labels:control-plane: controller-managername: controller-managernamespace: system spec:replicas: 1selector:matchLabels:control-plane: controller-managertemplate:metadata:labels:control-plane: controller-managerspec:containers:- args:- --enable-leader-electioncommand:- /managerimage: controller:latestname: managerresources:limits:cpu: 100mmemory: 30Mirequests:cpu: 100mmemory: 20MiterminationGracePeriodSeconds: 10

    上面就是將 bin/manager 部署到 Kubernetes 集群的 YAML configurations。

    2.1.3 創建API

    上面創建的工程僅僅只是一個空殼,還沒有提供任何的 Kubernetes API,也不能處理任何的 CR。使用下面的腳本創建一個 Kubernetes API:

    kubebuilder create api --group devops --version v1 --kind DemoMicroService

    上述命令的 group 將與之前創建工程時輸入的 domain 共同組成你創建的 Kubernetes API YAML resource 里 apiVersion 字段的前半部分,上面的 version 即后半部分,所以你自定義的 resource YAML 里的 apiVersion 就應該寫作:devops.my.domain/v1。上面的 kind 就是你自定義 resource 里的 kind 字段。通過該條指令創建的 resource 看起來正如 kubebuilder 創建的 config/samples/devops_v1_demomicroservice.yaml 文件一樣:

    apiVersion: devops.my.domain/v1 kind: DemoMicroService metadata:name: demomicroservice-sample spec:# Add fields here foo: bar

    輸入該命令會提示你是否創建 Resource(即 CRD),是否創建 Controller(即控制器),全部輸入「y」同意即可。
    在執行完該命令之后,你的工程結構將變成這樣:

    . ├── Dockerfile ├── Makefile ├── PROJECT ├── api │?? └── v1 │?? ├── demomicroservice_types.go │?? ├── groupversion_info.go │?? └── zz_generated.deepcopy.go ├── bin │?? └── manager ├── config │?? ├── certmanager │?? │?? ├── certificate.yaml │?? │?? ├── kustomization.yaml │?? │?? └── kustomizeconfig.yaml │?? ├── crd │?? │?? ├── kustomization.yaml │?? │?? ├── kustomizeconfig.yaml │?? │?? └── patches │?? │?? ├── cainjection_in_demomicroservices.yaml │?? │?? └── webhook_in_demomicroservices.yaml │?? ├── default │?? │?? ├── kustomization.yaml │?? │?? ├── manager_auth_proxy_patch.yaml │?? │?? ├── manager_webhook_patch.yaml │?? │?? └── webhookcainjection_patch.yaml │?? ├── manager │?? │?? ├── kustomization.yaml │?? │?? └── manager.yaml │?? ├── prometheus │?? │?? ├── kustomization.yaml │?? │?? └── monitor.yaml │?? ├── rbac │?? │?? ├── auth_proxy_role.yaml │?? │?? ├── auth_proxy_role_binding.yaml │?? │?? ├── auth_proxy_service.yaml │?? │?? ├── demomicroservice_editor_role.yaml │?? │?? ├── demomicroservice_viewer_role.yaml │?? │?? ├── kustomization.yaml │?? │?? ├── leader_election_role.yaml │?? │?? ├── leader_election_role_binding.yaml │?? │?? └── role_binding.yaml │?? ├── samples │?? │?? └── devops_v1_demomicroservice.yaml │?? └── webhook │?? ├── kustomization.yaml │?? ├── kustomizeconfig.yaml │?? └── service.yaml ├── controllers │?? ├── demomicroservice_controller.go │?? └── suite_test.go ├── go.mod ├── go.sum ├── hack │?? └── boilerplate.go.txt └── main.go

    比起創建 API 之前,增加了:

    • api 目錄——即定義了你創建的 Kubernetes API 的數據結構代碼。
    • controllers 目錄——即控制器的實現代碼。
    • config/crd 目錄——該目錄里的 kustomize 配置可生成你要定義的 CRD 的 YAML 配置。

    輸入以下命令:

    make manifests

    即可在?config/crd/bases/devops.my.domain_demomicroservices.yaml?文件里看到你創建該 Kubernetes API 時創建的 CRD:

    --- apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata:annotations:controller-gen.kubebuilder.io/version: v0.2.4creationTimestamp: nullname: demomicroservices.devops.my.domain spec:group: devops.my.domainnames:kind: DemoMicroServicelistKind: DemoMicroServiceListplural: demomicroservicessingular: demomicroservicescope: Namespacedvalidation:openAPIV3Schema:description: DemoMicroService is the Schema for the demomicroservices APIproperties:apiVersion:description: 'APIVersion defines the versioned schema of this representationof an object. Servers should convert recognized schemas to the latestinternal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'type: stringkind:description: 'Kind is a string value representing the REST resource thisobject represents. Servers may infer this from the endpoint the clientsubmits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'type: stringmetadata:type: objectspec:description: DemoMicroServiceSpec defines the desired state of DemoMicroServiceproperties:foo:description: Foo is an example field of DemoMicroService. Edit DemoMicroService_types.goto remove/updatetype: stringtype: objectstatus:description: DemoMicroServiceStatus defines the observed state of DemoMicroServicetype: objecttype: objectversion: v1versions:- name: v1served: truestorage: true status:acceptedNames:kind: ""plural: ""conditions: []storedVersions: []

    2.1.4 API屬性定義

    Kubernetes API 創建好了,現在我們需要定義該 API 的屬性,這些屬性才真正描述了創建出的 CRD 的抽象特征。
    在我們這個 DemoMicroService Kind 例子中,我們只簡單的抽象出一個微服務的部署 CRD,所以我們這個 CRD 只有一個屬性,即該服務的容器鏡像地址。
    為此,我們只需要修改 api/v1/demomicroservice_types.go 文件:

    上面的 git diff 顯示我們將原來示例的 Foo 屬性改成了我們需要的 Image 屬性。對 API 屬性的定義和對 CRD 的定義基本上只需要修改該文件即可。
    再次執行:

    make manifests

    即可看到生成的 CRD resource 發生了變更,這里不再贅述。
    現在我們也修改一下 config/samples/devops_v1_demomicroservice.yaml 文件,后面需要使用該文件測試我們實現的控制器:

    apiVersion: devops.my.domain/v1 kind: DemoMicroService metadata:name: demomicroservice-sample spec:image: stefanprodan/podinfo:0.0.1

    2.1.5 控制器邏輯實現

    CRD 定義好了,現在開始實現控制器。
    我們在這次示例中要實現的控制器邏輯非常簡單,基本可以描述成:

  • 當我們執行 kubectl create -f config/samples/devops_v1_demomicroservice.yaml 時,控制器會在集群中創建一個 Kubernetes Deployment resource,用于實現該 DemoMicroService 的部署。
  • 當我們執行?kubectl delete -f config/samples/devops_v1_demomicroservice.yaml 時,控制器會將在集群中創建的 Deployment resource 刪掉,表示該 DemoMicroService 的下線。
  • 2.1.5.1 部署的實現

    寫代碼之前,我們需要先了解 kubebuilder 程序的開發方式。
    因為我們要實現的是 DemoMicroService 的控制器,所以我們需要先將注意力集中在?_controllers/demomicroservice_controller.go_ 文件,如果不是復雜的功能,通常我們只需改該文件即可。而在文件中,我們最需要關心的就是 Reconcile 方法:

    func (r *DemoMicroServiceReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {_ = context.Background()_ = r.Log.WithValues("demomicroservice", req.NamespacedName)// your logic herereturn ctrl.Result{}, nil }

    簡單來說,每當 kubernetes 集群監控到 DemoMicroService CR 的變化時,都會調用到這個 Reconcile 方法,并將變更的 DemoMicroService resource name 及其所在的 namespace 作為 Reconcile 方法的參數,用于定位到變更的 resource。即上面的 req 參數,該參數的結構為:

    type Request struct {// NamespacedName is the name and namespace of the object to reconcile.types.NamespacedName } type NamespacedName struct {Namespace stringName string }

    熟悉前端開發的朋友可能會聯想到 React 的開發方式,兩者確實很像,都是監聽對象的變化,再根據監聽對象的變化來執行一些邏輯。不過 kubebuilder 做的更加極端,他沒有抽象出生命周期的概念,只提供一個 Reconcile 方法,開發者需要自己在這個方法中判斷出 CRD 的生命周期,并在不同的生命周期中執行不同的邏輯
    以下的代碼實現了部署的功能:

    func (r *DemoMicroServiceReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {ctx := context.Background()log := r.Log.WithValues("demomicroservice", req.NamespacedName)dms := &devopsv1.DemoMicroService{}if err := r.Get(ctx, req.NamespacedName, dms); err != nil {if err := client.IgnoreNotFound(err); err == nil {log.Info("此時沒有找到對應的 DemoMicroService resource, 即此處進入了 resource 被刪除成功后的生命周期")return ctrl.Result{}, nil} else {log.Error(err, "不是未找到的錯誤,那么就是意料之外的錯誤,所以這里直接返回錯誤")return ctrl.Result{}, err}}log.Info("走到這里意味著 DemoMicroService resource 被找到,即該 resource 被成功創建,進入到了可根據該 resource 來執行邏輯的主流程")podLabels := map[string]string{"app": req.Name,}deployment := appv1.Deployment{TypeMeta: metav1.TypeMeta{Kind: "Deployment",APIVersion: "apps/v1",},ObjectMeta: metav1.ObjectMeta{Name: req.Name,Namespace: req.Namespace,},Spec: appv1.DeploymentSpec{Selector: &metav1.LabelSelector{MatchLabels: podLabels,},Template: corev1.PodTemplateSpec{ObjectMeta: metav1.ObjectMeta{Labels: podLabels,},Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: req.Name,Image: dms.Spec.Image,ImagePullPolicy: "Always",Ports: []corev1.ContainerPort{{ContainerPort: 9898,},},},},},},},}if err := r.Create(ctx, &deployment); err != nil {log.Error(err, "創建 Deployment resource 出錯")return ctrl.Result{}, err}return ctrl.Result{}, nil }

    上述代碼展現了生命周期中的兩個階段:即第8行中代表 DemoMicroService resource 被成功刪除之后的階段,此時我們什么都沒有做。以及在第16行,進入到創建/更新完 DemoMicroService resource 之后的階段,此時我們構建了一個 Deployment resource,并將其創建到了 Kubernetes 集群中。
    這段代碼有個問題,即無法實現 DemoMicroService resource 的更新,如果同一個?DemoMicroService resource 的 spec.image 被改變了,那么在上述代碼中會再次 create 相同的 Deployment resource,這會導致一個 "already exists" 的報錯。這里為了方便說明開發邏輯,沒有處理這個問題,請讀者注意。

    2.1.5.1 下線的實現

    其實在上一節我們說明部署邏輯的時候,就能實現下線的邏輯:我們只需在「刪除成功后」的生命周期階段將創建的 Deployment 刪掉即可。但是這樣做有一個問題,我們是在?DemoMicroService resource 刪除成功之后再刪的 Deployment,如果刪除 Deployment 的邏輯出錯了,沒有將 Deployment 刪除成功,那么就會出現 Deployment 還在,DemoMicroService 卻不再的情況,如果我們需要用?DemoMicroService 管理 Deployment,那么這就不是我們想要的結果。
    所以我們最好在?DemoMicroService 真正消失之前(即「刪除 DemoMicroService」到「DemoMicroService 完全消失」這段時間)去刪除 Deployment,那么要怎么做呢?請看下面的代碼示例:

    const (demoMicroServiceFinalizer string = "demomicroservice.finalizers.devops.my.domain" )func (r *DemoMicroServiceReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {ctx := context.Background()log := r.Log.WithValues("demomicroservice", req.NamespacedName)dms := &devopsv1.DemoMicroService{}if err := r.Get(ctx, req.NamespacedName, dms); err != nil {if err := client.IgnoreNotFound(err); err == nil {log.Info("此時沒有找到對應的 DemoMicroService resource, 即此處進入了 resource 被刪除成功后的生命周期")return ctrl.Result{}, nil} else {log.Error(err, "不是未找到的錯誤,那么就是意料之外的錯誤,所以這里直接返回錯誤")return ctrl.Result{}, err}}if dms.ObjectMeta.DeletionTimestamp.IsZero() {log.Info("進入到 apply 這個 DemoMicroService CR 的邏輯")log.Info("此時必須確保 resource 的 finalizers 里有控制器指定的 finalizer")if !util.ContainsString(dms.ObjectMeta.Finalizers, demoMicroServiceFinalizer) {dms.ObjectMeta.Finalizers = append(dms.ObjectMeta.Finalizers, demoMicroServiceFinalizer)if err := r.Update(ctx, dms); err != nil {return ctrl.Result{}, err}}if _, err := r.applyDeployment(ctx, req, dms); err != nil {return ctrl.Result{}, nil}} else {log.Info("進入到刪除這個 DemoMicroService CR 的邏輯")if util.ContainsString(dms.ObjectMeta.Finalizers, demoMicroServiceFinalizer) {log.Info("如果 finalizers 被清空,則該 DemoMicroService CR 就已經不存在了,所以必須在次之前刪除 Deployment")if err := r.cleanDeployment(ctx, req); err != nil {return ctrl.Result{}, nil}}log.Info("清空 finalizers,在此之后該 DemoMicroService CR 才會真正消失")dms.ObjectMeta.Finalizers = util.RemoveString(dms.ObjectMeta.Finalizers, demoMicroServiceFinalizer)if err := r.Update(ctx, dms); err != nil {return ctrl.Result{}, err}}return ctrl.Result{}, nil }

    為了方便展示主邏輯,我將創建 Deployment 和刪除 Deployment 的代碼封裝到 applyDeployment 和 cleanDeployment 兩個方法中了。
    如上面代碼所示,想要判斷「刪除 DemoMicroService」到「DemoMicroService 完全消失」這段生命周期的階段,關鍵點在于判斷?DemoMicroService.ObjectMeta.DeletionTimestam 和 DemoMicroService.ObjectMeta.Finalizers 這兩個元信息。前者表示「刪除 DemoMicroService」這一行為的具體發生時間,如果不為0則表示「刪除 DemoMicroService」指令已經下達;而后者表示在真正刪除 DemoMicroService 之前,即「DemoMicroService 完全消失」之前,還有哪些邏輯沒有被執行,如果 Finalizers 不為空,那么該?DemoMicroService 則不會真正消失。
    任何 resource 的 ObjectMeta.Finalizers 都是一個字符串的列表,每一個字符串都表示一段 pre-delete 邏輯尚未被執行。如上面的「demomicroservice.finalizers.devops.my.domain」所示,表示著?DemoMicroService 控制器對?DemoMicroService resource 的 pre-delete 邏輯,該?Finalizer 會在?DemoMicroService 被創建之后,迅速被?DemoMicroService 控制器給種上,并在下達「刪除 DemoMicroService」指令后,且 pre-delete 邏輯(在這里即刪除 Deployment)被正確執行完后,再由?DemoMicroService 控制器將該?Finalizer 抹除。至此?DemoMicroService 上的 Finalizers 為空,此時 Kubernetes 才會讓該?DemoMicroService 完全消失。Kubernetes 正是通過這種機制創造出了 resource 的「對該 resource 下達刪除指令」到「該 resource 完全消失」這段生命周期的階段。
    如果因為各種原因導致?Finalizers 不可能為空,那么會發生什么?答案是會導致這個 resource 永遠無法被刪掉,如果你使用 kubectl delete 去刪,那么這個指令將永遠不會返回。這也是使用?Finalizers 機制時會經常碰到的問題,如果你發現有一個 resource 始終無法被刪掉,那么請檢查一下你是否種上了某個不會被刪掉的?Finalizer

    2.1.6 調試與發布

    2.1.5.1 調試

    在我們開始運行&調試我們編寫的控制器之前,我們最好為我們的 DemoMicroService CRD 設置一個縮寫名稱,這是為了我們后面的操作不用輸入"demomicroservice"這么長的名稱。

    如上圖在 _api/v1/demomicroservice_types.go_?文件里加上一行注釋,我們將"demomicroservice"的縮寫設置成"dms"。
    順帶一提,kubebuilder 中,所有帶"+kubebuilder"的注釋都是有用的,不要輕易刪掉,他們是對該 kubebuilder 工程的一些配置。
    改完之后再執行"make manifests"指令,會發現 kubebuiler 生成的 CRD 文件被修改,添加了縮寫的配置:

    再設置縮寫之后,我們執行以下命令,將 CRD 安裝到你開發機器當前連接的 Kubernetes 集群:

    make install

    之后就能看到你的集群里被安裝了自己定義的 CRD:

    現在我們可以啟動我們的控制器程序了,由于筆者使用的是 GoLand IDE,所以我直接點擊啟動了 main.go 里的 main 函數:

    讀者可根據自己的開發工具,選擇使用自己的啟動方式,總之就是運行該 Go 程序即可。也可以直接使用下面的命令啟動:

    make run

    在本地啟動控制器之后,你的開發機器就是該 DemoMicroService 的控制器了,你連接的 Kubernetes 集群會將有關 DemoMicroService resource 的變更發送到你的開發機器上執行,所以打斷點也會斷住。
    接下來我們驗收一下我們之前寫的代碼是否正常工作,執行命令:

    kubectl apply -f ./config/samples/devops_v1_demomicroservice.yaml

    我們會看到 Kubernetes 集群中出現了該 resource:

    以上的"dms"即我們剛才設置的"demomicroservice"的縮寫。
    以及伴隨著 DemoMicroService 創建的 Deployment 及其創建的 Pod:

    當我們執行刪除 dms 的命令時,這些 deployment 和 pod 也會被刪掉。

    2.1.5.1 發布

    使用如下命令將控制器發布到你開發機器當前連接的 Kubernetes 集群:

    make deploy

    之后 kubebuilder 會在集群中創建一個專門用于放該控制器的 namespace,在我們這個例子里,該 namespace 為 example-system,之后可以通過如下命令看到自己的控制器已經被發布到你當前連接的集群:

    kubectl get po -n example-system

    如果該 pod 發布失敗,那么多半是國內連接不上 gcr.io 的鏡像倉庫導致的,在工程內搜索"gcr.io"的鏡像倉庫,將其替換成你方便訪問的鏡像倉庫即可。

    2.2 總結

    我們在本節大致了解了 kubebuilder 腳手架的使用方法,和 kubebuilder 程序的開發方法。并實踐了一個實現了微服務的部署和下線的控制器。
    或許讀者會問,為什么不直接創建一個 Deployment,而要用這么麻煩的方式來實現。那是因為自定義的 CRD 能更好的抽象開發者的業務場景,比如在我們的這個例子中,我們的微服務只關心鏡像地址,其他的 Deployment 屬性全部可以默認,那么我們的 DemoMicroService 看上去就比 Deployment 清爽很多。
    除此之外,這樣做還有如下優點:

  • Kubernetes 的 resource 保證了執行結果的一致性:Kubernetes 對于執行 resource 天然的符合冪等性,并且其內提供的 resourceVersion 機制也解決了并發執行時帶來的結果不一致的問題,這些問題如果開發者自己去解,往往會費時費力,而且吃力不討好。
  • kubebuilder 的開發模型幫助開發者節省了大量工作量,這些工作量包括:監聽 resource 變化,出錯 resource 的重試,以及必要的 YAML configurations 生成。

  • 這里值得一提的是 kubebuilder 的重試機制,如果你自定的 resource 執行失敗,那么 kubebuilder 會幫助你重試直到該 resource 被成功執行,這省去了你自己實現重試邏輯的工作量。
  • 調用鏈路安全可控,通過將你的業務邏輯沉淀成 CRD 和控制器,可以完全享受 Kubernetes 的 rbac 權限管控系統,能更安全,方便和精細的管控你開發的控制器接口。
  • 由于我們的示例過于簡單,所以這些優勢聽起來可能比較蒼白,在下一節我們到更復雜的運維場景里之后,我們能對以上描述的這些優勢有更深的體會。

    3. 運維系統實現

    如果你只對 operator pattern 及其實踐感興趣,并不關心運維系統如何實現,那么可以不讀本節。
    下圖展示了在我們開發的微服務平臺中,微服務運維控制器所做的事情,讀者可以看到實現的這樣一個 operator 在整個微服務平臺中的位置:

    上圖中的 dmsp 表示我們的微服務平臺,而 dmsp-ops-operator 即該運維系統的控制器。可以看到因為 dmsp-ops-operator 的存在,用戶操作管控臺要下達的指令就很簡單,實際的運維操作都由 dmsp-ops-operator 執行即可。并且?dmsp-ops-operator 也作為 Kubernetes 集群里的能力沉淀到了技術棧的最下層,與上層的業務邏輯完全清晰的分離了開來。

    3.1 微服務的完整抽象

    在第2節,我們實現了一個 demo 微服務,事實上那個 demo 微服務只關心鏡像地址,這明顯是不夠的,所以我們實現了 MicroServiceDeploy CRD 及其控制器,能抽象和實現更多的運維功能,一個 MicroServiceDeploy CR 看起來如下所示:

    apiVersion: custom.ops/v1 kind: MicroServiceDeploy metadata:name: ms-sample-v1s0 spec:msName: "ms-sample" # 微服務名稱fullName: "ms-sample-v1s0" # 微服務實例名稱version: "1.0" # 微服務實例版本path: "v1" # 微服務實例的大版本,該字符串將出現在微服務實例的域名中image: "just a image url" # 微服務實例的鏡像地址replicas: 3 # 微服務實例的 replica 數量autoscaling: true # 該微服務是否開啟自動擴縮容功能needAuth: true # 訪問該微服務實例時,是否需要租戶 base 認證config: "password=88888888" # 該微服務實例的運行時配置項creationTimestamp: "1535546718115" # 該微服務實例的創建時間戳resourceRequirements: # 該微服務實例要求的機器資源limits: # 該微服務實例會使用到的最大資源配置cpu: "2"memory: 4Girequests: # 該微服務實例至少要用到的資源配置cpu: "2"memory: 4Giidle: false # 是否進入空載狀態

    而以上一個 resource 實際上創建了很多其他的 Kubernetes resource,這些 Kubernetes resource 才真正構成了該微服務實際的能力。創建這些 Kubernetes resource 的方式基本上就是第2節講解的方式。
    下面我將分開介紹這些 Kubernetes resource,并分別說明這些 Kubernetes resource 的意義和作用。

    3.2 Service&ServiceAccount&Deployment

    首先是對于一個微服務而言必備的 Service, ServiceAccount 和 Deployment。這三種 resource 大家應該已經很熟悉了,這里就不過多說明,直接貼出由 MicroServiceDeploy 控制器創建出的 YAML 配置。

    3.2.1 Service&ServiceAccount

    apiVersion: v1 kind: Service metadata:labels:app: ms-samplemy-domain-ops-controller-make: "true"name: ms-sample spec:ports:- name: httpport: 9898protocol: TCPtargetPort: 9898selector:app: ms-sample status:loadBalancer: {} --- apiVersion: v1 kind: ServiceAccount metadata:labels:my-domain-ops-controller-make: "true"name: ms-sample

    上面的?my-domain-ops-controller-make 是自定義控制器自己打上的 label,用于區分該 resource 是我們的自定義控制器創建的。

    3.2.2 Deployment

    apiVersion: apps/v1 kind: Deployment metadata:annotations:app.ops.my.domain/last-rollout-at: "1234"labels:app: ms-samplemy-domain-ops-controller-make: "true"name: ms-sample-v1s0 spec:replicas: 1selector:matchLabels:app: ms-sampletype: RollingUpdatetemplate:metadata:annotations:app.ops.my.domain/create-at: "1234"prometheus.io/scrape: "true"labels:app: ms-samplespec:containers:- env:- name: POD_NAMEvalueFrom:fieldRef:apiVersion: v1fieldPath: metadata.name- name: SERVICE_NAMEvalueFrom:fieldRef:apiVersion: v1fieldPath: metadata.labels['app']image: "just a image url"imagePullPolicy: Alwaysname: ms-sampleports:- containerPort: 9898protocol: TCPresources:limits:cpu: 100mmemory: 400Mirequests:cpu: 100mmemory: 400MivolumeMounts:- mountPath: /home/admin/logsname: log- mountPath: /home/admin/confname: configinitContainers:- command:- sh- -c- chmod -R 777 /home/admin/logs || exit 0image: busyboximagePullPolicy: Alwaysname: log-volume-mount-hackvolumeMounts:- mountPath: /home/admin/logsname: logvolumes:- hostPath:path: /data0/logs/ms-sample/1.0type: DirectoryOrCreatename: log- configMap:defaultMode: 420name: ms-sample-v1s0name: config

    上面的 Deployment 有三點需要說明:

  • Pod 里 app.ops.my.domain/create-at 的?annotation 是控制器給 Pod 打上的注釋,用于強制讓該 Deployment 下的 Pods重啟,這樣即使 Deployment apply 時沒有其他變化,這些 Pods 也會被重啟,這在需要 Pods 被強制重啟時很有用。
  • 上面 name 為 log 的 volume 表示日志的 volume 掛載,代表容器內的 /home/admin/logs 目錄里收集的日志會同步到宿主機的?/data0/logs/ms-sample/1.0 目錄下。要讓這個機制成立,需要你容器里的服務確保將日志打到 /home/admin/logs 目錄下。
  • 上面 name 為 config 的 volume 掛載,表示將 name 為 ms-sample-v1s0 的 ConfigMap 配置掛載到容器里的 /home/admin/conf 目錄下,這樣你在你的容器里通過讀取 /home/admin/conf 目錄下的配置文件,就能在容器中讀取到運行時配置。詳情將在 3.3 節里說明。
  • 3.3 代表運行時配置項的ConfigMap

    在 3.2.2 節提到的 ms-sample-v1s0 ConfigMap 如下所示:

    apiVersion: v1 data:ms.properties: name=foo kind: ConfigMap metadata:labels:app: ms-samplemy-domain-ops-controller-make: "true"name: ms-sample-v1s0

    上述 ConfigMap 當 mount 到容器內的 /home/admin/conf 下時,就會在 /home/admin/conf 下創建一個 ms.properties 文件,該文件的內容就是"name=foo"。此時容器內部便可以通過讀取該文件來獲取運行時配置。而且該配置是動態實時更新的,即 ConfigMap 變化了,容器里的文件內容也會變化,這樣就可以做到即使容器不重啟,最新的配置也會生效。

    3.3 資源管理

    在 Kubernetes 中,資源這一名詞一般指代系統默認的機器資源,即:cpu 與 memory。
    這里的資源管理是指對微服務部署的 namespace 進行資源總量的管控,以及對每個微服務部署的容器做資源限制。用于實現這一目的的 YAML 為:

    apiVersion: v1 kind: ResourceQuota metadata:labels:my-domain-ops-controller-make: "true"name: default-resource-quotanamespace: default spec:hard:requests.cpu: "88"limits.cpu: "352"requests.memory: 112Gilimits.memory: 448Gi --- apiVersion: v1 kind: LimitRange metadata:labels:my-domain-ops-controller-make: "true"name: default-limit-rangenamespace: default spec:limits:- default:cpu: 400mmemory: 2GidefaultRequest:cpu: 100mmemory: 500Mimax:cpu: "2"memory: 8Gitype: Container

    與其他的 Kubernetes resource 不同的是,上面兩個 resource 并不是在部署?MicroServiceDeploy CR 時創建的,而是在控制器部署時創建的,作為針對集群內某一 namespace 的配置。所以你需要在控制器的 init 方法中去 create 上面兩個 resource。
    上面的?ResourceQuota 限制了 default namespace 所能占用的資源額度總量。
    而?LimitRange 限制了 default namespace 中所有沒有限制資源量的容器所能占用的資源額度。之所以要為每個讓容器有缺省的資源額度,原因在于 Kubernetes 會對根據資源配置的情況對 Pod 做分級:如果一個 Pod 沒有被配置資源量,則該 Pod 重要性最低;其次是分配了資源配置,但是 limits != requests 的 Pod;最后是分配了資源配置,而且 limits == requests 的 Pod,其重要性最高。Kubernetes 會在資源總量不足時,將重要性更低的 Pod 釋放掉,用于調度更重要的 Pod。
    以上描述的三個等級分別稱作:BestEffort(優先級最低),Burstable,Guaranteed(優先級最高)。該等級稱作 QoS(Quality of Service) 等級。你可以在 Kubernetes Pod resource 的 status.qosClass 字段里查看該 Pod 的 QoS 等級。

    3.4 HPA自動擴縮容

    用于實現自動擴縮容的 HPA(HorizontalPodAutoscaler) resource 也是在部署?MicroServiceDeploy CR 時創建的,他針對的是這個 MicroServiceDeploy CR 代表的微服務:

    apiVersion: autoscaling/v1 kind: HorizontalPodAutoscaler metadata:labels:app: ms-samplemy-domain-ops-controller-make: "true"name: ms-sample-v1s0 spec:maxReplicas: 10minReplicas: 1scaleTargetRef:apiVersion: apps/v1kind: Deploymentname: ms-sample-v1s0targetCPUUtilizationPercentage: 81

    上面的 HPA resource 表示將根據 cpu 使用率來對 ms-sample-v1s0 Deployment 下的 Pod 進行擴縮容,并且 Pod 數的區間在:[1, 10]。
    關于自動擴縮容可以講的比較多,我會單寫一篇文章詳細的來說明這一塊內容。

    4. 參考資料

    《Kubernetes in Action》——電子工業出版社
    Kubernetes 對 Operator 的官方解釋:https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
    kubebuilder 使用手冊:https://book.kubebuilder.io/quick-start.html
    本文所有示例代碼開源于:https://github.com/l4wei/kubebuilder-example

    數加平臺&DataWorks團隊2020屆校招實習生報名已經開始!
    加入我們將有機會參與大數據,人工智能,算法,云計算,深度學習,機器學習,WebIDE 的產品化以相關的國內領先的創新工程, 將在專業師兄指導下快速成長。
    簡歷精準投遞,快速進入面試通道;
    請將簡歷發送至:?dataplus-develop-recruit@list.alibaba-inc.com
    郵件標題:“【實習報名】姓名-學校-專業-職位-期望base地(北京或杭州)”
    有其他疑問歡迎郵件咨詢。

    作者信息:
    吳謀,花名四唯,阿里云智能-計算平臺事業部技術專家,負責數加平臺 &DataWorks 的微服務生態建設,目前主要關注 Kubernetes、微服務等相關技術方向。

    原文鏈接
    本文為云棲社區原創內容,未經允許不得轉載。

    總結

    以上是生活随笔為你收集整理的Kubernetes operator 模式开发实践的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。