的健康檢查該怎么寫)
LLM 服務(wù)的健康檢查該怎么寫liveness 和 readiness 必須分開健康檢查大概是整個 LLM 服務(wù)里最不起眼、也最容易寫錯的一塊。大多數(shù)人會寫成查一下依賴通不通通就 200這在普通 Web 服務(wù)里問題不大放在 LLM 服務(wù)上會出事。一、混在一起寫會發(fā)生什么先看一個典型的錯誤寫法app.get(/health)defhealth():okping_llm_provider()# 探一下上游return{status:okifokelsedown}在 Kubernetes 里/health通常被配成liveness 探針。liveness 的語義是進程還活著嗎失敗就重啟容器。問題就在這上游抖了一下你的全部實例被重啟。LLM 上游比普通依賴更容易抖限流、排隊、區(qū)域故障我實測過同一批請求早上 p95 能到 22.85 秒、晚上只有 3.33 秒。一次上游抖動 → 健康檢查全失敗 → 全部實例重啟 → 重啟后冷啟動又慢 → 健康檢查繼續(xù)失敗。一次抖動被放大成一次雪崩。這不是理論是健康檢查最經(jīng)典的自傷式配置。二、正確的切分探針回答什么能不能碰網(wǎng)絡(luò)失敗后果liveness進程活著嗎絕對不能重啟容器readiness能接客嗎可以從負載均衡摘掉對應(yīng)到代碼app.get(/health,response_modelHealthOut)defhealth():K8s liveness 用的那種進程活著就 200不碰網(wǎng)絡(luò)永遠不會因外部故障而假死。returnHealthOut(statusok,uptime_sround(time.time()-START_TIME,3),checked_atdatetime.now().isoformat(timespecseconds),)app.get(/health/ready,response_modelReadyOut)defready(ping:boolQuery(False)):providersall_provider_status(settings,do_pingping).../health里一行網(wǎng)絡(luò)調(diào)用都沒有。它只回答這個進程還在不在。/health/ready才查依賴而且默認不真探活——要加?ping1才去打各家/models。原因有兩條就緒探針被調(diào)得很頻繁每次都打上游是浪費只列模型不產(chǎn)生 token 費用但依然要網(wǎng)絡(luò)往返沒必要每次都做。三、降級態(tài)也要能表達就緒探針的返回值不是只有好/壞兩態(tài)還有中間態(tài)——部分可用。configured[pforpinprovidersifp[configured]]ifnotconfigured:statusnot_readyelifping:reachable[pforpinconfiguredifp[status]ready]ifnotreachable:statusnot_readyeliflen(reachable)len(configured):statusdegraded# 降級態(tài)主用掛了備胎還行degraded很關(guān)鍵。三家 provider 里掛了一家服務(wù)其實還能用有降級鏈這時候該做的是告警而不是把它摘掉或重啟。另外我把成本閘門也接進了就緒探針costsummarize(settings)ifcost[over_limit]:statusdegradeddetailf本地成本賬本已達閘門 ¥{cost[limit_cny]}禁止繼續(xù)調(diào)用為什么成本要參與健康判斷因為對一個 LLM 服務(wù)來說錢花超了和依賴掛了是同一級別的不可用。早發(fā)現(xiàn)比事后對賬強。四、請求 ID沒有它線上只能靠猜classRequestIdMiddleware:純 ASGI 中間件不依賴 Starlette 的高層 API升級不易碎。asyncdef__call__(self,scope,receive,send):riddict(scope[headers]).get(bx-request-id)ridrid.decode()ifridelseuuid.uuid4().hex[:16]scope[request_id]ridasyncdefsend_wrapper(message):ifmessage[type]http.response.start:message.setdefault(headers,[]).append((bx-request-id,rid.encode()))awaitsend(message)awaitself.app(scope,receive,send_wrapper)兩個要點支持透傳客戶端帶了就用客戶端的這樣跨服務(wù)的鏈路能串起來。寫成純 ASGI而不是繼承BaseHTTPMiddleware后者會在每次請求額外起一個 anyio 任務(wù)組有已知的性能和流式問題。純 ASGI 就是處理三個 message 類型沒有隱藏開銷。五、成本賬本為什么用 append 而不是重寫defappend_entry(s:Settings,entry:dict)-None:entry.setdefault(ts,datetime.now().isoformat(timespecseconds))withopen(ledger_path(s),a,encodingutf-8)asf:f.write(json.dumps(entry,ensure_asciiFalse)\n)一行調(diào)用一個 JSON 對象JSONL 格式。選 append 的理由很實在進程崩了也不丟歷史。如果是攢在內(nèi)存里定期重寫崩掉的那一刻最近一批記錄全沒了——而那批往往正是出問題時最需要的。匯總時按行解析順便按 provider / model 聚合{total_cny:0.103152,calls:201,limit_cny:20.0,remaining_cny:19.896848,by_provider:{deepseek:0.103152,zhipu:0.0,dashscope:0.0},by_model:{deepseek-flash:0.103152,...}}by_model這一層不是湊數(shù)。真正排障時你要回答的是錢是哪個模型燒掉的只看總數(shù)答不了這個問題。小結(jié)要點做法liveness只回答進程存活絕不碰網(wǎng)絡(luò)readiness才查依賴默認不真探活?ping1才打中間態(tài)用degraded表達部分可用別只有好壞兩態(tài)成本閘門狀態(tài)參與健康判斷請求 ID支持透傳純 ASGI 中間件實現(xiàn)賬本追加寫 JSONL進程崩了不丟歷史下一篇模型輸出的 JSON 不可信——結(jié)構(gòu)化輸出的四層保障。