線上超時(shí)故障復(fù)盤:一次工具調(diào)用拖垮整個(gè)系統(tǒng))
開(kāi)發(fā) Agent Platform踩了一次真實(shí)的線上超時(shí)故障下午兩點(diǎn)半手機(jī)連著震了七八次全是告警群的消息。打開(kāi)監(jiān)控面板看到可用性從 99.99% 直線跌到 90% 附近第一反應(yīng)是模型供應(yīng)商又出問(wèn)題了——畢竟 Agent 平臺(tái)對(duì)外的體驗(yàn)幾乎完全綁定在大模型接口的穩(wěn)定性上。結(jié)果這次猜錯(cuò)了問(wèn)題出在我們自己的調(diào)用鏈上準(zhǔn)確地說(shuō)是一次不太起眼的第三方工具調(diào)用把整個(gè)平臺(tái)拖進(jìn)了超時(shí)風(fēng)暴。我做的這個(gè)項(xiàng)目是一個(gè)多智能體調(diào)度平臺(tái)核心工作就是編排任務(wù)拿到用戶請(qǐng)求后做意圖解析、拆解成步驟再依次調(diào)用大模型和各類工具檢索資料、執(zhí)行代碼、拉取第三方數(shù)據(jù)最后把結(jié)果整合返回。聽(tīng)上去不復(fù)雜但真正跑到線上后各種隱藏問(wèn)題才會(huì)暴露出來(lái)。這篇就完整復(fù)盤這次線上超時(shí)故障從表象、誤判、根因到修復(fù)方案和設(shè)計(jì)教訓(xùn)都攤開(kāi)講清楚。如果你是做 Agent、做平臺(tái)、做異步任務(wù)編排的應(yīng)該都能找到有用的東西。1. 從告警到失控故障發(fā)生時(shí)的第一現(xiàn)場(chǎng)1.1 告警指標(biāo)長(zhǎng)什么樣先說(shuō)告警。當(dāng)時(shí)同時(shí)觸發(fā)了三類監(jiān)控可用性告警核心接口成功率從 99.99% 掉到 90% 左右持續(xù) 3 分鐘未恢復(fù)延遲告警P95 延遲從正常時(shí)的 1.5 秒飆升到 18 秒P99 直接超過(guò) 40 秒線程池告警核心調(diào)度線程池活躍線程數(shù)接近最大值隊(duì)列任務(wù)數(shù)快速積壓??吹竭@三類告警同時(shí)亮起第一判斷就是某個(gè)下游依賴出問(wèn)題了。因?yàn)?Agent 平臺(tái)和普通 CRUD 服務(wù)不一樣它沒(méi)有簡(jiǎn)單的緩存可以扛這層緩沖——每次請(qǐng)求背后是一長(zhǎng)串實(shí)時(shí)調(diào)用鏈任何一個(gè)環(huán)節(jié)變慢整個(gè)請(qǐng)求都跟著慢。1.2 Agent 平臺(tái)請(qǐng)求鏈路的特點(diǎn)我們的一個(gè)典型 Agent 任務(wù)會(huì)經(jīng)歷這樣的循環(huán)接收用戶請(qǐng)求做意圖識(shí)別和任務(wù)規(guī)劃根據(jù)規(guī)劃結(jié)果調(diào)用大模型生成下一步動(dòng)作如果動(dòng)作需要工具就調(diào)用對(duì)應(yīng)的工具接口檢索、計(jì)算、第三方 API拿到工具結(jié)果后再次交給大模型讓它決定下一步循環(huán)直到任務(wù)完成或者到達(dá)全局步數(shù)上限。所以一個(gè)看似簡(jiǎn)單的用戶問(wèn)題底層可能串了四五次大模型調(diào)用中間還夾著好幾次工具調(diào)用。這意味著一次用戶請(qǐng)求的成敗取決于鏈條上最慢的那個(gè)環(huán)節(jié)而每個(gè)環(huán)節(jié)的網(wǎng)絡(luò)開(kāi)銷、超時(shí)設(shè)置、重試策略都會(huì)產(chǎn)生疊加效應(yīng)。1.3 從現(xiàn)象到初步定位故障當(dāng)時(shí)我們的第一輪排查動(dòng)作是打開(kāi)業(yè)務(wù)日志看到大量TaskRejectedException說(shuō)明線程池已經(jīng)拒絕新任務(wù)打開(kāi)全鏈路追蹤系統(tǒng)發(fā)現(xiàn)大量調(diào)用鏈的耗時(shí)集中在某第三方數(shù)據(jù)服務(wù)的 HTTP 調(diào)用上查看該第三方服務(wù)的調(diào)用統(tǒng)計(jì)P99 從正常時(shí)的 800ms 暴漲到 32 秒。到這里表象已經(jīng)很清楚了下游的一個(gè)數(shù)據(jù)服務(wù)響應(yīng)變慢拖垮了上游的整個(gè)線程池。但這只是表象歸因真正的根因藏在為什么它會(huì)這么快拖垮全局以及為什么我們沒(méi)有在第一時(shí)間攔截住。2. 第一次誤判把矛頭指向了外部模型服務(wù)2.1 為什么第一反應(yīng)是模型供應(yīng)商又掛了Agent 平臺(tái)的日常運(yùn)維中大模型服務(wù)的穩(wěn)定性是我們最敏感的一根神經(jīng)。接口動(dòng)不動(dòng)就限流、超時(shí)、甚至返回異常都是家常便飯。所以故障發(fā)生后團(tuán)隊(duì)前十分鐘幾乎都撲在檢查模型服務(wù)的調(diào)用數(shù)據(jù)上。當(dāng)時(shí)檢查了幾個(gè)點(diǎn)模型服務(wù)的錯(cuò)誤率正常沒(méi)有明顯上升模型服務(wù)延遲P95 維持在 2~3 秒和平時(shí)差不多模型鑒權(quán)是否出問(wèn)題沒(méi)有異常記錄。也就是說(shuō)模型服務(wù)并不是這次的瓶頸。但我仍然提了工單去問(wèn)因?yàn)?Agent 平臺(tái)的體驗(yàn)太依賴模型了誰(shuí)也說(shuō)不準(zhǔn)是不是對(duì)方內(nèi)部有小概率故障只是我們這邊的監(jiān)控粒度看不到。2.2 錯(cuò)誤歸因帶來(lái)的時(shí)間成本大概過(guò)了十分鐘才有人喊了一句你們看全鏈路追蹤那個(gè)第三方數(shù)據(jù)服務(wù)是不是有問(wèn)題這時(shí)候我們才把視線從模型服務(wù)移開(kāi)開(kāi)始仔細(xì)看全鏈路數(shù)據(jù)。事后復(fù)盤這十分鐘其實(shí)非常寶貴。誤判方向本身不可怕可怕的是整個(gè)團(tuán)隊(duì)在錯(cuò)誤的方向上反復(fù)確認(rèn)而線上故障還在持續(xù)。這也是監(jiān)控設(shè)計(jì)的一個(gè)教訓(xùn)光有告警還不夠告警必須盡量帶上方向性的信息否則大家在慌亂中會(huì)按慣性猜測(cè)。2.3 真正有用的第一手線索后來(lái)我們是怎么快速鎖定的靠的是全鏈路追蹤里的兩個(gè)字段耗時(shí)分布故障期間新發(fā)起的請(qǐng)求中有 70% 以上的耗時(shí)集中在某個(gè) HTTP Span 上錯(cuò)誤類型該 Span 的錯(cuò)誤主要是Read timed out說(shuō)明是客戶端讀到超時(shí)不是對(duì)方返回失敗。這幾乎可以斷定對(duì)方接口本身還在響應(yīng)但響應(yīng)速度極慢導(dǎo)致我們的客戶端在等待中不斷堆積。于是真正的排查才剛開(kāi)始為什么一個(gè)下游變慢會(huì)讓整個(gè)平臺(tái)幾近癱瘓3. 真正的根因線程池耗盡與調(diào)用鏈的放大效應(yīng)3.1 排查鏈路從線程池到 JVM 到網(wǎng)絡(luò)層我們按以下順序逐一排查看線程池狀態(tài)核心調(diào)度線程池配置的核心線程數(shù)為 200最大線程數(shù)為 400隊(duì)列容量 1000。故障時(shí)活躍線程數(shù)長(zhǎng)期維持在 380 以上隊(duì)列持續(xù)打滿新任務(wù)直接被拒絕看線程堆棧jstack抓了一次線程 dump發(fā)現(xiàn) 70% 以上的工作線程阻塞在同一個(gè)第三方 HTTP 調(diào)用的 socket 讀等待中看連接池下游連接池共 50 個(gè)連接全部被占滿等待獲取連接的線程在排隊(duì)看超時(shí)配置核心調(diào)度線程池對(duì)外部工具調(diào)用使用的超時(shí)時(shí)間默認(rèn)是 60 秒當(dāng)時(shí)那個(gè)第三方服務(wù)單次響應(yīng)已經(jīng)達(dá)到 30~60 秒一個(gè)線程一個(gè)請(qǐng)求就要等滿 60 秒才能釋放。到這里根因已經(jīng)很清晰了局部下游變慢 過(guò)長(zhǎng)的超時(shí)時(shí)間 無(wú)差別的重試策略 線程池迅速耗盡。3.2 重試風(fēng)暴看起來(lái)沒(méi)多少流量實(shí)際上翻了幾倍還有一個(gè)隱蔽問(wèn)題重試。我們當(dāng)時(shí)的工具調(diào)用模塊里對(duì)部分第三方服務(wù)設(shè)置了失敗重試默認(rèn)重試 2 次。本來(lái)在正常情況下這沒(méi)什么但當(dāng)對(duì)方服務(wù)開(kāi)始變慢時(shí)重試變成了災(zāi)難第一次調(diào)用慢到 30 秒超時(shí)失敗后立刻重試又等 30 秒第二次重試再等 30 秒。一次工具調(diào)用最長(zhǎng)可能吃掉 90 秒而這段時(shí)間內(nèi)線程一直掛在這次任務(wù)上無(wú)法處理任何新請(qǐng)求。上游還在不斷發(fā)起新請(qǐng)求每個(gè)都往線程池里占一個(gè)位置然后全部卡在等待中。這就是經(jīng)典的線程池饑餓現(xiàn)象。3.3 用表格復(fù)盤參數(shù)配置的問(wèn)題我把故障前后的關(guān)鍵配置整理成了一張表方便大家對(duì)照看問(wèn)題出在哪配置項(xiàng)故障前故障中問(wèn)題分析工具調(diào)用超時(shí)60 秒全局默認(rèn)無(wú)法自動(dòng)縮短超時(shí)過(guò)長(zhǎng)線程長(zhǎng)時(shí)間占用重試次數(shù)失敗重試 2 次每次失敗都重試放大下游壓力倍增等待時(shí)間連接池大小5050全部占滿下游變慢時(shí)連接池迅速耗盡任務(wù)隊(duì)列有界隊(duì)列 1000持續(xù)打滿新任務(wù)被拒絕可用性下降線程池隔離無(wú)核心線程池共用所有任務(wù)共用單點(diǎn)變慢拖垮全局這些配置單獨(dú)看都不是致命問(wèn)題但組合在一起就成了一個(gè)放大器下游慢一點(diǎn)整個(gè)系統(tǒng)就翻車。3.4 Agent 場(chǎng)景為什么會(huì)放大這類故障普通 Web 服務(wù)遇到下游變慢最多就是請(qǐng)求變慢、用戶排隊(duì)等待。但 Agent 平臺(tái)不一樣一個(gè) Agent 任務(wù)內(nèi)部有循環(huán)——它會(huì)反復(fù)調(diào)用大模型、反復(fù)調(diào)用工具一次任務(wù)內(nèi)可能包含 5~10 次外部調(diào)用。這意味著假設(shè)一個(gè)下游工具變慢導(dǎo)致單次調(diào)用耗時(shí)從 1 秒變成 30 秒那一個(gè)原本只需要 5 次工具調(diào)用的 Agent 任務(wù)整體耗時(shí)可能從 5 秒惡化到 150 秒。而在這 150 秒內(nèi)線程池中的所有線程都在為一個(gè)任務(wù)服務(wù)。同樣的下游故障Agent 平臺(tái)的放大倍數(shù)遠(yuǎn)高于普通服務(wù)這是 Agent 類平臺(tái)在超時(shí)設(shè)計(jì)上必須特別警惕的原因。4. 修復(fù)方案超時(shí)治理、隔離艙室與服務(wù)降級(jí)4.1 給所有外部調(diào)用設(shè)置分類型超時(shí)第一件事是取消那個(gè)全局默認(rèn) 60 秒的懶人配置。我們對(duì)所有外部調(diào)用按照類型和用途重新梳理了超時(shí)時(shí)間調(diào)用類型推薦超時(shí)理由大模型推理調(diào)用30 秒模型推理本身耗時(shí)長(zhǎng)但超過(guò) 30 秒大概率是網(wǎng)絡(luò)或服務(wù)問(wèn)題實(shí)時(shí)工具調(diào)用檢索/查數(shù)5 秒工具響應(yīng)通??? 秒足夠覆蓋絕大多數(shù)情況非核心工具調(diào)用輔助信息3 秒拿不到就丟棄不影響主流程整 Agent 任務(wù)上限60~120 秒防止單個(gè)任務(wù)無(wú)限循環(huán)全局兜底這些超時(shí)值不是拍腦袋定的我們是參考了線上 P99 延遲的分布取正常情況下的 P99 加上一定余量。比如工具調(diào)用的 P99 是 800ms設(shè)置 5 秒超時(shí)就是留了約 6 倍余量既不會(huì)誤殺正常請(qǐng)求又能在故障時(shí)快速釋放線程。4.2 重試策略只看冪等且必須走退避重試必須有三個(gè)前提只對(duì)冪等操作重試讀操作、單純的檢索操作可以重試寫操作、有副作用的操作比如下單、發(fā)消息堅(jiān)決不重試限制重試次數(shù)最多 1 次超過(guò)就直接放棄重試必須帶退避抖動(dòng)第一次失敗后至少等待 500ms 再重試隨機(jī)加 0~200ms 抖動(dòng)避免同一時(shí)刻大量請(qǐng)求集中重試。這個(gè)改動(dòng)非常關(guān)鍵。故障期間最早的 2 次重試策略讓請(qǐng)求數(shù)直接翻了三倍改成最多 1 次重試后請(qǐng)求量最多只會(huì)翻一倍而且退避機(jī)制能給下游留出恢復(fù)窗口不會(huì)形成對(duì)下游的二次沖擊。4.3 線程池隔離按依賴的重要程度拆池原來(lái)的問(wèn)題是所有任務(wù)共用同一個(gè)核心線程池任何一個(gè)依賴變慢都會(huì)占滿全部線程。我們重新設(shè)計(jì)了線程池結(jié)構(gòu)核心編排線程池負(fù)責(zé) Agent 的主循環(huán)只做調(diào)度和編排不做任何網(wǎng)絡(luò) IO 等待大模型調(diào)用線程池單獨(dú)一個(gè)池專門處理模型推理調(diào)用配獨(dú)立超時(shí)和連接池工具調(diào)用線程池再單獨(dú)一個(gè)池按工具類別拆分為多個(gè)小組比如檢索類、計(jì)算類、第三方數(shù)據(jù)類。這樣做的好處是某個(gè)工具組的線程池被打滿時(shí)其他組的任務(wù)仍然可以正常運(yùn)行。用一句通俗的話說(shuō)就像一棟樓里每戶裝了獨(dú)立電表一家跳閘不至于整棟樓停電。4.4 信號(hào)量隔離與艙壁模式線程池隔離之外還有一個(gè)更輕量級(jí)的方案信號(hào)量Semaphore。它的特點(diǎn)是只控制并發(fā)數(shù)不額外占用線程資源。我們?cè)诿總€(gè)工具調(diào)用的入口加了一個(gè)并發(fā)信號(hào)量。比如某第三方數(shù)據(jù)服務(wù)最多允許 20 個(gè)并發(fā)調(diào)用超出并發(fā)上限的請(qǐng)求直接快速失敗Fail Fast而不是排隊(duì)等待。這有兩個(gè)直接效果下游變慢時(shí)最多只有 20 個(gè)線程被這個(gè)服務(wù)拖住不會(huì)繼續(xù)蔓延超出上限的請(qǐng)求秒敗讓調(diào)用方盡快走降級(jí)邏輯而不是把用戶掛在那里等 60 秒。這就是艙壁模式的核心思想把對(duì)某個(gè)依賴的訪問(wèn)限制在一個(gè)隔間里即使它炸了也只影響這一個(gè)隔間。4.5 降級(jí)策略拿不到結(jié)果也要讓 Agent 走下去第三步是降級(jí)。Agent 平臺(tái)有個(gè)天然優(yōu)勢(shì)任務(wù)流程本身就是彈性的。工具拿不到結(jié)果時(shí)可以有兩種降級(jí)方式空結(jié)果降級(jí)告訴大模型這次檢索沒(méi)拿到數(shù)據(jù)請(qǐng)基于已有知識(shí)回答讓流程繼續(xù)默認(rèn)值降級(jí)某些參數(shù)類查詢比如匯率、基準(zhǔn)值直接返回一個(gè)默認(rèn)值并標(biāo)注數(shù)據(jù)延遲。降級(jí)方案實(shí)施后即使第三方服務(wù)完全不可用用戶任務(wù)也不會(huì)卡死只是結(jié)果質(zhì)量會(huì)略降。對(duì)于絕大多數(shù)場(chǎng)景給結(jié)果但不夠好遠(yuǎn)勝于一直等然后報(bào)錯(cuò)。4.6 全局任務(wù)超時(shí)兜底最后加了一個(gè)總閘任何單個(gè) Agent 任務(wù)整體耗時(shí)超過(guò) 90 秒就強(qiáng)制終止。這個(gè)時(shí)間從任務(wù)開(kāi)始算起不管內(nèi)部循環(huán)多少次、調(diào)了多少工具到了時(shí)間就掐斷返回給用戶一個(gè)任務(wù)處理超時(shí)的明確提示后臺(tái)再慢慢補(bǔ)跑或重試。這一步是為了防止死循環(huán)——比如大模型一直規(guī)劃同一個(gè)動(dòng)作、工具一直返回異常數(shù)據(jù)導(dǎo)致 Agent 反復(fù)重試沒(méi)有全局兜底的話單個(gè)任務(wù)可能吃住一個(gè)線程幾個(gè)小時(shí)。5. 復(fù)盤清單Agent 類系統(tǒng)設(shè)計(jì)超時(shí)機(jī)制的幾條鐵律5.1 第三方服務(wù)的 SLA 絕不等于我們的超時(shí)上限這是這次故障最深刻的教訓(xùn)。我們當(dāng)時(shí)對(duì)那個(gè)第三方數(shù)據(jù)服務(wù)的判斷是SLA 穩(wěn)定、平均延遲低所以沒(méi)有專門給它設(shè)計(jì)超時(shí)策略而是套用了全局默認(rèn)值。結(jié)果它一旦抖動(dòng)我們連反應(yīng)時(shí)間都沒(méi)有。所有外部依賴哪怕是響應(yīng)速度一直很快的也必須有自己的超時(shí)配置和并發(fā)上限。穩(wěn)定性是動(dòng)態(tài)的不是靜態(tài)的。5.2 超時(shí)必須分層越往下越短一個(gè)好的超時(shí)體系應(yīng)該是金字塔結(jié)構(gòu)底層每個(gè)網(wǎng)絡(luò) IO 調(diào)用都有短超時(shí)3~10 秒中間每個(gè) Agent 步驟有中粒度超時(shí)比如 20 秒頂層整個(gè)任務(wù)有全局超時(shí)60~120 秒。每層超時(shí)要比上層短這樣才會(huì)形成快速失敗向上反饋的傳導(dǎo)機(jī)制。如果反過(guò)來(lái)——任務(wù)層 30 秒、調(diào)用層 60 秒——那任務(wù)層兜底就失效了。超時(shí)是層層預(yù)警不是最后兜底。5.3 全鏈路追蹤是排查 Agent 故障的第一生產(chǎn)力這次故障如果沒(méi)有全鏈路追蹤我們大概率還要在模型供應(yīng)商是否出問(wèn)題上浪費(fèi)更多時(shí)間。Agent 平臺(tái)的調(diào)用鏈長(zhǎng)、環(huán)節(jié)多沒(méi)有可靠的 trace 系統(tǒng)排查故障基本只能靠猜。建議至少做到每次外部調(diào)用都記錄獨(dú)立的 span包含耗時(shí)、結(jié)果、重試次數(shù)每個(gè) Agent 任務(wù)記錄完整的調(diào)用鏈上下文。這在平時(shí)可能看不出用處故障發(fā)生時(shí)就是救命稻草。5.4 演練不能只演成功路徑要演依賴故障我們之前做過(guò)很多次演練但演練的大多是服務(wù)自身故障、機(jī)器宕機(jī)、流量突發(fā)很少演練某個(gè)看似不重要的第三方服務(wù)變慢的場(chǎng)景。這次故障恰恰是這種邊緣場(chǎng)景。之后我們把混沌工程加入了常態(tài)化演練隨機(jī)選一個(gè)下游依賴人為注入 10 秒延遲觀察系統(tǒng)是否能自動(dòng)降級(jí)、快速恢復(fù)。一個(gè)不敢拔掉電源的系統(tǒng)就永遠(yuǎn)不知道自己有多脆弱。5.5 并發(fā)和排隊(duì)是兩回事別混在一起這里的經(jīng)驗(yàn)是并發(fā)控制要做到寧可拒絕不要排隊(duì)。對(duì)于 Agent 平臺(tái)這種延遲敏感的編排系統(tǒng)隊(duì)列并不能提高吞吐只會(huì)讓大量請(qǐng)求一起等待然后把遲到的錯(cuò)誤又進(jìn)一步放大。我們后來(lái)把大多數(shù)調(diào)用場(chǎng)景改成信號(hào)量 快速失敗模式寧可讓一小部分請(qǐng)求直接報(bào)錯(cuò)重試也不要讓大量請(qǐng)求排隊(duì)等死。寫在最后這次故障給我?guī)?lái)的改變故障修復(fù)后的一個(gè)月里我又回看了很多遍當(dāng)時(shí)的 trace 和線程 dump。說(shuō)實(shí)話這類問(wèn)題在 Agent 平臺(tái)里幾乎不可能完全避免——你的系統(tǒng)一定會(huì)有某個(gè)依賴在一個(gè)意想不到的時(shí)間點(diǎn)突然變慢。技術(shù)方案其實(shí)都是通用工程手段真正難的是把它們落實(shí)到每一個(gè)調(diào)用細(xì)節(jié)中。我現(xiàn)在設(shè)計(jì)任何一個(gè)小工具調(diào)用都會(huì)先問(wèn)三個(gè)問(wèn)題如果這個(gè)調(diào)用要等 30 秒系統(tǒng)會(huì)怎樣如果這個(gè)調(diào)用被重試兩次流量會(huì)翻幾倍如果這個(gè)調(diào)用完全不可用任務(wù)能不能降級(jí)那次故障之后我給自己定了一條規(guī)矩每次接入新的外部服務(wù)第一件事不是寫業(yè)務(wù)代碼而是寫超時(shí)配置、并發(fā)上限、降級(jí)策略、trace 埋點(diǎn)——代碼之后可以慢慢補(bǔ)這四樣?xùn)|西少了任何一個(gè)都別上線。