優先啟動 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 不存在並提供服務時它可能會出錯。如果我使用沒有生命週期 postStart
的 livenessProbe
會怎麼樣?兩個容器將立即就緒:起初,這種行為與沒有任何額外探針的行為沒有區別,因為存活探針根本不影響就緒狀態。過了一段時間,Sidecar 將開始自行重啟,但不會影響主容器。
發現總結
我將在下表中總結啟動行為。
探針/鉤子 | Sidecar 是否在主應用前啟動? | 主應用是否等待 Sidecar 就緒? | 如果檢查未透過會怎樣? |
---|---|---|---|
readinessProbe | 是,但幾乎是並行的(實際上是否) | 否 | Sidecar 未就緒;主應用繼續執行 |
livenessProbe | 是,但幾乎是並行的(實際上是否) | 否 | Sidecar 被重啟,主應用繼續執行 |
startupProbe | 是 | 是 | 主應用未啟動 |
postStart | 是,主應用容器在 postStart 完成後啟動 | 是,但你必須為此提供自定義邏輯 | 主應用未啟動 |
總結一下:由於 Sidecar 通常是主應用的依賴項,你可能希望延遲後者的啟動,直到 Sidecar 健康為止。理想的模式是同時啟動兩個容器,並讓應用容器的邏輯在所有層面上進行延遲,但這並不總是可行。如果你需要這樣做,就必須對 Pod 定義使用正確的自定義方式。幸運的是,這很方便快捷,而且你已經有了上面的現成方案。
部署愉快!