antony@notes:~/misc$ cat "驗證-kubelet-serializeImagePulls-行為差異.md"
驗證 kubelet `serializeImagePulls` 行為差異
驗證 kubelet serializeImagePulls 行為差異
透過實際部署多個 Pod,觀察 kubelet 在
serializeImagePulls=true與false兩種設定下的 image 拉取行為差異。
目錄
- 背景知識
- 前置準備
- 第一階段:測試
serializeImagePulls=false(平行拉取) - 第二階段:測試
serializeImagePulls=true(序列拉取) - 第三階段:對照分析
- 還原步驟
- 注意事項
- 參考資料
背景知識
兩個層級的序列化
Kubernetes 的 image pull 序列化行為其實有兩個層級:
- 跨 Pod 層級:由
serializeImagePulls控制,決定不同 Pod 之間的 image 拉取是序列還是平行 - Pod 內部層級:kubelet 永遠會逐一處理同一個 Pod 內的 container,這個行為不受
serializeImagePulls影響
⚠️ 重要:測試時若用「單一 Pod 多 container」,無論
serializeImagePulls設成什麼,都會看到序列行為。必須用「多 Pod 同時部署」才能觀察到差異。
參數版本支援
- Kubernetes 1.27+:
maxParallelImagePulls為 Alpha - Kubernetes 1.28+:升為 Beta
- Kubernetes 1.32+:加入 e2e 測試
前置準備
1. 選定測試用的 worker node
kubectl get nodes本次測試使用節點:tk8s-worker2
2. 備份 kubelet 設定檔
SSH 到目標 worker node:
sudo cp /var/lib/kubelet/config.yaml /var/lib/kubelet/config.yaml.bak3. 準備測試用 YAML
建立檔案 multi-pod-test.yaml:
apiVersion: v1
kind: Pod
metadata:
name: pull-test-1
spec:
nodeName: tk8s-worker2
containers:
- name: c
image: nginx:1.25
imagePullPolicy: Always
command: ["sleep", "3600"]
---
apiVersion: v1
kind: Pod
metadata:
name: pull-test-2
spec:
nodeName: tk8s-worker2
containers:
- name: c
image: redis:7.2
imagePullPolicy: Always
command: ["sleep", "3600"]
---
apiVersion: v1
kind: Pod
metadata:
name: pull-test-3
spec:
nodeName: tk8s-worker2
containers:
- name: c
image: mysql:8.0
imagePullPolicy: Always
command: ["sleep", "3600"]
---
apiVersion: v1
kind: Pod
metadata:
name: pull-test-4
spec:
nodeName: tk8s-worker2
containers:
- name: c
image: postgres:16
imagePullPolicy: Always
command: ["sleep", "3600"]第一階段:測試 serializeImagePulls=false(平行拉取)
步驟 1:修改 kubelet 設定為 false
在 worker node 上編輯設定檔:
sudo vi /var/lib/kubelet/config.yaml加入或修改以下欄位:
serializeImagePulls: false
maxParallelImagePulls: 5步驟 2:重啟 kubelet
sudo systemctl restart kubelet
sudo systemctl status kubelet確認狀態為 active (running)。
步驟 3:驗證設定生效
kubectl get --raw "/api/v1/nodes/tk8s-worker2/proxy/configz" \
| jq '.kubeletconfig | {serializeImagePulls, maxParallelImagePulls}'預期輸出:
{
"serializeImagePulls": false,
"maxParallelImagePulls": 5
}步驟 4:清除節點上的舊 image
在 worker node 上執行:
sudo crictl rmi nginx:1.25 redis:7.2 mysql:8.0 postgres:16步驟 5:開啟 events 監看視窗
另開一個 terminal:
kubectl get events --watch步驟 6:部署 Pod 進行測試
kubectl apply -f multi-pod-test.yaml步驟 7:實際執行結果
0s Normal Pulling pod/pull-test-1 Pulling image "nginx:1.25"
0s Normal Pulling pod/pull-test-2 Pulling image "redis:7.2"
0s Normal Pulling pod/pull-test-3 Pulling image "mysql:8.0"
0s Normal Pulling pod/pull-test-4 Pulling image "postgres:16"
0s Normal Pulled pod/pull-test-2 Successfully pulled image "redis:7.2" in 3.559s (3.559s including waiting). Image size: 44645040 bytes.
0s Normal Created pod/pull-test-2 Container created
0s Normal Started pod/pull-test-2 Container started
0s Normal Pulled pod/pull-test-3 Successfully pulled image "mysql:8.0" in 3.792s (3.792s including waiting). Image size: 233592821 bytes.
0s Normal Created pod/pull-test-3 Container created
0s Normal Started pod/pull-test-3 Container started
0s Normal Pulled pod/pull-test-4 Successfully pulled image "postgres:16" in 4.077s (4.077s including waiting). Image size: 160157788 bytes.
0s Normal Created pod/pull-test-4 Container created
0s Normal Started pod/pull-test-4 Container started
0s Normal Pulled pod/pull-test-1 Successfully pulled image "nginx:1.25" in 5.568s (5.568s including waiting). Image size: 71005258 bytes.
0s Normal Created pod/pull-test-1 Container created
0s Normal Started pod/pull-test-1 Container started步驟 8:結果判讀(平行拉取的證據)
| Image | pull time | including waiting | 等待時間 |
|---|---|---|---|
| redis:7.2 | 3.559s | 3.559s | 0s ✅ |
| mysql:8.0 | 3.792s | 3.792s | 0s ✅ |
| postgres:16 | 4.077s | 4.077s | 0s ✅ |
| nginx:1.25 | 5.568s | 5.568s | 0s ✅ |
判讀重點:
- ✅ 四個
Pulling事件同時發生(時間戳同為0s) - ✅ 每個 image 的
pull time=including waiting,代表沒有任何排隊等待 - ✅ 完成順序與啟動順序不同(redis 第一個拉完,但 nginx 第一個啟動)
- ✅ 總時間 ≈ 最慢 image 的時間(~5.5 秒)
步驟 9:清理測試環境
kubectl delete -f multi-pod-test.yaml第二階段:測試 serializeImagePulls=true(序列拉取)
步驟 10:修改 kubelet 設定為 true
SSH 到 worker node,編輯設定檔:
sudo vi /var/lib/kubelet/config.yaml修改為:
serializeImagePulls: true⚠️ 注意:設為
true時必須移除或註解maxParallelImagePulls那一行,否則 kubelet 會啟動失敗。
步驟 11:重啟 kubelet
sudo systemctl restart kubelet
sudo systemctl status kubelet步驟 12:驗證設定生效
kubectl get --raw "/api/v1/nodes/tk8s-worker2/proxy/configz" \
| jq '.kubeletconfig.serializeImagePulls'預期輸出:
true步驟 13:清除節點上的 image
在 worker node 上執行:
sudo crictl rmi nginx:1.25 redis:7.2 mysql:8.0 postgres:16步驟 14:重新部署 Pod 並監看 events
kubectl get events --watch另開 terminal:
kubectl apply -f multi-pod-test.yaml步驟 15:實際執行結果
0s Normal Pulling pod/pull-test-3 Pulling image "mysql:8.0"
0s Normal Pulling pod/pull-test-1 Pulling image "nginx:1.25"
0s Normal Pulling pod/pull-test-2 Pulling image "redis:7.2"
0s Normal Pulling pod/pull-test-4 Pulling image "postgres:16"
0s Normal Pulled pod/pull-test-3 Successfully pulled image "mysql:8.0" in 3.858s (3.858s including waiting). Image size: 233592821 bytes.
0s Normal Created pod/pull-test-3 Container created
0s Normal Started pod/pull-test-3 Container started
0s Normal Pulled pod/pull-test-1 Successfully pulled image "nginx:1.25" in 4.783s (8.629s including waiting). Image size: 71005258 bytes.
0s Normal Created pod/pull-test-1 Container created
0s Normal Started pod/pull-test-1 Container started
0s Normal Pulled pod/pull-test-2 Successfully pulled image "redis:7.2" in 3.565s (12.188s including waiting). Image size: 44645040 bytes.
0s Normal Created pod/pull-test-2 Container created
0s Normal Started pod/pull-test-2 Container started
0s Normal Pulled pod/pull-test-4 Successfully pulled image "postgres:16" in 3.585s (15.751s including waiting). Image size: 160157788 bytes.
0s Normal Created pod/pull-test-4 Container created
0s Normal Started pod/pull-test-4 Container started步驟 16:結果判讀(序列拉取的證據)
| 順序 | Image | pull time | including waiting | 等待時間 |
|---|---|---|---|---|
| 1 | mysql:8.0 | 3.858s | 3.858s | 0s |
| 2 | nginx:1.25 | 4.783s | 8.629s | 3.846s ⏳ |
| 3 | redis:7.2 | 3.565s | 12.188s | 8.623s ⏳ |
| 4 | postgres:16 | 3.585s | 15.751s | 12.166s ⏳ |
判讀重點:
- ⚠️ 雖然四個
Pulling事件看起來也是同一秒(這是事件精度只到秒的觀察陷阱),但底層只有一個真的在下載 - ✅
including waiting時間遞增(0s → 3.8s → 8.6s → 12.2s),代表後面的 image 都在排隊 - ✅ 等待時間 ≈ 前面所有 image 的 pull time 累加
- ✅ 總時間 ≈ 所有 image 拉取時間總和(~15.7 秒)
步驟 17:清理測試環境
kubectl delete -f multi-pod-test.yaml第三階段:對照分析
兩種模式並列比較
| 觀察項目 | serializeImagePulls=false | serializeImagePulls=true |
|---|---|---|
| 各 image 等待時間 | 0s, 0s, 0s, 0s | 0s, 3.8s, 8.6s, 12.2s |
pull time vs including waiting | 兩者相同 | including waiting 大於 pull time |
| 完成順序 | 取決於 image 大小與網速 | 接近啟動順序 |
| 最後一個完成時間 | ~5.5 秒 | ~15.7 秒 |
| 總耗時特徵 | ≈ 最慢 image 時間 | ≈ 所有 image 時間總和 |
| 效能差異 | 基準 | 慢約 2.85 倍 |
視覺化對照
serializeImagePulls=false(平行):
時間軸: 0s ─────────── 5.5s
mysql [████████] ← 同時下載
nginx [██████████]
redis [████████]
postgres [██████████]serializeImagePulls=true(序列):
時間軸: 0s ─────────────────────── 15.7s
mysql [████] ← 先下載
nginx [█████] ← 等 mysql 完
redis [████] ← 再等
postgres [████] ← 最後🎯 最關鍵的單一判斷依據
看 Pulled 事件訊息中的 including waiting 數字:
in X (X including waiting)→ 平行拉取(沒排隊)in X (Y including waiting)且Y > X→ 序列拉取(有排隊)
這個欄位就是 Kubernetes 為了幫使用者判斷序列化行為而特別設計的觀察點。
還原步驟
步驟 18:還原 kubelet 設定
sudo cp /var/lib/kubelet/config.yaml.bak /var/lib/kubelet/config.yaml
sudo systemctl restart kubelet
sudo systemctl status kubelet步驟 19:確認還原成功
kubectl get --raw "/api/v1/nodes/tk8s-worker2/proxy/configz" \
| jq '.kubeletconfig.serializeImagePulls'注意事項
1. 測試節點選擇
選一台非生產關鍵的 worker node。重啟 kubelet 期間該節點上的 Pod 會短暫斷線。
2. kubeadm 叢集警告
本機修改的 /var/lib/kubelet/config.yaml 在執行 kubeadm upgrade 時可能會被覆蓋。正式長期套用建議改 kube-system 命名空間下的 kubelet-config ConfigMap(但會影響全部節點)。
3. Image 必須不存在於節點
每次測試前都要 crictl rmi,否則 kubelet 會跳過 pull 階段直接顯示 Container image already present,觀察不到差異。
4. maxParallelImagePulls 限制規則
- 需要 Kubernetes 1.27+ 才支援
- 設為
≥ 2時,必須搭配serializeImagePulls: false - 若
serializeImagePulls: true,則maxParallelImagePulls必須設為1或不設定 - 違反規則 kubelet 會啟動失敗
5. Pod 內 container 不受影響
kubelet 永遠逐一處理同一個 Pod 內的多個 container 的 image 拉取,這是固定設計,與 serializeImagePulls 設定無關。所以驗證時務必使用「多 Pod 同時部署」的方式。