iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
AI Engineering

OpenShift AI 簡易入門30天系列 第 19

Day 19:下線與回滾——把東西拿掉這件事

  • 分享至 

  • xImage
  •  

這是什麼、解決什麼問題

模型會換,而且比程式碼換得頻繁。

如果換一個權重檔要走完整套「重 build image → 推 registry → 掃描 → 部署」,
兩週換一次還好,每週換兩次就受不了。

Day 10 講過的 STORAGE_URI 就是為了這個:權重不進 image,換模型只是換 S3 上的檔案。


什麼時候你會用到

  • 模型定期重訓(多數 ML 專案都會)
  • 要 A/B 比較兩個版本
  • 上線後發現不對要回滾

四種換法

換法 停機 能回滾 有核准紀錄 適合
A 改 STORAGE_URI 短暫 改回去 lab、內部服務
B 建新 ISvc + 切 Route 切回去 需要零停機時
C canary 流量分配 調比例 要漸進驗證
D 改 git,讓 Argo 部署 依底下用 A 或 B revert commit 正式環境

⚠️ 注意最後一欄。 A、B、C 都是某個人在終端機打指令——
做得到,但沒有任何東西記得是誰、什麼時候、根據什麼做的。
正式環境要的是 D,而 D 底下跑的還是 A 或 B。


換法 A:改 STORAGE_URI

最直接的:

oc patch isvc my-model --type=merge \
  -p '{"spec":{"predictor":{"model":{"storageUri":"s3://models/my-model/v3/"}}}}'

KServe 會滾動更新 pod,新 pod 的 storage-initializer 抓新版。

⚠️ 只改 S3 上的檔案、不改 URI 是不會生效的——
storage-initializer 只在 pod 啟動時跑一次,要刪 pod 才會重來。

而「同一個路徑換內容」這種做法要盡量避免:它讓 Day 16 講的血緣鏈直接斷掉,
而且你會有一個顯示正常、內容早就換過的服務每一版一個路徑。


換法 B:新開一個,切 Route

# 舊的留著,新開一個
metadata:
  name: my-model-v3
spec:
  predictor:
    model:
      storageUri: s3://models/my-model/v3/

新的起來、驗過之後才切:

oc patch route my-model --type=merge \
  -p '{"spec":{"to":{"name":"my-model-v3-predictor"}}}'

好處是回滾只要把 Route 切回去,舊的一直都在;代價是同時跑兩份,資源要兩倍。


換法 C:canary

KServe 支援按比例分流:

spec:
  predictor:
    model:
      storageUri: s3://models/my-model/v3/
  canaryTrafficPercent: 10

10% 的請求打到新版。

⚠️ 兩個要先想清楚的:10% 的流量要累積多久才有結論(統計問題,低頻服務可能要好幾天);
以及你要用什麼指標判斷新版比較好——延遲和錯誤率看得到,
「預測比較準」要等真實標籤回來
,那可能是幾個月後。

canary 對前者有效,對後者無效。 別把它當成模型品質的驗證機制。


換法 D:改 git,讓 Argo 去部署

這是正式環境的做法,前三種是它底下的實作。

git 上只改兩行——storageUri 指到新版路徑、model/sha256 換成新的 digest——
開 PR、有人 review、merge,Argo 同步。

換到的東西是紀錄:誰核准=PR 的 approver、回滾=git revert
上線前檢查=PR 上的 CI。

⚠️ 界線:Argo 保證「叢集上的宣告 == git 上的宣告」,
但權重在 S3 不在 git——有人換掉 S3 上的檔案,Argo 全綠而服務已經變了。
下面那些檢查照樣要做。 完整寫法在 Day 25。


步驟:⭐ 換完之後怎麼確認真的換了

這是本篇最重要的一節。 我為了寫它真的換了一次,
第一次就失敗了——而且四個檢查點全是綠的。

我實際做的事

lab 上有兩個模型:線上的 v1(32 MB)和 gate 放行過但沒人上線的候選(5.5 MB)。
我把候選複製到一個有版本的路徑 s3://models/llm-v2/,然後改 STORAGE_URI

storage-initializer 下載成功——1.43 秒,兩個檔案。

Found S3 object: llm-v2/ckpt.pt (5784727 bytes)
Found S3 object: llm-v2/tokenizer.json (37097 bytes)
Successfully copied s3://models/llm-v2/ to /mnt/models
Model downloaded in 1.4338307090074522 seconds.

然後服務起不來。

File "/app/serve/app.py", line 95, in _load_model
    drift=DriftMonitor.from_artifacts(ART),
