今天進入 MLOps 職缺中出現頻率最高的領域:AI/ML 平臺與基礎設施(58.5%)。
本日不包含 Kubernetes 的全部——那是一本書的量,而且新入職的你優先事項不包含建立叢集(Day 04 的止損點清單裡有說明)。今天只講跑 ML 工作負載時,與跑一般 Web 服務不一樣的那些地方。
一個新手 MLOps 工程師的第一週,通常長這樣:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
model-api-7d4b8c9f5-x2p8q 0/1 CrashLoopBackOff 7 12m
然後花一小時以上,最後發現原因是:模型載入要 40 秒,但 liveness probe 的 initialDelaySeconds 設了 10 秒。Kubernetes 以為服務死了,殺掉重啟;重啟又要 40 秒,又被殺掉——完美的無限迴圈。
這個坑只需要改一行 YAML。今天就把這類「ML 特有的坑」一次講完。
📊 職缺訊號
58.5% 的 MLOps 職缺提到 AI/ML 平臺與基礎設施,是所有任務訊號中最高的一項。但要注意 JD 的用詞差異:寫「熟悉 K8s」通常是要你會部署與排錯;寫「建置與維運 K8s 叢集」才是要你建叢集。前者的職缺數遠多於後者——先把「會用」練到位,投報率最高。

圖 12-1:Kubernetes 上的三種 ML 工作負載(示意架構)。訓練用 Job(跑完就結束)、推論用 Deployment(長期存活)、批次推論用 CronJob(定期執行),三者的資源與排程需求完全不同。
| 特殊性 | 一般 Web 服務 | ML 服務 | 造成的問題 |
|---|---|---|---|
| 啟動時間 | 1–3 秒 | 30 秒–5 分鐘(要載入模型) | 探針設定不當 → 無限重啟 |
| 記憶體用量 | 穩定、可預測 | 尖峰高(載入時)、與批次大小相關 | 設錯 limits → OOMKilled |
| 運算資源 | CPU 為主 | GPU,且不可分割共享 | 一個 Pod 佔一張卡,資源浪費 |
第三點值得展開:CPU 可以要 0.5 顆,GPU 預設不行——一個 Pod 要嘛拿到整張卡,要嘛拿不到。這是 GPU 利用率經常只有 12%(Day 15 會看到這個數字)的結構性原因。
很多人的第一個困惑是:為什麼 YAML 裡寫了 nvidia.com/gpu: 1,Pod 卻一直 Pending?

