題幹與適用場景
一個結帳 API 會同步呼叫第三方稅費服務。正常時下游 p50 為 80 毫秒、p99 為 180 毫秒,但會偶爾出現連線失敗、慢回應、HTTP 429、503,以及「請求可能已到達服務端、回應卻遺失」的情況。結帳 API 的端到端目標為 800 毫秒;服務呼叫鏈可能有五層,多個 SDK 也可能已內建重試。
請設計呼叫策略:如何劃分截止時間與單次逾時,哪些失敗可以重試,如何設定最大嘗試次數、指數退避與抖動,斷路器如何從關閉進入開啟再恢復,以及如何用隔離、降級、指標與故障注入證明方案不會把局部故障放大成級聯故障。
題目中的延遲與層數都是面試假設。它考察的是同步服務間呼叫的失敗語意、資源保護與恢復控制,因此歸入 backend。訂單介面冪等題重點解決一次寫入操作的資料庫契約;本題只把冪等當成「逾時後能否重試」的判斷條件,核心是整條呼叫鏈的時間與負載預算。
面試官考察點
第一項訊號是先替請求建立一份時間帳本。端到端 800 毫秒不能等同於下游逾時 800 毫秒;本地驗證、排隊、序列化與回傳回應都需要預算。每次嘗試還要涵蓋連線、TLS 與讀取階段,並在開始下一次重試前檢查剩餘時間是否足夠。
第二項訊號是依失敗語意決定是否重試。建立連線前失敗、短暫 503 與帶有重試提示的 429 可能值得重試;參數、認證與權限錯誤不會因等待而修復。寫入請求在送出後逾時,結果可能是「已提交但回應未知」,只有穩定冪等鍵、狀態查詢或對帳機制才能把盲目重試變成安全重試。
第三項訊號是能算出重試放大。五層呼叫若每層都執行最多三次嘗試,最底層可能收到 3^5 = 243 次呼叫。重試應集中在一個理解業務截止時間的層,並受嘗試次數、累計時間與權杖式重試預算約束。
最後看恢復閉環。逾時限制一次等待,重試處理短暫失敗,斷路器在持續失敗時快速拒絕,隔離限制並行與佇列占用;它們職責不同。強回答還會約束半開探針、拒絕不真實的 fallback,並分別觀察邏輯請求與實際嘗試次數。
回答前需要釐清的問題
- 呼叫是讀取還是有副作用的寫入? 讀取通常可重複;寫入逾時後結果可能未知,需要冪等鍵、查詢狀態或對帳。
- 800 毫秒是硬截止時間還是觀測目標? 本題把它當成呼叫方不再等待的截止時間,上游取消後下游也應盡快取消,避免完成無人需要的工作。
- 下游公開了哪些可重試錯誤? 應依協定與供應商契約分類,尤其是 429 的
Retry-After、503、連線錯誤與業務錯誤,不能只按「非 200」處理。 - SDK、代理與服務網格是否已經重試? 必須盤點所有層的預設行為與最大嘗試次數,否則應用只重試一次也可能疊加成重試風暴。
- 下游的延遲分布與容量是多少? 逾時從分位數、可接受的誤逾時率與網路餘量推導,並在部署新連線、跨地域與尖峰負載下驗證。
- 失敗時允許怎樣降級? 商品稅費若有合規要求,不能偽造一個「成功」的預設稅額;可以明確失敗、轉人工或延後結帳,但要由業務契約決定。
- 斷路狀態按什麼邊界隔離? 應按獨立供應商、地域、端點或操作設定,避免一個分片故障讓所有健康資源一起被斷開。
30 秒回答框架
「我先把 800 毫秒拆成端到端預算,並向下游傳遞絕對截止時間。每次呼叫都有連線與請求逾時,下一次嘗試只有在剩餘預算容得下時才開始。錯誤按可恢復性與副作用是否已知分類:429 遵守服務端提示,短暫連線錯誤與選定 5xx 才重試;寫入結果未知時必須有冪等鍵或查詢對帳。重試只放在一層,最多兩次嘗試,使用有上限的指數退避與抖動,再用權杖預算限制故障期額外流量。持續失敗時斷路器開啟並快速失敗,冷卻後只放少量半開探針;並行隔離與有界佇列保護本地資源。最後用故障注入核對截止時間、實際嘗試數、重複副作用與恢復過程。」
分步驟深入解答
先把時間變成可計算的預算。以本題為例,800 毫秒中預留 120 毫秒做呼叫前本地工作,100 毫秒做呼叫後的回應處理,留給稅費依賴的總預算是 580 毫秒。一種可驗證的初始設定是:首次嘗試最多 220 毫秒,失敗後等待 [0, 80) 毫秒的隨機退避,第二次嘗試最多 220 毫秒;最壞占用 520 毫秒,依賴預算還剩 60 毫秒。數字不是通用答案,正式環境值要從下游分位數、誤逾時容忍度、跨網延遲與壓測中校準。
呼叫鏈傳遞絕對截止時間或單調遞減的剩餘預算。準備重試時重新計算 remaining,若它小於「下一次嘗試上限 + 最小收尾時間」,直接回傳,不啟動註定來不及的呼叫。用戶端要明確連線逾時與請求逾時的含義,並確認 DNS、TLS 與連線池等待是否包含在內;新執行個體可在接流量前預熱連線,避免握手被誤判為下游變慢。上游取消後應向下游傳遞取消,但仍要假設取消可能到得太晚。
再建立失敗語意矩陣:
| 結果 | 是否重試 | 前提與動作 | |---|---|---| | 建立連線前失敗 | 可以 | 剩餘預算足夠;使用退避與抖動 | | HTTP 429 | 有條件 | 遵守 Retry-After;等待後仍須能在截止時間內完成 | | HTTP 503 或選定 5xx | 有條件 | 供應商聲明為暫時故障,且重試預算未耗盡 | | HTTP 400、401、403 | 通常不可以 | 修正參數、憑證或權限,原請求等待後不會成功 | | 已送出寫入請求後逾時 | 不能盲目重試 | 使用同一冪等鍵,或先查詢操作狀態並對帳 | | 上游已取消或截止時間已到 | 不可以 | 停止增加無效工作並回傳明確失敗 |
重試策略同時受三道門控制:錯誤可重試、剩餘時間足夠、重試預算還有權杖。退避採用有上限的指數增長,並加入隨機抖動以打散同時失敗的用戶端;若服務端提供可用的重試時間,應優先遵守。最大嘗試次數包含首次呼叫,本題先從 2 次嘗試開始。讀清 SDK 設定,因為 maxAttempts = 2 與「重試 2 次」可能分別表示總共 2 次與總共 3 次。
重試只放在一個最了解業務截止時間的層。若五層都嘗試三次,最底層理論負載會被放大 243 倍。可用 downstreamattempts / logicalrequests 作為重試放大係數;正常時接近 1,故障時必須受預算上限約束。權杖桶或類似重試配額在失敗增加時耗盡,隨後暫停或以低速允許重試,避免所有用戶端在下游最脆弱時繼續加壓。
斷路器包裹具體依賴操作。關閉狀態允許呼叫,並在有最小樣本量的滑動視窗內統計可歸因的失敗與慢呼叫;超過閾值後進入開啟狀態。開啟狀態不存取下游,立即回傳可識別的失敗或經業務批准的降級。冷卻期結束後進入半開,只允許少量並行探針;探針達到成功條件才關閉,任一關鍵失敗則重新開啟。閾值必須來自流量與恢復特徵,不能把範例裡的「20 個請求、50% 失敗」當成所有服務的常數。
斷路器本身也有代價:狀態模式會讓測試與恢復更複雜,冷卻過長會延遲恢復,邊界過粗會誤傷健康分片。因此要記錄每次狀態變更,限制半開探針並行,並按真實故障域分組。對短暫、低風險故障,嚴格的逾時、單層重試與重試配額可能已經足夠;不應為了術語完整而強行增加斷路器。
本地資源還需要隔離。替稅費服務設定獨立並行上限、連線池與有界佇列,防止慢呼叫占滿結帳服務的全部執行緒或連線。佇列中的請求也消耗截止時間,出列時若已無法完成就捨棄。fallback 只回傳業務上真實且可解釋的結果:允許顯示「暫時無法計算稅費」或進入人工流程,不能把未知稅費當成零並宣告結帳成功。
驗證要在故障條件下檢查不變量。分別注入連線失敗、250 毫秒慢回應、429 攜帶不同 Retry-After、503、不可重試 400,以及「寫入操作已提交但回應遺失」。斷言單個邏輯請求最多產生 2 次下游嘗試、總延遲不越過 800 毫秒、永久錯誤不重試、未知寫入結果不重複執行。再持續注入失敗讓斷路器開啟,確認請求快速失敗、半開只有有界探針、依賴恢復後能重新關閉,同時檢查隔離池未耗盡。
線上指標要分清邏輯請求與嘗試:端到端成功率與 p95/p99、每次嘗試的結果與耗時、連線與請求階段逾時、重試放大係數、重試挽回率與新增延遲、重試預算餘額、斷路狀態與拒絕量、半開探針結果、並行池與佇列深度,以及冪等衝突與對帳結果。只看最終成功率會隱藏「成功是靠三倍下游流量換來的」。
高品質示範回答
「我會先確認這是同步稅費查詢,端到端呼叫方 800 毫秒後就不再等待。假設本地前置工作預留 120 毫秒、回傳處理預留 100 毫秒,依賴預算是 580 毫秒。初始設定用兩次嘗試:首次與第二次各最多 220 毫秒,中間做 [0, 80) 毫秒抖動退避;每次開始前都檢查傳遞下來的剩餘截止時間,所以排隊變長時不會機械地送出第二次。
錯誤必須分類。建立連線前失敗、供應商定義的暫時 503 可以在預算內重試;429 遵守 Retry-After,若等待後會越過截止時間就直接失敗;400、401、403 不重試。若呼叫會寫入,送出請求後的逾時屬於結果未知,必須重用冪等鍵,或先查狀態與對帳,不能換一個新請求盲目重試。
我會盤點 SDK、閘道與服務網格的預設重試,把重試集中在一個能看見業務截止時間的層,並設定權杖式重試預算。五層各三次嘗試會把底層呼叫放大到 243 倍,所以我要監控實際嘗試數除以邏輯請求數。
持續失敗時,按稅費供應商與操作設定斷路器。關閉狀態在有最小樣本的視窗內計算失敗率,開啟後快速失敗;冷卻後半開只放少量探針,成功才恢復。稅費依賴使用獨立並行池與有界佇列,降級只能回傳業務認可的明確狀態,不能偽造零稅費。
最後我會做故障注入,驗證最大兩次嘗試、800 毫秒邊界、不重試永久錯誤、沒有重複寫入、斷路能開啟且靠有界探針恢復。線上同時看邏輯成功率、嘗試級結果、重試放大、預算餘額、斷路狀態與隔離池飽和度。」
常見錯誤
- 把下游逾時直接設為 800 毫秒 → 本地收尾沒有時間,上游放棄後下游仍占資源 → 從端到端截止時間扣除本地與網路預算,並傳遞剩餘時間。
- 所有非成功回應都重試 → 參數與權限錯誤不會自癒,只會增加負載與延遲 → 按協定、錯誤碼與副作用結果分類。
- 每層都開啟三次嘗試 → 五層呼叫可把底層放大到 243 次 → 只在一個合適層重試,並監控放大係數。
- 寫入請求逾時後換新 ID 重送 → 第一次可能已提交,導致重複扣費或建立 → 重用冪等鍵,或查詢狀態與對帳。
- 只有指數退避,沒有抖動與次數上限 → 用戶端仍可能在相同時間成批重試 → 使用有上限的指數退避、隨機抖動與累計時間上限。
- 把斷路器當成逾時替代品 → 已經送出的慢呼叫仍會占用執行緒、連線與佇列 → 每次嘗試保留逾時,同時做並行隔離。
- 半開後立即放開全部流量 → 剛恢復的依賴再次被壓垮 → 只允許有界探針,並用明確成功條件恢復。
- 一個全域斷路器覆蓋所有供應商與端點 → 局部故障會阻斷健康資源 → 按獨立故障域劃分斷路狀態。
- fallback 一律回傳成功 → 把未知結果偽裝成正確資料,破壞業務語意 → 只使用經業務批准、可說明限制的降級。
- 只看最終成功率 → 重試風暴可能暫時掩蓋下游惡化 → 同時看邏輯請求、實際嘗試、額外延遲與資源飽和度。
追問及應對
追問一:逾時應設定成下游 p99 的多少倍?
沒有固定倍數。先選可接受的誤逾時率,從對應延遲分位數出發,再加入網路、跨地域、連線建立與小幅抖動餘量;同時受上游截止時間約束。若 p99 與 p50 很接近,小幅延遲變化就可能觸發大量逾時,需要額外 padding。上線前還要驗證 DNS、TLS、連線池等待是否包含在計時範圍內。
追問二:429 和 503 都可以重試嗎?
都只能有條件重試。429 應優先遵守 Retry-After,但等待與下一次嘗試必須放得進剩餘預算;否則直接把可識別失敗回傳上層。503 只有在供應商契約聲明為暫時故障時才進入退避。兩者都要消耗重試預算,不能把服務端已經過載的訊號轉化成更多即時流量。
追問三:為什麼重試挽回率很高,系統仍可能不健康?
因為最終成功可能由額外流量與額外延遲換來。若 100 個邏輯請求產生 180 次下游嘗試,放大係數是 1.8;下游已接近容量時,這 80% 的附加流量會延遲恢復。把重試挽回率與放大係數、嘗試延遲、預算耗盡率和下游飽和度一起看,才能判斷收益是否值得。
追問四:斷路器開啟後,還需要限流或隔離嗎?
需要。斷路器只阻止特定依賴的新呼叫;本地可能仍有已經在途或排隊的請求,其他依賴也可能繼續占資源。並行上限、獨立連線池與有界佇列控制資源所有權,入口限流控制新負載。恢復時,半開探針還需要獨立小額度,不能與正常流量共享無限容量。
追問五:多個執行個體各自維護斷路狀態,會不會不一致?
會,但共享強一致狀態也會增加同步依賴與延遲。常見做法是每個執行個體用相同設定維護本地狀態,讓故障訊號自然擴散;高流量時各執行個體都有足夠樣本。若執行個體流量很低或故障域需要全域協調,可由服務網格或集中層執行。無論哪種方案,都要說明狀態範圍、樣本量與故障時的最大額外流量。
追問六:什麼時候應改成非同步佇列?
如果業務不要求在本次 HTTP 回應裡得到稅費結果,或下游恢復時間可能遠超使用者截止時間,非同步化更合適。請求先持久化任務並回傳可查詢狀態,消費者按獨立重試與死信策略處理。非同步佇列沒有消除冪等、過期與降級問題,但能把長時間恢復從同步連線與 800 毫秒預算中移走。
追問七:如何上線而不因設定錯誤製造事故?
先只記錄擬執行的逾時、重試與斷路決策,不實際增加重試或拒絕流量;核對 SDK 是否已有隱藏嘗試。隨後按供應商或少量執行個體灰度,限制全域重試預算,並保留快速關閉開關。重點觀察嘗試量、端到端尾延遲、斷路開啟時長、半開失敗與下游容量,再逐步擴大。