的健康檢查該怎么寫(xiě))
LLM 服務(wù)的健康檢查該怎么寫(xiě)liveness 和 readiness 必須分開(kāi)健康檢查大概是整個(gè) LLM 服務(wù)里最不起眼、也最容易寫(xiě)錯(cuò)的一塊。大多數(shù)人會(huì)寫(xiě)成查一下依賴通不通通就 200這在普通 Web 服務(wù)里問(wèn)題不大放在 LLM 服務(wù)上會(huì)出事。一、混在一起寫(xiě)會(huì)發(fā)生什么先看一個(gè)典型的錯(cuò)誤寫(xiě)法app.get(/health)defhealth():okping_llm_provider()# 探一下上游return{status:okifokelsedown}在 Kubernetes 里/health通常被配成liveness 探針。liveness 的語(yǔ)義是進(jìn)程還活著嗎失敗就重啟容器。問(wèn)題就在這上游抖了一下你的全部實(shí)例被重啟。LLM 上游比普通依賴更容易抖限流、排隊(duì)、區(qū)域故障我實(shí)測(cè)過(guò)同一批請(qǐng)求早上 p95 能到 22.85 秒、晚上只有 3.33 秒。一次上游抖動(dòng) → 健康檢查全失敗 → 全部實(shí)例重啟 → 重啟后冷啟動(dòng)又慢 → 健康檢查繼續(xù)失敗。一次抖動(dòng)被放大成一次雪崩。這不是理論是健康檢查最經(jīng)典的自傷式配置。二、正確的切分探針回答什么能不能碰網(wǎng)絡(luò)失敗后果liveness進(jìn)程活著嗎絕對(duì)不能重啟容器readiness能接客嗎可以從負(fù)載均衡摘掉對(duì)應(yīng)到代碼app.get(/health,response_modelHealthOut)defhealth():K8s liveness 用的那種進(jìn)程活著就 200不碰網(wǎng)絡(luò)永遠(yuǎn)不會(huì)因外部故障而假死。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)用都沒(méi)有。它只回答這個(gè)進(jìn)程還在不在。/health/ready才查依賴而且默認(rèn)不真探活——要加?ping1才去打各家/models。原因有兩條就緒探針被調(diào)得很頻繁每次都打上游是浪費(fèi)只列模型不產(chǎn)生 token 費(fèi)用但依然要網(wǎng)絡(luò)往返沒(méi)必要每次都做。三、降級(jí)態(tài)也要能表達(dá)就緒探針的返回值不是只有好/壞兩態(tài)還有中間態(tài)——部分可用。configured[pforpinprovidersifp[configured]]ifnotconfigured:statusnot_readyelifping:reachable[pforpinconfiguredifp[status]ready]ifnotreachable:statusnot_readyeliflen(reachable)len(configured):statusdegraded# 降級(jí)態(tài)主用掛了備胎還行degraded很關(guān)鍵。三家 provider 里掛了一家服務(wù)其實(shí)還能用有降級(jí)鏈這時(shí)候該做的是告警而不是把它摘掉或重啟。另外我把成本閘門(mén)也接進(jìn)了就緒探針costsummarize(settings)ifcost[over_limit]:statusdegradeddetailf本地成本賬本已達(dá)閘門(mén) ¥{cost[limit_cny]}禁止繼續(xù)調(diào)用為什么成本要參與健康判斷因?yàn)閷?duì)一個(gè) LLM 服務(wù)來(lái)說(shuō)錢(qián)花超了和依賴掛了是同一級(jí)別的不可用。早發(fā)現(xiàn)比事后對(duì)賬強(qiáng)。四、請(qǐng)求 ID沒(méi)有它線上只能靠猜classRequestIdMiddleware:純 ASGI 中間件不依賴 Starlette 的高層 API升級(jí)不易碎。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)兩個(gè)要點(diǎn)支持透?jìng)骺蛻舳藥Я司陀每蛻舳说倪@樣跨服務(wù)的鏈路能串起來(lái)。寫(xiě)成純 ASGI而不是繼承BaseHTTPMiddleware后者會(huì)在每次請(qǐng)求額外起一個(gè) anyio 任務(wù)組有已知的性能和流式問(wèn)題。純 ASGI 就是處理三個(gè) message 類型沒(méi)有隱藏開(kāi)銷。五、成本賬本為什么用 append 而不是重寫(xiě)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)用一個(gè) JSON 對(duì)象JSONL 格式。選 append 的理由很實(shí)在進(jìn)程崩了也不丟歷史。如果是攢在內(nèi)存里定期重寫(xiě)崩掉的那一刻最近一批記錄全沒(méi)了——而那批往往正是出問(wèn)題時(shí)最需要的。匯總時(shí)按行解析順便按 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í)你要回答的是錢(qián)是哪個(gè)模型燒掉的只看總數(shù)答不了這個(gè)問(wèn)題。小結(jié)要點(diǎn)做法liveness只回答進(jìn)程存活絕不碰網(wǎng)絡(luò)readiness才查依賴默認(rèn)不真探活?ping1才打中間態(tài)用degraded表達(dá)部分可用別只有好壞兩態(tài)成本閘門(mén)狀態(tài)參與健康判斷請(qǐng)求 ID支持透?jìng)骷?ASGI 中間件實(shí)現(xiàn)賬本追加寫(xiě) JSONL進(jìn)程崩了不丟歷史下一篇模型輸出的 JSON 不可信——結(jié)構(gòu)化輸出的四層保障。