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