什么是聲明式API呢?
答案是,kubectl apply命令。
舉個栗子
在本地編寫一個Deployment的YAML文件:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment spec: selector: matchLabels: app: nginx replicas: 2 template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx ports: - containerPort: 80
###然后用kubectl apply創建這個Deployment $ kubectl apply -f nginx.yaml ### 倘若修改一下nginx里定義的鏡像 ... spec: containers: - name: nginx image: nginx:1.7.9 ###可以繼續執行一條kubectl apply命令,觸發滾動更新 $ kubectl apply -f nginx.yaml
kubectl apply執行了一個對原有API對象的PATCH(補丁)操作。而kubectl replace的執行過程,是使用新的YAML文件中的API對象,替換原來的API對象。
這意味着kube-apiserver在響應命令式請求(kubectl replace)的時候,一次只能處理一個寫請求,否則會有產生沖突的可能。而對於聲明式請求(kubectl apply),一次能處理多個寫操作,並且具備Merge能力。
那聲明式API在實際使用中有何重要意義呢?舉起第二個栗子
Istio是一個基於Kubernetes項目的微服務治理框架。它最根本的組件是允許在每一個應用Pod里的Envoy容器,Envoy是一個高性能的C++網絡代理,Istio把這個代理服務以sidecar容器的方式,運行在了每一個被治理的應用Pod中,因Pod里的所有容器共享同一個Network Namespace,所以Envoy容器就能夠通過配置Pod里的iptables規則,把整個Pod的進出流量接管下來。這時候Istio的控制層里的Pilot組件就能夠通過調用每個Envoy容器的API,對這個Envoy代理進行配置,從而實現微服務治理。
更重要的是,在整個微服務治理過程中,無論是對Envoy容器的部署,還是對Envoy代理的配置,用戶和應用都是無感的。
那istio明明需要在每個Pod里安裝一個Envoy容器,又怎么能做到無感呢?
實際上,istio項目使用的是,Kubernetes中一個非常重要的功能,叫做Dynamic Admission Control。
在Kubernetes中,當一個Pod或者任何一個API對象被提交給APIServer之后,總有一些初始化性質的工作需要在被Kubernetes項目正式處理之間進行(如自動為所有Pod加上某些標簽),而這個初始化操作的實現,借助的是一個叫做Admission的功能,它其實是接祖Kubernetes項目里一組被稱為Admission Controller的代碼,可以選擇性地被編譯進APIServer中,在API對象創建之后會被立刻調用到。但這就意味着如果現在想要添加一些自己的規則到Admission Controller會比較困難,因為這要求重新編譯並重啟APIServer。顯然這個對istio影響太大。
因此Kubernetes提供了一種熱插拔的Admission機制,它就是Dynamic Admission Control,也叫做Initializer。
那加入有這么一個應用Pod:
apiVersion: v1 kind: Pod metadata: name: myapp-pod labels: app: myapp spec: containers: - name: myapp-container image: busybox command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
可以看到,這個Pod里面只有一個用戶容器:myapp-container
istio要做的就是在這個Pod YAML被提交給Kubernetes之后,在它對應的API對象里自動加上Envoy容器的配置,使對象變成如下的樣子:
apiVersion: v1 kind: Pod metadata: name: myapp-pod labels: app: myapp spec: containers: - name: myapp-container image: busybox command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600'] - name: envoy image: lyft/envoy:845747b88f102c0fd262ab234308e9e22f693a1 command: ["/usr/local/bin/envoy"] ...
可以看到,多出了一個叫envoy的容器,那istio又是如何在用戶不知情的前提下完成這個操作的呢?
istio做的就是編寫一個用來為Pod自動注入Envoy容器的Initializer。
首先Istio會將這個Envoy容器本身的定義,以ConfigMap的方式保存在Kubernetes中
apiVersion: v1 kind: ConfigMap metadata: name: envoy-initializer data: config: | containers: - name: envoy image: lyft/envoy:845747db88f102c0fd262ab234308e9e22f693a1 command: ["/usr/local/bin/envoy"] args: - "--concurrency 4" - "--config-path /etc/envoy/envoy.json" - "--mode serve" ports: - containerPort: 80 protocol: TCP resources: limits: cpu: "1000m" memory: "512Mi" requests: cpu: "100m" memory: "64Mi" volumeMounts: - name: envoy-conf mountPath: /etc/envoy volumes: - name: envoy-conf configMap: name: envoy
這個ConfigMap的data部分,正是一個Pod對象的一部分定義,其中可以看到Envoy容器對應的Container字段,以及一個用來聲明Envoy配置文件的volumes字段。Initializer要做的就是把這部分Envoy相關的字段,自動添加到用戶提交的Pod的API對象里。但是用戶提交的Pod里本來就有containers和volumes字段,所以Kubernetes在處理這樣的更新請求時,就必須使用類似於git merge這樣的操作,才能將這兩部分內容合並在一起。即Initializer更新用戶的Pod對象時,必須使用PATCH API來完成。
接下來,Istio將編寫好的Initializer作為一個Pod部署在Kubernetes中
apiVersion: v1 kind: Pod metadata: labels: app: envoy-initializer name: envoy-initializer spec: containers: - name: envoy-initializer image: envoy-initializer:0.0.1 imagePullPolicy: Always
envoy-initializer:0.0.1鏡像時一個自定義控制器(Custom Controller)。Kubernetes的控制器實際上是一個死循環:它不斷地獲取實際狀態,然后與期望狀態作對比,並以此為依據決定下一步的操作。
對Initializer控制器,不斷獲取的實際狀態,就是用戶新創建的Pod,它期望的狀態就是這個Pod里被添加了Envoy容器的定義。它的控制邏輯如下:
for { // 獲取新創建的 Pod pod := client.GetLatestPod() // Diff 一下,檢查是否已經初始化過 if !isInitialized(pod) { // 沒有?那就來初始化一下 //istio要往這個Pod里合並的字段,就是ConfigMap里data字段的值 doSomething(pod) } } func doSomething(pod) { //調用APIServer拿到ConfigMap cm := client.Get(ConfigMap, "envoy-initializer") //把ConfigMap里存在的containers和volumes字段,直接添加進一個空的Pod對象 newPod := Pod{} newPod.Spec.Containers = cm.Containers newPod.Spec.Volumes = cm.Volumes // Kubernetes的API庫,提供一個方法使我們可以直接使用新舊兩個Pod對象,生成 patch 數據 patchBytes := strategicpatch.CreateTwoWayMergePatch(pod, newPod) // 發起 PATCH 請求,修改這個 pod 對象 client.Patch(pod.Name, patchBytes) }
所以Envoy機制得以實現正是借助了Kubernetes能夠對API對象進行在線更新的能力,這也是Kubernetes聲明式API的獨特之處
-
- 聲明式:指只需要提交一個定義好的API對象來聲明期望的狀態是什么樣子的
- 聲明式API允許有多個API寫短,以PATCH的方式對API對象進行修改,無需關心本地原始YAML文件的內容
- 有了上述兩個能力,Kubernetes才可以基於對API對象的增刪改查,在完全無需外界干預的情況下,完成對實際狀態和期望狀態的調諧過程。
聲明式API才算Kubernetes項目編排能力賴以生存的核心所在
Kubernetes編程范式即:如何使用控制器模式,同Kubernetes里的API對象的“增、刪、改、查”進行協作,進而完成用戶業務邏輯的編寫過程