FileNotFoundError: [Errno 2] No such file or directory: '/mnt/models/clean_corpus.txt'

新版本的路徑少了一個檔案——而且不是模型檔,是漂移監控用的基準語料。
我複製權重的時候只想到「模型」,沒想到「這個服務啟動需要什麼」。

教訓比我原本寫的那條更嚴格:不是「每一版一個路徑」,
每一版的路徑要裝得下那一版服務啟動所需要的全部東西

⭐ 而失敗當下,這四個訊號全是綠的

你會看的 顯示 真相
oc get isvc READY True 舊 pod 還在服務
ISvc 的 conditions Ready / PredictorReady / IngressReady 全 True 同上
打 API 正常回應 回的是舊版
oc get deploy READY 1 / UPDATED 1 / AVAIL 1 / DESIRED 1 這幾欄各自數的不是同一個 pod

最後一列最陰險:UPDATED 1 數的是正在 CrashLoop 的新 pod,
READY 1AVAIL 1 數的是還在服務的舊 pod。每一欄都沒說謊,合起來卻是假的。

只有兩個訊號抓得到:

# ① 有兩個 pod,而且一個沒 ready
oc get pods -l serving.kserve.io/inferenceservice=<name>   -o custom-columns='NAME:.metadata.name,READY:.status.containerStatuses[*].ready,RESTARTS:.status.containerStatuses[0].restartCount'
# llm-scratch-predictor-6786d546b4-5tfgh   false,true   6      ← 新的,重啟 6 次
# llm-scratch-predictor-868c589678-6k9cm   true,true    1      ← 舊的,還在服務

# ② 服務回報的 digest 沒有變

補完之後:真正的數字

補上缺的檔案、刪掉 pod 讓它重抓:

動作 實測
換版 rollout(5.5 MB 權重) 13 秒
回滾 rollout(32 MB 權重) 14 秒
storage-initializer 下載 5.8 MB 1.43 秒
這期間的服務中斷 0(見下)

⭐ 不要刪 pod——我第一次做錯了

我第一次補完檔案之後,是用 oc delete pod -l <selector> 讓它重抓的。
那一條指令把新舊兩個 pod 都刪掉了,服務真的斷了。

正確做法是什麼都不刪,只 patch。 因為 KServe 產的 Deployment 本來就有滾動策略:

oc get deploy <name>-predictor -o jsonpath='{.spec.strategy}'
# {"rollingUpdate":{"maxSurge":"25%","maxUnavailable":"25%"},"type":"RollingUpdate"}

replicas: 1 時 k8s 這樣換算:
maxSurge 25% → 進位成 1maxUnavailable 25% → 捨去成 0

也就是說:新 pod 必須先 Ready,舊 pod 才會被收掉。零停機是預設行為。

我重做了一次來驗證——patch 之後什麼都不碰,同時開一個探針每 0.3 秒打一次 /health
連續 150 秒,中間完成一次換版加一次回滾:

RESULT ok=465 fail=0

465 次請求,0 次失敗。

所以真正該注意的是:

情況 做法
一般換版 只 patch,什麼都不刪——滾動更新會處理
新 pod CrashLoop、修好了要它重試 只刪那個新的 podoc delete pod <新pod名>),不要用 label selector
只改了 S3 上的內容、URI 沒變 這時才需要刪 pod——而且要一個一個刪

⚠️ oc delete pod -l <selector> 在單副本服務上就是一次停機。
這個指令常被當成「讓它重讀設定」的萬用招,但它不區分新舊,
會把正在服務的那個一起帶走。
(副本數拉到 2 以上就連刪錯都還有另一個頂著。)

⭐ 換完之後,服務自己說它沒登記

換到 v2 之後打服務的 /model

{"serving_digest":"sha256:bb614beb…", "in_registry":false,
 "status":"UNREGISTERED", "metrics":null}

digest 換對了,但這個模型不在台帳上——因為我是手動 promote 的,跳過了註冊。
如果這是正式環境,現在沒有人答得出「線上這版的評估指標是多少」。
回滾之後才變回 "status":"production" 加上完整 metrics。

手動換版最貴的代價不是停機,是斷掉證據鏈。


四個檢查

前三個上面的實測已經示範過了,這裡只列指令:

# ① 只剩一個 pod,而且是新的
oc get pods -l serving.kserve.io/inferenceservice=my-model \
  -o custom-columns='NAME:.metadata.name,READY:.status.containerStatuses[*].ready,AGE:.metadata.creationTimestamp'

# ② 權重檔對(時間戳與大小)
oc exec <pod> -c kserve-container -- ls -la /mnt/models

# ③ digest 對
oc exec <pod> -c kserve-container -- sha256sum /mnt/models/ckpt.pt

⚠️ 第 ② 項要看的是整個目錄,不是只看模型檔——
我今天就是因為少複製了一個非模型的檔案,服務起不來。

第 ④ 個是新的:

檢查四:⭐ 讓服務自己說它在跑哪一版

在你的服務上開一個回報身分的端點,像上面那個 /model
一次回答三題——跑哪一版、在不在台帳上、那一版的指標是多少。

⚠️ 這是你要自己寫的,平台不會給你。
但它是我今天唯一一眼就看出「換成功了但沒登記」的訊號。
如果只能加一個東西到模型服務裡,我會加這個。


怎麼確認做對了

檢查 通過條件
1 只剩一個 pod,而且是新的 ⚠️ 兩個 pod 就是沒換成功,不管 ISvc 說什麼
2 ISvc Ready 這條沒有用——換版失敗時它也是 True
3 權重檔對 ls -la /mnt/models 時間戳與大小
4 digest 對 sha256sum 等於預期
5 服務自報的身分對 /model 之類的端點(檢查四)
6 在台帳上 ⚠️ 換對了不代表登記了
7 回滾演練過 ⭐ 見下

第 7 項:在你需要回滾之前,先回滾一次。

換版當下就是最好的演練時機——新版剛上、舊版還在、你人還在。
沒演練過的回滾等於沒有回滾。

我今天做完換版就立刻回滾了一次,15 秒,digest 與台帳狀態都回到原樣。
知道這個數字,跟猜「應該很快吧」是兩件事。


常見問題

Q:換版要不要重跑資安掃描?
A:image 沒變就不用重掃 image。但模型檔本身該不該掃是另一個問題——
pickle 格式是可執行內容,資安審會問。

Q:能不能自動換?資料更新就自動重訓上線?
A:技術上可以。但「自動上線」要先有自動的品質閘門
否則你只是把出錯的速度加快了。Day 28 專講這個。

Q:舊版本要留多久?
A:至少留到你確定不用回滾為止。S3 上的舊權重很便宜,留著。

Q:怎麼確認換版真的沒有斷線?
A:自己開探針量,不要相信「應該不會斷」。 我用的方法:

# 在叢集內起一個 pod,每 0.3 秒打一次 /health,數成功與失敗
oc run probe --image=curlimages/curl --restart=Never -- sh -c '
  ok=0; fail=0; end=$(( $(date +%s) + 150 ))
  while [ $(date +%s) -lt $end ]; do
    curl -fsS -m 2 http://<svc>/health >/dev/null 2>&1 && ok=$((ok+1)) || { fail=$((fail+1)); echo FAIL; }
    sleep 0.3
  done
  echo "RESULT ok=$ok fail=$fail"'

這也是驗收清單上該有的一條——「換版不停機」如果沒量過,它就只是個說法。

Q:換版之後監控的基準要跟著換嗎?
A:要,而且很容易忘。用舊版基準看新版會噴一堆假警報
然後大家就開始忽略告警。Day 29 會講。


  • 叢集:CRC 2.63.0 · OpenShift 4.22.7 · Kubernetes v1.35.6
    單節點 13 vCPU / 40 GiB / 120 GB,叢集內沒有 GPU
  • Operatoropendatahub-operator.v3.5.0(即 RHOAI 3.x 的上游開源版)、
    cert-manager-operator.v1.20.0(3.x 的必要相依,2.x 不需要)
  • 開啟的元件kserveaipipelinesdashboardworkbenchesmodelregistry
  • 叢集外:Harbor v2.15.2(私有 registry)、MinIO(S3),跑在宿主的 podman 上
  • 宿主:Framework Laptop 16(Ryzen AI 7 350 · 8C/16T · 64 GB · 1 TB NVMe)

⚠️ ODH ≠ RHOAI:元件同源,但 namespace 與部分名稱不同
(這裡是 opendatahub,商用版是 redhat-ods-*)。指令邏輯可照用,字串要自己對一次。


補充資料

=

  • 🧪 這篇用到的 YAML/腳本:https://github.com/ryanGTR/openshift-ai-30days
  • 🤖 lab 服務的那個模型(從零手刻的小 GPT):https://github.com/ryanGTR/llm-from-scratch

你們換模型版本要走什麼流程?跟換程式一樣嗎? 留言或到原文留言都可以,我會回。


上一篇
Day 18:換版——不重 build image 換一個模型
下一篇
Day 20:離線環境——image 怎麼進來、怎麼拉得到
系列文
OpenShift AI 簡易入門30天23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言