圖 12-2:GPU 成為可調度資源的前置條件(示意流程)。缺任何一步,Pod 都會卡在 Pending。
關鍵觀念:GPU 不像 CPU/記憶體是 Kubernetes 內建認識的資源,它是擴充資源(Extended Resource),必須由 device plugin 註冊之後,排程器才知道它存在。
當一張卡對一個工作負載太大時,有三種共享方式:
| 方式 | 原理 | 適合 |
|---|---|---|
| Time-slicing | 多個 Pod 輪流用同一張卡 | 開發測試、低流量推論;沒有記憶體隔離 |
| MIG(Multi-Instance GPU) | 硬體層切成多個獨立實例 | 生產環境多租戶;限特定卡種 |
| MPS | 多程序共用 CUDA context | 小模型高併發 |
⚠️ time-slicing 的陷阱
它沒有記憶體隔離——A 服務吃光顯存,B 服務就會 OOM。生產環境要隔離請用 MIG;time-slicing 適合開發環境提高利用率。
apiVersion: apps/v1
kind: Deployment
metadata:
name: model-api
spec:
replicas: 2
selector:
matchLabels: { app: model-api }
template:
metadata:
labels: { app: model-api }
spec:
containers:
- name: api
image: registry.example.com/model-api:0.3.1 # 永遠用明確版本,不用 latest
ports: [{ containerPort: 8000 }]
# ── 坑 1:資源設定 ──
resources:
requests: # 排程依據:至少要有這麼多才排得進去
memory: "4Gi"
cpu: "1"
limits: # 上限:超過記憶體會被 OOMKilled
memory: "8Gi" # 模型載入的尖峰通常是穩態的 1.5–2 倍
cpu: "2"
nvidia.com/gpu: 1 # GPU 只能寫在 limits,且必須是整數
# ── 坑 2:探針設定(本文開頭那個無限重啟的元凶)──
startupProbe: # 專門給「啟動慢」的服務用,這是關鍵
httpGet: { path: /health, port: 8000 }
failureThreshold: 30 # 最多等 30 × 10 = 300 秒
periodSeconds: 10
readinessProbe: # 沒 ready 就不會有流量進來
httpGet: { path: /health, port: 8000 }
periodSeconds: 5
livenessProbe: # 掛了才重啟;有 startupProbe 就不會誤殺
httpGet: { path: /health, port: 8000 }
periodSeconds: 20
failureThreshold: 3
# ── 坑 3:金鑰用 Secret 注入,不寫在映像裡(Day 05)──
env:
- name: MODEL_REGISTRY_URI
valueFrom:
secretKeyRef: { name: mlflow-secret, key: tracking-uri }
# ── 坑 4:優雅關機,避免處理到一半的請求被砍 ──
lifecycle:
preStop:
exec: { command: ["sleep", "10"] }
terminationGracePeriodSeconds: 60
startupProbe 是本篇最有價值的一行。 它的作用是:在服務啟動完成前,暫停 liveness 的檢查。這樣你可以給 liveness 一個緊湊的檢查週期(快速偵測真正的死亡),同時容許很長的啟動時間——兩者不再互相衝突。
# kind:用 Docker 容器當節點,筆電就能跑(無 GPU,但足以驗證 YAML 與探針)
brew install kind kubectl # 或見官方安裝說明
kind create cluster --name mlops-lab
kind load docker-image model-api:0.3.1 --name mlops-lab # 把本地映像載入叢集
kubectl apply -f deployment.yaml
kubectl get pods -w # 觀察狀態變化
# 排錯三連——順序不要變
kubectl describe pod <pod-name> # 1. 看 Events:排程失敗?OOMKilled?探針失敗?
kubectl logs <pod-name> --previous # 2. 看上一輪的日誌(重啟前的錯誤在這裡)
kubectl exec -it <pod-name> -- sh # 3. 進去看看(環境變數、檔案掛載對不對)
💡
--previous是排 CrashLoopBackOff 的關鍵Pod 一直重啟時,
kubectl logs看到的是新一輪剛啟動的日誌,通常什麼都沒有。真正的錯誤訊息在上一輪的日誌裡,必須加--previous才看得到。這是新手最常卡住的地方。
Kubernetes 很強,但它的複雜度是真實成本:
| 情境 | 建議 | 理由 |
|---|---|---|
| 單一模型、流量穩定、無需自動擴縮 | docker compose 或雲端容器服務 | K8s 的維運成本高於它解決的問題 |
| 多個模型、需要自動擴縮與資源配額 | Kubernetes | 這正是它的主場 |
| 只有推論、想要極簡 | 雲端 Serverless 容器(Cloud Run 等) | 免維運,但注意冷啟動與 GPU 支援限制 |
| 需要 GPU 共享與多租戶 | Kubernetes + MIG/時間切片 | 目前沒有更好的替代 |
| 團隊沒有人熟 K8s | 先別上 | 半夜叢集出事時,沒人會修才是真正的風險 |
把實務上遇到的故障整理成一張對照表。這張表建議存起來,值班時查得到:
| 症狀 | 最可能的根因 | 怎麼確認 |
|---|---|---|
Pod 一直 Pending |
GPU 資源不足或 device plugin 沒裝;節點選擇器條件無法滿足 | kubectl describe pod 看 Events 的排程訊息 |
CrashLoopBackOff 且日誌空白 |
探針逾時(本文開頭那個坑),或啟動時就 panic | kubectl logs --previous |
OOMKilled |
limits 記憶體不足;模型被重複載入(例如多個 worker 各載一份) | describe 看終止原因;檢查 worker 數與載入邏輯 |
| 服務啟動成功但預測全錯 | 掛載到錯的模型版本;特徵順序不一致 | 檢查 registry 別名與模型簽章(Day 10) |
| 偶發性逾時、P99 特別高 | 冷啟動(剛擴縮出新 Pod)、GPU 被同節點的其他工作搶佔 | 比對逾時時間點與 Pod 建立時間 |
第四列值得特別注意:這是唯一一個「Kubernetes 全綠、服務全綠、但結果全錯」的情況——它屬於 Day 14 的無聲壞掉,用 K8s 的工具查不出來。這也再次說明為什麼 MLOps 不等於 DevOps。
如果你走生成式 AI 應用路線,不需要精通 K8s,但這三件事會直接影響你能不能把東西上線:
kubectl describe 與 logs --previous——你的 RAG 服務出事時,第一時間能自己判斷是應用問題還是叢集問題。📌 給求職者的實話
你不需要會建叢集,但你必須會排錯。面試最常考的就是本文的排錯三連加上「Pod 一直 Pending / CrashLoopBackOff 你怎麼查」。能有條理地說出檢查順序與各自對應的根因,比背得出十個 API 物件更有說服力。
startupProbe 是最值錢的一行 YAML:讓 liveness 檢查在啟動完成前暫停,解決無限重啟的假故障。describe(看 Events)→ logs --previous(看重啟前的錯誤)→ exec(進去看環境)。本文尚未討論分散式與極大模型的載入瓶頸:針對 70B+ 等需要多卡分散式推論(vLLM / TensorRT-LLM)或跨節點通訊的場景,只靠單 Pod 單卡 limits 與常規 startupProbe 無法涵蓋權重分片、NCCL 初始化超時與 Shared Memory (/dev/shm) 不足等問題。
本文尚未討論冷啟動與自動擴縮(HPA/KEDA)的延遲代價評估:設定了長達 300 秒的 startupProbe 雖然能保證啟動成功,但在流量爆發引發自動水平擴展時,長達數分鐘的冷啟動會導致請求嚴重堆積與 P99 延遲飆高,文中未提及 Warm-pool 或預載入等解法。
MIG 的硬體限制與成本門檻未細化:本文提及 MIG 是生產隔離最佳解,但 MIG 僅支援 NVIDIA A100/H100 等高階資料中心卡,對中小企業常用的 L4、T4 或 RTX 系列不適用也請讀者留意。
服務跑起來了,明天談它跑得好不好:延遲與吞吐為什麼不可兼得、動態批次的取捨曲線長什麼樣、以及那個所有人都會問但很多人答不好的問題——P99 延遲為什麼比平均值重要。
startupProbe 那一節請完整讀