昨天我把「模型能回答」和「平台能營運」分開來看,最後留下一個很實際的問題:當使用者只說「AI 今天很慢」,我究竟要從哪裡開始找?
直覺上會先看 GPU,但當我把一次完整的請求路徑畫出來,才發現模型只是其中一段。前面還有 Web UI 和 API,旁邊有 Embedding、Vector Store 與資料庫,底下又共用 Host、Storage、Network 與 Power。其中一段卡住,使用者看到的可能都是同一句:「AI 沒有回答」。
所以今天我不先裝監控工具,也不先做效能測試。我先把依賴關係與故障域整理成一份可以檢查的清單。
我先用這條簡化路徑來思考:
使用者
│
▼
Web UI / API
├──────► PostgreSQL / 使用者與對話資料
│
├──► Embedding ─► Vector Store / 文件索引
│
├──► ASR ──────► 暫存檔與音訊處理
│
└──► LLM Serving
├──► GPU / VRAM / Driver / Runtime
└──► Model Storage / Cache
共同底層:Host、Storage、Network、Power
畫完以後,我最在意的不是「有幾個 container」,而是共用什麼。
即使 Web UI、資料庫和 LLM Serving 都已經拆成獨立 container,只要還放在同一個 Host,共用同一套 Storage 和 Network,那麼 Host 失效、Storage 無法讀寫或網路中斷時,這些服務還是會一起失去功能。
這就是我對 failure domain 的理解:
不是數有幾個服務,而是一次故障會帶走多少能力。
我接著把使用者可能描述的現象,改寫成排錯時可以決定下一步的表格:
| 使用者看到的現象 | 我會先懷疑什麼 | 我要找的證據 |
|---|---|---|
| 第一個字等很久 | Queue 堆積、模型冷啟動、GPU contention | TTFT、waiting requests、VRAM |
| 回答到一半中斷 | Serving process crash、OOM、proxy timeout | Error log、restart count、VRAM peak |
| 文件問答突然找不到資料 | Embedding 失敗、索引未更新、DB 異常 | Pipeline log、index version、DB health |
| UI 開得起來卻無法登入 | API 或資料庫異常 | HTTP status、connection pool、DB log |
| 所有功能同時變慢 | RAM pressure、Storage I/O、Network 或 Host | Host metrics、I/O、filesystem usage |
這張表對我最大的用處,是提醒自己不要一看到 AI 回答失敗,就先重啟模型。重啟有時會讓現象暫時消失,但也會把當下最有價值的證據一起清掉。
只留一張圖有個問題:當我後面多加一個 Embedding Service 或把資料庫移出主機時,很難確認哪些故障域已經改變。所以我把這張圖改寫成一份與任何實際環境無關的公開範例:
components = {
"web-api": {
"depends_on": ["database", "llm-serving"],
"failure_domains": ["shared-host", "network-edge"],
},
"database": {
"depends_on": [],
"failure_domains": ["shared-host", "system-storage"],
},
"embedding": {
"depends_on": ["vector-store"],
"failure_domains": ["shared-host", "system-storage"],
},
"vector-store": {
"depends_on": [],
"failure_domains": ["shared-host", "system-storage"],
},
"llm-serving": {
"depends_on": ["model-storage", "gpu-runtime"],
"failure_domains": ["shared-host", "network-edge"],
},
}
domains = {}
for component, config in components.items():
for domain in config["failure_domains"]:
domains.setdefault(domain, []).append(component)
for domain, affected in sorted(domains.items()):
print(f"{domain}: {len(affected)} services -> {', '.join(affected)}")
執行後會列出:
network-edge: 2 services -> web-api, llm-serving
shared-host: 5 services -> web-api, database, embedding, vector-store, llm-serving
system-storage: 3 services -> database, embedding, vector-store
這個輸出讓我很快看到一個現象:在應用架構圖上,我已經有五個不同服務;但在 failure domain 的角度,shared-host 一旦失效,五個服務還是會同時受影響。
這也說明了為什麼「拆成多個 container」不等於「消除單點故障」。Container 邊界解決部署與依賴隔離;failure domain 關心的是 Host、Storage、Network 或電力中斷時,實際會少掉哪些能力。
我後來又在清單中加了「有沒有狀態」和「怎麼恢復」,因為同樣是服務掛掉,後果不一樣:
| 元件 | 關鍵依賴 | 狀態特性 | 失效時的影響 | 預期恢復方式 |
|---|---|---|---|---|
| Web UI / API | Network、Database、Serving | 少量 session 狀態 | 使用者無法操作 | 重建服務,再檢查 session 與 DB |
| LLM Serving | GPU Runtime、Model Storage | 主要是 cache | 無法生成回答 | 重啟並重新載入模型 |
| Embedding | CPU/GPU、Model、Index Pipeline | 任務需可重入 | 新文件無法入庫 | 恢復服務後重送任務 |
| Database | Storage、Memory、Backup | 不可任意遺失 | 帳號、對話或設定不可用 | Restore 後做一致性檢查 |
| Vector Store | Storage、Corpus、Embedding Version | 可從固定 corpus 重建 | Retrieval 失效 | 使用固定版本重建 index |
| Model Storage | Storage、Registry、Network | 通常可重建 | 冷啟動失敗 | 重新取得並驗證 checksum |
這裡最容易誤判的是檔案大小。模型權重可能很大,但若能從可信來源重新取得,它的復原優先順序未必比資料庫高。資料庫體積可能比較小,裡面的狀態卻可能無法重建。
整理完依賴與故障域後,我先留下三個後續實驗都要遵守的原則。
第一,使用者現象與系統內部指標要同時保留。只看 GPU、RAM 與 I/O,不一定知道使用者受到多大影響;只看 latency,也不知道是哪一段依賴出問題。
第二,監控要跟著 dependency map 走。Web/API、Database、Embedding、Vector Store 與 LLM Serving 都要有自己的可觀測證據,否則最後還是只能用重啟來猜。
第三,恢復計畫不能只看服務是否能重啟。我還要問狀態能不能重建、會遺失多少資料,以及恢復後如何證明資料一致。
今天我沒有測任何模型速度,而是先確認一件事:應用在邏輯上拆成多個服務,不代表已經擺脫共同的故障域。
這張 dependency map 讓我從「模型有沒有活著」,改成問「使用者的請求經過哪些元件,又共用哪些故障域」。
明天我會沿著這張依賴圖,把使用者真正感受得到的服務品質寫成 SLI 與 SLO。因為知道哪裡會壞之後,下一個問題才是:
慢到什麼程度、錯多少次,我們才認為這個平台已經不可接受?
下一篇:Day 03|先定 SLI/SLO,再談監控