優先啟動 Sidecar:如何避免障礙

透過《Kubernetes 多容器 Pod:概述》這篇博文,你已經瞭解了它們的工作內容、主要架構模式以及它們在 Kubernetes 中的實現方式。本文將重點介紹如何確保你的 Sidecar 容器在主應用之前啟動。這比你想象的要複雜!

溫和回顧

我想提醒讀者,Kubernetes 的 v1.29.0 版本增加了對Sidecar 容器的原生支援,現在可以在 .spec.initContainers 欄位中定義,但需要設定 restartPolicy: Always。你可以在以下 Pod 清單片段示例中看到這一點。

initContainers:
  - name: logshipper
    image: alpine:latest
    restartPolicy: Always # this is what makes it a sidecar container
    command: ['sh', '-c', 'tail -F /opt/logs.txt']
    volumeMounts:
    - name: data
        mountPath: /opt

與使用多個 .spec.containers 的傳統多容器 Pod 相比,使用 .spec.initContainers 塊定義 Sidecar 有什麼特殊之處呢?嗯,所有 .spec.initContainers 總是在主應用之前啟動。如果你定義了 Kubernetes 原生 Sidecar,它們會在主應用之後終止。此外,與作業(Jobs)一起使用時,Sidecar 容器應該仍然存活,甚至可能在所屬的 Job 完成後重啟;Kubernetes 原生 Sidecar 容器不會阻塞 Pod 的完成。

要了解更多資訊,你還可以閱讀官方的Pod Sidecar 容器教程

問題所在

現在你知道了,使用這種原生方法定義 Sidecar 將始終使其在主應用之前啟動。從 kubelet 原始碼中可以看出,這通常意味著幾乎是並行啟動的,而這並非工程師總是希望實現的效果。我真正感興趣的是,我是否可以延遲主應用的啟動,直到 Sidecar 不僅已經啟動,而且完全執行並準備好提供服務。這可能有點棘手,因為 Sidecar 的問題在於沒有明顯的成功訊號,這與 Init 容器相反——Init 容器被設計為只執行指定的時間段。對於 Init 容器,退出狀態 0 明確表示“我成功了”。而對於 Sidecar,有很多時間點可以說“某個東西正在執行”。只有一個容器就緒後再啟動下一個容器是平滑部署策略的一部分,確保啟動期間的正確順序和穩定性。實際上,我也希望 Sidecar 容器也能這樣工作,以覆蓋主應用依賴於 Sidecar 的場景。例如,如果 Sidecar 無法為請求提供服務(例如,使用 DataDog 進行日誌記錄),應用可能會出錯。當然,可以更改應用程式碼(這實際上是“最佳實踐”解決方案),但有時他們無法這樣做——而本文正專注於這種用例。

我將解釋一些你可能嘗試的方法,並向你展示哪些方法會真正有效。

就緒探針(Readiness Probe)

為了檢查 Kubernetes 原生 Sidecar 是否會延遲主應用的啟動直到 Sidecar 就緒,讓我們模擬一個簡短的調查。首先,我將透過實現一個永遠不會成功的就緒探針來模擬一個永遠不會就緒的 Sidecar 容器。提醒一下,就緒探針會檢查容器是否準備好開始接受流量,從而判斷 Pod 是否可以作為服務的後端。

(與標準的 Init 容器不同,Sidecar 容器可以有探針,以便 kubelet 可以監督 Sidecar 並在出現問題時進行干預。例如,如果 Sidecar 容器的健康檢查失敗,則重新啟動它。)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    app: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: alpine:latest
          command: ["sh", "-c", "sleep 3600"]
      initContainers:
        - name: nginx
          image: nginx:latest
          restartPolicy: Always
          ports:
            - containerPort: 80
              protocol: TCP
          readinessProbe:
            exec:
              command:
              - /bin/sh
              - -c
              - exit 1 # this command always fails, keeping the container "Not Ready"
            periodSeconds: 5
      volumes:
        - name: data
          emptyDir: {}

結果是

controlplane $ kubectl get pods -w
NAME                    READY   STATUS    RESTARTS   AGE
myapp-db5474f45-htgw5   1/2     Running   0          9m28s

controlplane $ kubectl describe pod myapp-db5474f45-htgw5 
Name:             myapp-db5474f45-htgw5
Namespace:        default
(...)
Events:
  Type     Reason     Age               From               Message
  ----     ------     ----              ----               -------
  Normal   Scheduled  17s               default-scheduler  Successfully assigned default/myapp-db5474f45-htgw5 to node01
  Normal   Pulling    16s               kubelet            Pulling image "nginx:latest"
  Normal   Pulled     16s               kubelet            Successfully pulled image "nginx:latest" in 163ms (163ms including waiting). Image size: 72080558 bytes.
  Normal   Created    16s               kubelet            Created container nginx
  Normal   Started    16s               kubelet            Started container nginx
  Normal   Pulling    15s               kubelet            Pulling image "alpine:latest"
  Normal   Pulled     15s               kubelet            Successfully pulled image "alpine:latest" in 159ms (160ms including waiting). Image size: 3652536 bytes.
  Normal   Created    15s               kubelet            Created container myapp
  Normal   Started    15s               kubelet            Started container myapp
  Warning  Unhealthy  1s (x6 over 15s)  kubelet            Readiness probe failed:

