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-example2.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-manager
name: system
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
control-plane: controller-manager
name: controller-manager
namespace: system
spec:
replicas: 1
selector:
matchLabels:
control-plane: controller-manager
template:
metadata:
labels:
control-plane: controller-manager
spec:
containers:
- args:
- --enable-leader-election
command:
- /manager
image: controller:latest
name: manager
resources:
limits:
cpu: 100m
memory: 30Mi
requests:
cpu: 100m
memory: 20Mi
terminationGracePeriodSeconds: 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.4
creationTimestamp: null
name: demomicroservices.devops.my.domain
spec:
group: devops.my.domain
names:
kind: DemoMicroService
listKind: DemoMicroServiceList
plural: demomicroservices
singular: demomicroservice
scope: Namespaced
validation:
openAPIV3Schema:
description: DemoMicroService is the Schema for the demomicroservices API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: DemoMicroServiceSpec defines the desired state of DemoMicroService
properties:
foo:
description: Foo is an example field of DemoMicroService. Edit DemoMicroService_types.go
to remove/update
type: string
type: object
status:
description: DemoMicroServiceStatus defines the observed state of DemoMicroService
type: object
type: object
version: v1
versions:
- name: v1
served: true
storage: 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 here
return 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 string
Name 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: 4Gi
requests: # 該微服務執行個體至少要用到的資源配置
cpu: "2"
memory: 4Gi
idle: 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-sample
my-domain-ops-controller-make: "true"
name: ms-sample
spec:
ports:
- name: http
port: 9898
protocol: TCP
targetPort: 9898
selector:
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-sample
my-domain-ops-controller-make: "true"
name: ms-sample-v1s0
spec:
replicas: 1
selector:
matchLabels:
app: ms-sample
type: RollingUpdate
template:
metadata:
annotations:
app.ops.my.domain/create-at: "1234"
prometheus.io/scrape: "true"
labels:
app: ms-sample
spec:
containers:
- env:
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: SERVICE_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.labels['app']
image: "just a image url"
imagePullPolicy: Always
name: ms-sample
ports:
- containerPort: 9898
protocol: TCP
resources:
limits:
cpu: 100m
memory: 400Mi
requests:
cpu: 100m
memory: 400Mi
volumeMounts:
- mountPath: /home/admin/logs
name: log
- mountPath: /home/admin/conf
name: config
initContainers:
- command:
- sh
- -c
- chmod -R 777 /home/admin/logs || exit 0
image: busybox
imagePullPolicy: Always
name: log-volume-mount-hack
volumeMounts:
- mountPath: /home/admin/logs
name: log
volumes:
- hostPath:
path: /data0/logs/ms-sample/1.0
type: DirectoryOrCreate
name: log
- configMap:
defaultMode: 420
name: ms-sample-v1s0
name: 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-sample
my-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-quota
namespace: default
spec:
hard:
requests.cpu: "88"
limits.cpu: "352"
requests.memory: 112Gi
limits.memory: 448Gi
---
apiVersion: v1
kind: LimitRange
metadata:
labels:
my-domain-ops-controller-make: "true"
name: default-limit-range
namespace: default
spec:
limits:
- default:
cpu: 400m
memory: 2Gi
defaultRequest:
cpu: 100m
memory: 500Mi
max:
cpu: "2"
memory: 8Gi
type: 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-sample
my-domain-ops-controller-make: "true"
name: ms-sample-v1s0
spec:
maxReplicas: 10
minReplicas: 1
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: ms-sample-v1s0
targetCPUUtilizationPercentage: 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本文所有示例代碼開源于:
數加平台&DataWorks團隊2020屆校招實習生報名已經開始!
加入我們将有機會參與大資料,人工智能,算法,雲計算,深度學習,機器學習,WebIDE 的産品化以相關的國内領先的創新工程, 将在專業師兄指導下快速成長。
履歷精準投遞,快速進入面試通道;
請将履歷發送至:
[email protected]郵件标題:“【實習報名】姓名-學校-專業-職位-期望base地(北京或杭州)”
有其他疑問歡迎郵件咨詢。
作者資訊:
吳謀,花名四唯,阿裡雲智能-計算平台事業部技術專家,負責數加平台 &DataWorks 的微服務生态建設,目前主要關注 Kubernetes、微服務等相關技術方向。