從Helm到 Operator:Kubernetes應(yīng)用管理的進(jìn)化
Helm 的作用
在開始前需要先對(duì) kubernetes Operator 有個(gè)簡(jiǎn)單的認(rèn)識(shí)。
以為我們?cè)诰帉懖渴鹨恍┖?jiǎn)單 Deployment 的時(shí)候只需要自己編寫一個(gè) yaml 文件然后 kubectl apply 即可。
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: k8s-combat
name: k8s-combat
spec:
replicas: 1
selector:
matchLabels:
app: k8s-combat
template:
metadata:
labels:
app: k8s-combat
spec:
containers:
- name: k8s-combat
image: crossoverjie/k8s-combat:v1
imagePullPolicy: Always
resources:
limits:
cpu: "1"
memory: 300Mi
requests:
cpu: "0.1"
memory: 30Mi
kubectl apply -f deployment.yaml
這對(duì)于一些并不復(fù)雜的項(xiàng)目來(lái)說(shuō)完全夠用了,但組件一多就比較麻煩了。
這里以 Apache Pulsar 為例:它的核心組件有:
- Broker
- Proxy
- Zookeeper
- Bookkeeper
- Prometheus(可選)
- Grafana(可選) 等組件,每個(gè)組件的啟動(dòng)還有這依賴關(guān)系。
必須需要等 Zookeeper 和 Bookkeeper 啟動(dòng)之后才能將流量放進(jìn)來(lái)。
此時(shí)如何還繼續(xù)使用 yaml 文件一個(gè)個(gè)部署就會(huì)非常繁瑣,好在社區(qū)有提供 Helm 一鍵安裝程序,使用它我們只需要在一個(gè)同意的 yaml 里簡(jiǎn)單的配置一些組件,配置就可以由 helm 來(lái)部署整個(gè)復(fù)雜的 Pulsar 系統(tǒng)。
components:
# zookeeper
zookeeper: true
# bookkeeper
bookkeeper: true
# bookkeeper - autorecovery
autorecovery: true
# broker
broker: true
# functions
functions: false
# proxy
proxy: true
# toolset
toolset: true
# pulsar manager
pulsar_manager: false
monitoring:
# monitoring - prometheus
prometheus: true
# monitoring - grafana
grafana: true
# monitoring - node_exporter
node_exporter: true
# alerting - alert-manager
alert_manager: false
比如在 helm 的 yaml 中我們可以選擇使用哪些 components,以及是否啟用監(jiān)控組件。
最后直接使用這個(gè)文件進(jìn)行安裝:
helm install pulsar apache/pulsar \
--values charts/pulsar/values.yaml \
--set namespace=pulsar \
--set initialize=true
它就會(huì)自動(dòng)生成各個(gè)組件的 yaml 文件,然后統(tǒng)一執(zhí)行。
所以 helm 的本質(zhì)上和 kubectl apply yaml
一樣的,只是我們?cè)诙x value.yaml 時(shí)幫我們處理了許多不需要用戶低頻修改的參數(shù)。
我們可以使用 helm 將要執(zhí)行的 yaml 輸出后人工審核
helm install pulsar apache/pulsar --dry-run --debug > debug.yaml
Operator 是什么
Helm 的痛點(diǎn)
Helm 雖然可以幫我們部署或者升級(jí)一個(gè)大型應(yīng)用,但他卻沒法幫我們運(yùn)維這個(gè)應(yīng)用。
舉個(gè)例子:比如我希望當(dāng) Pulsar Broker 的流量或者內(nèi)存達(dá)到某個(gè)閾值后就指定擴(kuò)容 Broker,閑時(shí)再自動(dòng)回收。
或者某個(gè) Bookkeeper 的磁盤使用率達(dá)到閾值后可以自動(dòng)擴(kuò)容磁盤,這些僅僅使用 Helm 時(shí)都是無(wú)法實(shí)現(xiàn)的。
以上這些需求我們目前也是通過(guò)監(jiān)控系統(tǒng)發(fā)出報(bào)警,然后再由人工處理。
其中最大的痛點(diǎn)就是進(jìn)行升級(jí):
- 升級(jí)ZK
- 關(guān)閉auto recovery
- 升級(jí)Bookkeeper
- 升級(jí)Broker
- 升級(jí)Proxy
- 開啟auto recovery
因?yàn)槊看紊?jí)是有先后順序的,需要依次觀察每個(gè)組件運(yùn)行是否正常才能往后操作。
如果有 Operator 理性情況下下我們只需要更新一下鏡像版本,它就可以自動(dòng)執(zhí)行以上的所有步驟最后將集群升級(jí)完畢。
所以相對(duì)于 Helm 來(lái)說(shuō) Operator 是可以站在一個(gè)更高的視角俯視整個(gè)應(yīng)用系統(tǒng),它能發(fā)現(xiàn)系統(tǒng)哪個(gè)地方需要它從而直接修復(fù)。
CRD(Custom Resource Definitions)
而提到 Operator 那就不得不提到 CRD(Custom Resource Definitions)翻譯過(guò)來(lái)就是自定義資源。
這是 kubernetes 提供的一個(gè) API 擴(kuò)展機(jī)制,類似于內(nèi)置的 Deployment/StatefulSet/Services 資源,CRD 是一種自定義的資源。
這里以我們常用的 prometheus-operator 和 VictoriaMetrics-operator 為例:
Prometheus:
- **Prometheus**:用于定義 Prometheus 的 Deployment
- **Alertmanager**:用于定義 Alertmanager
- **ScrapeConfig**:用于定會(huì)抓取規(guī)則
apiVersion: monitoring.coreos.com/v1alpha1
kind: ScrapeConfig
metadata:
name: static-config
namespace: my-namespace
labels:
prometheus: system-monitoring-prometheus
spec:
staticConfigs:
- labels:
job: prometheus
targets:
- prometheus.demo.do.prometheus.io:9090
使用時(shí)的一個(gè)很大區(qū)別就是資源的 kind: ScrapeConfig 為自定義的類型。
VictoriaMetrics 的 CRD:
- VMPodScrape:Pod 的抓取規(guī)則
- VMCluster:配置 VM 集群
- VMAlert:配置 VM 的告警規(guī)則
- 等等
# vmcluster.yaml
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMCluster
metadata:
name: demo
spec:
retentionPeriod: "1"
replicationFactor: 2
vmstorage:
replicaCount: 2
storageDataPath: "/vm-data"
storage:
volumeClaimTemplate:
spec:
resources:
requests:
storage: "10Gi"
resources:
limits:
cpu: "1"
memory: "1Gi"
vmselect:
replicaCount: 2
cacheMountPath: "/select-cache"
storage:
volumeClaimTemplate:
spec:
resources:
requests:
storage: "1Gi"
resources:
limits:
cpu: "1"
memory: "1Gi"
requests:
cpu: "0.5"
memory: "500Mi"
vminsert:
replicaCount: 2
以上是用于創(chuàng)建一個(gè) VM 集群的 CRD 資源,應(yīng)用之后就會(huì)自動(dòng)創(chuàng)建一個(gè)集群。
Operator 原理
Operator 通常是運(yùn)行在 kubernetes API server 的 webhook 之上,簡(jiǎn)單來(lái)說(shuō)就是在一些內(nèi)置資源的關(guān)鍵節(jié)點(diǎn) API-server 會(huì)調(diào)用我們注冊(cè)的一個(gè) webhook,在這個(gè) webhook 中我們根據(jù)我們的 CRD 做一些自定義的操作。
理論上我們可以使用任何語(yǔ)言都可以寫 Operator,只需要能處理 api-server 的回調(diào)即可。
只是 Go 語(yǔ)言有很多成熟的工具,比如常用的 kubebuilder 和 operator-sdk.
他們內(nèi)置了許多命令行工具,可以幫我們節(jié)省需要工作量。
這里以 operator-sdk 為例:
$ operator-sdk create webhook --group cache --version v1alpha1 --kind Memcached --defaulting --programmatic-validation
會(huì)直接幫我們創(chuàng)建好一個(gè)標(biāo)準(zhǔn)的 operator 項(xiàng)目:
├── Dockerfile
├── Makefile
├── PROJECT
├── api
│ └── v1alpha1
│ ├── memcached_webhook.go
│ ├── webhook_suite_test.go
├── config
│ ├── certmanager
│ │ ├── certificate.yaml
│ │ ├── kustomization.yaml
│ │ └── kustomizeconfig.yaml
│ ├── default
│ │ ├── manager_webhook_patch.yaml
│ │ └── webhookcainjection_patch.yaml
│ └── webhook
│ ├── kustomization.yaml
│ ├── kustomizeconfig.yaml
│ └── service.yaml
├── go.mod
├── go.sum
└── main.go
其中 Makefile 中包含了開發(fā)過(guò)程中常用的工具鏈(包括根據(jù)聲明的結(jié)構(gòu)體自動(dòng)生成 CRD 資源、部署k8s 環(huán)境測(cè)試等等)、Dockerfile 等等。
這樣我們就只需要專注于開發(fā)業(yè)務(wù)邏輯即可。
因?yàn)槲仪岸螘r(shí)間給 https://github.com/open-telemetry/opentelemetry-operator 貢獻(xiàn)過(guò)兩個(gè) feature,所以就以這個(gè) Operator 為例:
它有一個(gè) CRD: kind: Instrumentation,在這個(gè) CRD 中可以將 OpenTelemetry 的 agent 注入到應(yīng)用中。
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
name: instrumentation-test-order
namespace: test
spec:
env:
- name: OTEL_SERVICE_NAME
value: order
selector:
matchLabels:
app: order
java:
image: autoinstrumentation-java:2.4.0-release
extensions:
- image: autoinstrumentation-java:2.4.0-release
dir: /extensions
env:
- name: OTEL_RESOURCE_ATTRIBUTES
value: service.name=order
- name: OTEL_INSTRUMENTATION_MESSAGING_EXPERIMENTAL_RECEIVE_TELEMETRY_ENABLED
value: "true"
- name: OTEL_TRACES_EXPORTER
value: otlp
- name: OTEL_METRICS_EXPORTER
value: otlp
- name: OTEL_LOGS_EXPORTER
value: none
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://open-telemetry-opentelemetry-collector.otel.svc.cluster.local:4317
- name: OTEL_EXPORTER_OTLP_COMPRESSION
value: gzip
- name: OTEL_EXPERIMENTAL_EXPORTER_OTLP_RETRY_ENABLED
value: "true"
它的運(yùn)行規(guī)則是當(dāng)我們的 Pod 在啟動(dòng)過(guò)程中會(huì)判斷 Pod 的注解中是否開啟了注入 OpenTelemetry 的配置。
如果開啟則會(huì)將我們?cè)?CRD 中自定義的鏡像里的 javaagent 復(fù)制到業(yè)務(wù)容器中,同時(shí)會(huì)將下面的那些環(huán)境變量也一起加入的業(yè)務(wù)容器中。
要達(dá)到這樣的效果就需要我們注冊(cè)一個(gè)回調(diào) endpoint。
mgr.GetWebhookServer().Register("/mutate-v1-pod", &webhook.Admission{
Handler: podmutation.NewWebhookHandler(cfg, ctrl.Log.WithName("pod-webhook"), decoder, mgr.GetClient(),
[]podmutation.PodMutator{
sidecar.NewMutator(logger, cfg, mgr.GetClient()),
instrumentation.NewMutator(logger, mgr.GetClient(), mgr.GetEventRecorderFor("opentelemetry-operator"), cfg),
}),})
當(dāng) Pod 創(chuàng)建或有新的變更請(qǐng)求時(shí)就會(huì)回調(diào)我們的接口。
func (pm *instPodMutator) Mutate(ctx context.Context, ns corev1.Namespace, pod corev1.Pod) (corev1.Pod, error) {
logger := pm.Logger.WithValues("namespace", pod.Namespace, "name", pod.Name)
}
在這個(gè)接口中我們就可以拿到 Pod 的信息,然后再獲取 CRD Instrumentation 做我們的業(yè)務(wù)邏輯。
var otelInsts v1alpha1.InstrumentationList
if err := pm.Client.List(ctx, &otelInsts, client.InNamespace(ns.Name)); err != nil {
return nil, err
}
// 從 CRD 中將數(shù)據(jù)復(fù)制到業(yè)務(wù)容器中。
pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{
Name: javaInitContainerName,
Image: javaSpec.Image,
Command: []string{"cp", "/javaagent.jar", javaInstrMountPath + "/javaagent.jar"},
Resources: javaSpec.Resources,
VolumeMounts: []corev1.VolumeMount{{
Name: javaVolumeName,
MountPath: javaInstrMountPath,
}},
})
for i, extension := range javaSpec.Extensions {
pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{
Name: initContainerName + fmt.Sprintf("-extension-%d", i),
Image: extension.Image,
Command: []string{"cp", "-r", extension.Dir + "/.", javaInstrMountPath + "/extensions"},
Resources: javaSpec.Resources,
VolumeMounts: []corev1.VolumeMount{{
Name: javaVolumeName,
MountPath: javaInstrMountPath,
}},
})
}
不過(guò)需要注意的是想要在測(cè)試環(huán)境中測(cè)試 operator 是需要安裝一個(gè) cert-manage,這樣 webhook 才能正常的回調(diào)。
要使得 CRD 生效,我們還得先將 CRD 安裝進(jìn) kubernetes 集群中,不過(guò)這些 operator-sdk 這類根據(jù)已經(jīng)考慮周到了。
我們只需要定義好 CRD 的結(jié)構(gòu)體:
然后使用 Makefile 中的工具 make bundle 就會(huì)自動(dòng)將結(jié)構(gòu)體轉(zhuǎn)換為 CRD。
參考鏈接:
- https://github.com/VictoriaMetrics/operator。
- https://github.com/prometheus-operator/prometheus-operator。