從這些日誌中可以明顯看出,只有一個容器就緒——而且我知道它不可能是 Sidecar,因為我將其定義為永遠不會就緒(你也可以在 kubectl get pod -o json 中檢查容器狀態)。我還看到我的應用(myapp)在 Sidecar 就緒之前已經啟動了。這不是我想要實現的結果;在這種情況下,主應用容器對其 Sidecar 有硬性依賴。

也許用啟動探針(Startup Probe)?

為了確保 Sidecar 在主應用容器啟動前就緒,我可以定義一個 startupProbe。它將延遲主容器的啟動,直到命令成功執行(返回 0 退出狀態)。如果你想知道我為什麼將其新增到我的 initContainer 中,讓我們分析一下如果我將其新增到 myapp 容器中會發生什麼。我無法保證探針會在主應用程式碼之前執行——而主應用程式碼在 Sidecar 未啟動並執行時可能會出錯。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    app: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: alpine:latest
          command: ["sh", "-c", "sleep 3600"]
      initContainers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80
              protocol: TCP
          restartPolicy: Always
          startupProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 5
            periodSeconds: 30
            failureThreshold: 10
            timeoutSeconds: 20
      volumes:
        - name: data
          emptyDir: {}

這導致 2/2 的容器處於就緒和執行狀態,從事件中可以推斷出主應用僅在 nginx 已經啟動後才啟動。但為了確認它是否等待 Sidecar 就緒,讓我們將 startupProbe 更改為 exec 型別的命令。

startupProbe:
  exec:
    command:
    - /bin/sh
    - -c
    - sleep 15

並執行 kubectl get pods -w 來即時觀察兩個容器的就緒狀態是否僅在 15 秒延遲後才改變。同樣,事件證實主應用在 Sidecar 之後啟動。這意味著使用帶有正確 startupProbe.httpGet 請求的 startupProbe 有助於延遲主應用的啟動,直到 Sidecar 就緒。這並非最優,但它有效。

那麼 postStart 生命週期鉤子呢?

有趣的是:使用 postStart 生命週期鉤子塊也能完成任務,但我必須編寫自己的迷你 shell 指令碼,效率更低。

initContainers:
  - name: nginx
    image: nginx:latest
    restartPolicy: Always
    ports:
      - containerPort: 80
        protocol: TCP
    lifecycle:
      postStart:
        exec:
          command:
          - /bin/sh
          - -c
          - |
            echo "Waiting for readiness at https://:80"
            until curl -sf https://:80; do
              echo "Still waiting for https://:80..."
              sleep 5
            done
            echo "Service is ready at https://:80"            

存活探針(Liveness Probe)

一個有趣的練習是檢查 Sidecar 容器在使用存活探針時的行為。存活探針的行為和配置與就緒探針類似——唯一的區別是它不影響容器的就緒狀態,而是在探針失敗時重新啟動容器。

livenessProbe:
  exec:
    command:
    - /bin/sh
    - -c
    - exit 1 # this command always fails, keeping the container "Not Ready"
  periodSeconds: 5

在添加了與之前就緒探針配置相同的存活探針後,透過 kubectl describe pod 檢查 Pod 的事件,可以看到 Sidecar 的重啟次數大於 0。然而,主應用既沒有被重啟,也沒有受到任何影響,儘管我知道(在我們假想的最壞情況下)當 Sidecar 不存在並提供服務時它可能會出錯。如果我使用沒有生命週期 postStartlivenessProbe 會怎麼樣?兩個容器將立即就緒:起初,這種行為與沒有任何額外探針的行為沒有區別,因為存活探針根本不影響就緒狀態。過了一段時間,Sidecar 將開始自行重啟,但不會影響主容器。

發現總結

我將在下表中總結啟動行為。

探針/鉤子Sidecar 是否在主應用前啟動?主應用是否等待 Sidecar 就緒?如果檢查未透過會怎樣?
readinessProbe,但幾乎是並行的(實際上是Sidecar 未就緒;主應用繼續執行
livenessProbe是,但幾乎是並行的(實際上是Sidecar 被重啟,主應用繼續執行
startupProbe主應用未啟動
postStart,主應用容器在 postStart 完成後啟動,但你必須為此提供自定義邏輯主應用未啟動

總結一下:由於 Sidecar 通常是主應用的依賴項,你可能希望延遲後者的啟動,直到 Sidecar 健康為止。理想的模式是同時啟動兩個容器,並讓應用容器的邏輯在所有層面上進行延遲,但這並不總是可行。如果你需要這樣做,就必須對 Pod 定義使用正確的自定義方式。幸運的是,這很方便快捷,而且你已經有了上面的現成方案。

部署愉快!