退款供應商變慢了。原本一秒內回來的請求,開始等到逾時。值班工程師把 worker 從兩個加到八個,想把待辦清掉;幾分鐘後,逾時更多,最早那筆退款還躺在佇列裡。
監控上的八個 worker 都很忙,客人卻沒有比較快拿到結果。
這是訂單系列的虛構案例。前幾篇已經處理付款晚到後的退款處理、待辦索引與畫面新鮮度。這次要收住外呼出口:首次嘗試、重試與結果核對,都必須算進同一份供應商呼叫額度。多開 worker 不能憑空增加這份額度。
每次退款只試三次,仍可能一起塞住出口
「每筆最多三次」限制單筆花費,沒有約束同一秒有多少筆一起發送。假設每秒新進十筆退款,每筆最多三次嘗試,最壞情況每秒會形成約三十次外呼需求;這是持續流入下的粗估,不是任何單一秒的精確上限。再遇到整批工作同時醒來,瞬間尖峰還會更高。
若 HTTP SDK、worker 與外層排程各自重試,連「三次」是在哪一層算的都會失真。AWS Builders’ Library說明重試會加重已經過載的服務,也建議避免多層各自放大重試,並以退避與 jitter 分散呼叫時間。
本案例把重試決策集中在退款派送器。SDK 的自動重試必須納入同一計數,或在這個呼叫路徑關閉後由派送器負責。不能只看工作執行了幾次,要能數出實際送到供應商的嘗試。
限速與在途數,守的是兩種資源
系統仍是模組化單體、MySQL 持久待辦與少量 worker。第一版只安排一個供應商派送程序,停機時待辦留在資料庫;不為了做限速就先部署一套分散式協調服務。部署時先停止舊程序領取與發送,確認舊程序已退出,才讓新程序取得派送權;重啟後先保守等待上一輪本地呼叫時限,再恢復平滑發送,不把停機期間當成可累積的額度。這仍不證明遠端已停止,未決案件要繼續核對。它有單點可用性的代價,但外呼額度的所有權清楚。
以下數字都是教學假設,未經壓測,也不是任何金流業者的限制:
- 每秒最多開始五次外呼,平滑發送,相鄰啟動至少間隔二百毫秒,不累積閒置額度來爆量補送。
- 最多十個本地在途呼叫。即使到了下一個發送時點,只要十個位置都被占用,也先不送。
- 每筆呼叫設連線與整體時限,範例整體時限三秒;實作需確認 DNS、TLS 與讀取是否都被涵蓋。
每秒五次控制啟動速率,十個在途位置控制本地同時等待的資源。若平均每次耗時零點五秒,五次/秒約占二點五個在途位置;若每次都等滿三秒,十個位置最多只能支撐約 10 ÷ 3 = 3.33 次/秒。取平均作這種估算只適合穩態理解,尖峰與尾延遲仍要另外量測。
本地逾時只代表我們不再等待,不保證供應商停止執行。所以十個位置不是「供應商最多同時執行十筆退款」的證明;遠端取消與查詢能力必須依實際契約確認。
持久待辦
首次嘗試與重試/核對交替取件,各組依受理時間排序。
共用出口
啟動間隔至少 200 ms,且本地在途少於 10 個,兩者都符合才送出。
沒有額度就留在待辦。多開 worker 不增加額度。
確認案件去向
已確認的結果記錄終局;未知結果繼續核對。超過自動預算轉人工,案件不刪除。
發送間隔要守在真正交給 HTTP transport 的位置,不能先發給 worker 一張「可呼叫」許可,任它排隊幾秒後再用。假設十個 worker 每隔二百毫秒拿到許可,卻全卡在另一個本地佇列;佇列恢復時十個一起送出,許可日誌雖然漂亮,出口仍爆量。派送器應在準備啟動呼叫時,同時確認在途空位與下一個允許時點;實際啟動後,才從這次啟動時刻往後排二百毫秒。等待時不占資料庫連線,也不補發錯過的時槽。這是本案例的派送設計,需用下文的替身服務驗證。
首次呼叫、可安全重試的呼叫、結果核對,都要經過這個出口。若供應商各端點有不同配額,可再分子額度,但不能漏算共同的帳號總限制。增加第二個派送程序前,也必須決定額度如何分配;各自拿五次/秒,總數就已翻倍。
逾時的退款,先留下「不知道」
派送器拿到錯誤後,要先判斷它告訴了我們多少事。
欄位驗證失敗等明確拒絕,修正資料以前不重送。供應商明確回覆暫時無法接受,而且契約允許重試,才安排下一次嘗試。不能只憑所有 4xx 或所有 5xx 就做同一個決定。
送出後斷線、逾時或程序中斷,可能已經退款,只是答案沒回來。這類工作保留為結果待確認,沿用原業務操作識別值,優先利用供應商查詢、通知或對帳資料收斂結果。若契約明確提供仍有效的冪等重送能力,也可以照該契約重送;不能換一個新鍵來碰碰運氣。
Stripe 的 API v1 冪等文件是具體例子:同鍵會回傳已保存的第一次回應,包括部分 500 結果;鍵被清除後再次使用可能被視為新請求。這說明「有冪等鍵」仍需要閱讀保存期限與錯誤語義,不能假定無限期安全。本篇的虛構供應商沒有被假定成 Stripe。
等待中的工作只保存下次可執行時間,不占住資料庫交易或連線睡覺。領取工作時用短交易建立處理權,交易結束後才外呼。程序在送出與記錄結果之間崩潰,接手者仍要依上述未知結果流程處理,不能把租約到期當成遠端未執行。
等待拉開了,總量還是要限制
對已確認可以重試的工作,範例使用帶隨機性的指數退避:第二次嘗試等一至二秒,第三次等一至四秒。這只是本案例的排程方式;供應商若給了有效的 Retry-After,至少等到該時點,再受共用出口限制。工作到期可執行,不代表立刻取得發送權。
本案例設定每個業務操作最多三次自動退款嘗試,並在受理後十五分鐘仍無終局時轉人工核對。核對查詢另有有限次數及間隔,仍計入五次/秒總額度,避免退款重送少了、查詢風暴卻來了。這些值都要與實際退款時效、冪等有效期間及維運能力一起調整;超過自動處理預算,保留案件與責任人,不刪除未完成的退款。
如果所有工作共用 FIFO,一批重試可能讓新工作長時間排不到。第一版可在首次嘗試與重試/核對兩組待辦間交替取件,各組按最早受理時間排序;某組空了才讓另一組使用空位。這會讓新工作有進展,也讓舊案不被永久餓死。分組不增加總額度,人工補送也必須走同一出口。
Google SRE 的過載處理章節提醒,光看 QPS 不能完整描述資源成本,也討論在客戶端限制發送。對退款系統,這些原則的落點是同時觀察外呼速率、在途量、成功結案率與最老待辦年齡;worker 忙碌率不是完成率。
佇列能延後壓力,不能兌現無限等待
假設故障期間兩分鐘完全無法結案,每秒仍受理兩筆,就累積二百四十筆。恢復後若每秒確實能結案五筆,而新進仍是兩筆,淨消化速度是三筆/秒,理想上約八十秒清完。
這裡用的是「成功結案五筆/秒」,不是「發出五次/秒」。如果每筆平均耗兩次外呼,五次/秒只夠約二點五筆/秒,扣除新進兩筆後,二百四十筆要約四百八十秒才能清完;核對查詢也會再消耗額度。這些都是簡化推算,未模擬排程、失敗與延遲分布。
持續流入大於成功結案能力時,任何佇列最後都會滿。對尚未承諾受理的新申請,可以在接近容量或時效上限時明確拒絕並提供查詢方式;已受理、或業務上必須履行的退款,則保留並告警、擴大人工處理能力,不能靜默丟棄,也不能回報已完成。
另一個方案是讓客人的 HTTP 請求同步等退款結果。它少一段背景派送流程,低流量時容易理解;代價是供應商變慢會直接占用應用資源,客人斷線後仍需要未知結果核對。這個案例已有持久退款待辦,因此選擇背景處理,並承擔排程與可觀測性的成本。
若恢復後最老案件仍無法在承諾期限內結案,才討論協調更高供應商配額、縮減非必要查詢,或依供應商/帳號隔離派送。單純多開 worker,只在本地執行能力確實是瓶頸、且外部額度與資料庫容量都有餘裕時才有幫助。
驗收要看供應商變慢的那十分鐘
本文沒有執行金流 API 或壓測。以下是實作驗證計畫,不能當成通過結果。
在隔離環境用可控制的替身服務,依序注入快速成功、延遲三秒、先完成再中斷回應、明確拒絕與恢復正常。在 transport 邊界記錄每次實際啟動的單調時鐘時間、類型、操作識別值與本地在途數,再與替身服務收到的請求逐筆對上,抓出 SDK 隱藏重試。啟動間隔應至少二百毫秒,在途數最多十;替身收到的時間還受網路影響,不能拿接收間隔直接冒充本地啟動間隔。也不要只看整分鐘平均 QPS,短暫的一波十次會被平均掉。
讓一批工作同時到期,先暫停派送程序,恢復後觀察它是否逐筆重新排二百毫秒,而非補送暫停期間「欠下的」時槽。同時放入首次呼叫、重試、核對與人工補送,確認四種都出現在同一份外呼紀錄,沒有旁路。
重啟則分開驗兩個問題。第一個是額度所有權:暫停舊程序但不要結束它,嘗試啟動新版本,部署流程必須阻止新版本派送;放行舊程序時也不能出現兩個出口。僅看到某筆工作的租約失效,不足以證明舊程序不會再送其他工作。第二個是結果恢復:讓替身先記下已受理的退款,再於本地落庫前中止程序;確認舊程序退出後才啟動接手者,案件應保持未知並進行核對,同一業務操作不被換鍵重做。等待三秒只能清理本地等待預算,不代表替身或真實供應商已停止處理。
超過次數與十五分鐘上限的案件,要能在人工清單找到,並有可追溯的處理紀錄。
最後持續送入新工作,量故障解除後的最老待辦年齡是否下降。只有呼叫成功率恢復、積壓卻繼續增加,仍未恢復服務承諾。
回到那八個忙碌的 worker,先把每秒實際外呼與成功結案畫在同一條時間軸上。如果外呼升高、結案不動,再沿同一個操作識別值數它被送了幾次。這時要收住的是多層重試與旁路派送;繼續加 worker,只會讓同一件未確認的事做得更密。
練習
故障後累積六百筆退款,每秒新進三筆;供應商允許每秒十次外呼,每筆平均需要兩次外呼才能結案。忽略其他瓶頸時,多久才能清完積壓?若產品要求兩分鐘內清完,你會先確認哪個假設,並調整哪項承諾或容量?
參考思路
十次外呼約支撐五筆結案,扣除三筆新進,淨消化兩筆/秒,需要三百秒。兩分鐘清六百筆需要每秒額外結案五筆,加上新進共八筆,按每筆兩次需要每秒十六次外呼。先確認「平均兩次」有沒有漏算核對、重試與長尾;配額未增加前,不能靠加 worker 承諾兩分鐘。也可以協商受理速度或結案期限,但已受理案件的責任要保留。
