【Kubernetes】聲明式API與Kubernetes編程范式


  什么是聲明式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對象的“增、刪、改、查”進行協作,進而完成用戶業務邏輯的編寫過程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM