冷啟動之後,DSC 說 KServe 沒就緒:

kserve-controller-manager    0/1  CrashLoopBackOff  10 (5m ago)   4d4h
llmisvc-controller-manager   0/1  CrashLoopBackOff   5 (2m ago)   4d4h

DSC: Kserve False (DeploymentNotReady)
ISvc llm-scratch READY=False      ← 但舊 pod 還在跑,推論打得通

(順帶一提最後那行:舊的 predictor pod 還活著,服務照樣有回應。 所以「服務能打」不代表「平台是健康的」——這件事我另外寫過。)


log 停在這一行

兩個 controller 的 log 都停在同一個地方:

failed to wait for tls-profile-watcher caches to sync kind source:
*v1.APIServer: timed out waiting for cache to be synced for Kind *v1.APIServer

我在網路上搜不到這一行。 KServe 的 GitHub issue 裡有 controller CrashLoop 的案例, 但都是別的原因(inferencegraph 的 cache sync)。

第一眼看起來像什麼?像網路問題、像 API server 慢、像叢集還沒起完。 所以我一開始的反應是「再等等」,然後等了五分鐘它又 crash 一次。


根因:ServiceAccount 沒有那個權限

controller 啟動時要 watch apiservers.config.openshift.io—— 它要讀叢集的 TLS profile 設定。

先確認那個資源存在:

oc get apiserver
# NAME      AGE
# cluster   30d

存在。那就是權限

oc auth can-i watch apiservers.config.openshift.io \
   --as=system:serviceaccount:opendatahub:kserve-controller-manager
# no

no


為什麼它不回 Forbidden

這是這篇的重點,也是它難查的原因。

controller-runtime 建立 informer 去 watch 一個資源時,如果 SA 沒權限:

  1. watch 請求被 API server 拒絕
  2. informer 不會把這個錯誤往上拋——它會重試,那是它的設計(網路抖動時要能自己恢復)
  3. 於是 cache 永遠 sync 不到
  4. manager 等到逾時,整個 process 退出
  5. Kubernetes 看到容器結束,重啟它,回到第 1 步 → CrashLoopBackOff

你看到的是「逾時」,而逾時是「權限被拒絕」被重試機制吞掉之後的殘影。

一般寫程式時,權限不足會拿到 403,訊息很明確。 但在 controller 這個架構裡,權限錯誤被包在一層「會自動重試的抽象」底下, 而那層抽象只會告訴你「我等不到」。


修法

補一個 ClusterRole 給那兩個 SA:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: kserve-apiserver-config-reader
rules:
- apiGroups: ["config.openshift.io"]
  resources: ["apiservers"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: kserve-apiserver-config-reader
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: kserve-apiserver-config-reader
subjects:
- kind: ServiceAccount
  name: kserve-controller-manager
  namespace: opendatahub
- kind: ServiceAccount
  name: llmisvc-controller-manager
  namespace: opendatahub

oc apply 之後約 40 秒兩個 pod 自動恢復——CrashLoop 的退避重啟會自己再試一次, 不用手動刪 pod。DSC Kserve: TrueISvc READY=True

這個修法撐得過冷啟動。 隔天重開機再查,兩個 controller 都正常, 沒有復發——它不是一次性的 workaround。


可以帶走的除錯順序

CrashLoopBackOff 而 log 是「等不到 / 逾時 / timed out」時,先懷疑權限

# 1. log 停在哪一個資源?(上面那行是 *v1.APIServer)
oc logs <pod> -n <ns> --previous --tail=20

# 2. 那個資源存在嗎?
oc get <resource>

# 3. 這個 SA 有權限嗎? ← 關鍵這步
oc get pod <pod> -o jsonpath='{.spec.serviceAccountName}'
oc auth can-i <verb> <resource> --as=system:serviceaccount:<ns>:<sa>

第 3 步的 oc auth can-i --as= 是最被低估的 k8s 除錯指令。 它讓你不用改任何東西就能問「如果我是它,我做得到嗎」。


這對驗收的意義

這類坑是 operator 的 CSV 權限清單漏了東西——不是你的設定錯。 在 ODH 3.5 上我遇到三個同類的。

所以驗收清單裡值得加一條:

冷啟動之後(不是安裝完當下),逐一確認所有 controller 的狀態。 安裝當下很多權限問題不會浮現,因為那時候該建的都建好了; 它們會在下一次重啟才爆出來。

而正式環境的「下一次重啟」,通常是某個你不希望它出事的時間點。


你有沒有遇過「錯誤訊息完全指向錯的方向」的 k8s 問題?最後是怎麼找到根因的?

🧪 這篇的實驗環境與 lab 檔案(最後更新 2026-08-30)

叢集

  • CRC 2.63.0 · OpenShift 4.22.7 · Kubernetes v1.35.6
  • 單節點:13 vCPU / 40 GiB RAM / 120 GB disk
  • 宿主:Framework Laptop 16(Ryzen AI 7 350 · 8C/16T · 64 GB RAM · 1 TB NVMe · RTX 5070 顯卡模組)
  • ⚠️ 叢集內看不到 GPU(CRC 是 VM,RTX 5070 未 passthrough)

Operator

  • opendatahub-operator.v3.5.0 ← 即 RHOAI 3.x 的上游開源版
  • cert-manager-operator.v1.20.0(3.x 的必要相依;2.x 不需要)
  • openshift-pipelines-operator-rh.v1.23.2openshift-gitops-operator.v1.21.3

DataScienceCluster 開啟的元件

  • kserveaipipelinesdashboardworkbenchesmodelregistrykueue(Unmanaged)
  • 其餘(raytrustyaifeastaigateway…)為 Removed

叢集外的依賴(跑在宿主的 podman 上,crc start 不會帶起來)

  • Harbor v2.15.2(私有 registry)· MinIO(S3)

模型端

  • Python 3.12.13 · PyTorch 2.11.0+cu128 · FastAPI + uvicorn · prometheus-client

lab 檔案

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