iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
AI Engineering

從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰系列 第 12

Day 12:容器與 Kubernetes — GPU 排程與那些會讓 Pod 重啟的坑

  • 分享至 

  • xImage
  •  

今天進入 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 叢集」才是要你建叢集。前者的職缺數遠多於後者——先把「會用」練到位,投報率最高。


一、現象:ML 工作負載的三個特殊性

Kubernetes 上的 ML 工作負載型態

圖 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 會看到這個數字)的結構性原因。

二、原理:GPU 在 Kubernetes 裡是「擴充資源」

很多人的第一個困惑是:為什麼 YAML 裡寫了 nvidia.com/gpu: 1,Pod 卻一直 Pending?

image

圖 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 適合開發環境提高利用率。

三、動手:一份能跑的 ML 服務 YAML

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 建一個測試叢集

# 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

Kubernetes 很強,但它的複雜度是真實成本:

情境 建議 理由
單一模型、流量穩定、無需自動擴縮 docker compose 或雲端容器服務 K8s 的維運成本高於它解決的問題
多個模型、需要自動擴縮與資源配額 Kubernetes 這正是它的主場
只有推論、想要極簡 雲端 Serverless 容器(Cloud Run 等) 免維運,但注意冷啟動與 GPU 支援限制
需要 GPU 共享與多租戶 Kubernetes + MIG/時間切片 目前沒有更好的替代
團隊沒有人熟 K8s 先別上 半夜叢集出事時,沒人會修才是真正的風險

4.1 五個最常見的 ML 服務故障與對應根因

把實務上遇到的故障整理成一張對照表。這張表建議存起來,值班時查得到:

症狀 最可能的根因 怎麼確認
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。

4.2 給主軸二讀者:你至少要會的三件事

如果你走生成式 AI 應用路線,不需要精通 K8s,但這三件事會直接影響你能不能把東西上線:

  1. 看得懂 kubectl describelogs --previous——你的 RAG 服務出事時,第一時間能自己判斷是應用問題還是叢集問題。
  2. 知道自己的服務要多少記憶體——載入嵌入模型與向量索引後的常駐記憶體,是 limits 設定的依據。答不出來,維運團隊也無從幫你。
  3. 理解探針的意義——你的服務可能要花 60 秒載入嵌入模型與索引,如果沒告訴維運團隊,就會遇到本文開頭那個無限重啟。

📌 給求職者的實話

你不需要會建叢集,但你必須會排錯。面試最常考的就是本文的排錯三連加上「Pod 一直 Pending / CrashLoopBackOff 你怎麼查」。能有條理地說出檢查順序與各自對應的根因,比背得出十個 API 物件更有說服力。


今日小結

  • ML 工作負載的三個特殊性:啟動慢(探針要調)、記憶體尖峰高(limits 要留餘裕)、GPU 不可分割(利用率天生偏低)。
  • GPU 是擴充資源,需要驅動 + device plugin 註冊後排程器才看得見;Pod 一直 Pending 多半是這一步沒做。
  • startupProbe 是最值錢的一行 YAML:讓 liveness 檢查在啟動完成前暫停,解決無限重啟的假故障。
  • 排錯三連:describe(看 Events)→ logs --previous(看重啟前的錯誤)→ exec(進去看環境)。
  • 團隊沒人熟 K8s 就先別上——半夜沒人會修才是真正的風險

限制與提醒

  • 本文尚未討論分散式與極大模型的載入瓶頸:針對 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 延遲為什麼比平均值重要

延伸閱讀



上一篇
Day 11:訓練管線與 CI/CD—品質門檻設定
下一篇
Day 13:推論服務 — 延遲 vs 吞吐的取捨曲線
系列文
從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言