iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0

本日程式碼:repo tag day-12

昨天 relayer 已經有辦法救一筆卡住的交易,但它對失敗的補救依然是不斷重複「Nack 回 queue、五秒後再交付一次」;RPC 抽筋就算了,但如果「這條鏈根本沒有設定簽名金鑰」的話那重試到死都不會成功。今天要處理的就是這件事:一次失敗到底值不值得再交付一次。

今天的目標

我們今天要做一個判失敗的元件。假設有一次失敗進來的話,它會決定「這份 job 還要不要再交付」以及「要的話隔多久」。沿用的公開設計有三個:

  1. AWS Architecture Blog 的 Exponential Backoff And Jitter 把三種 jitter 實際量過一遍,我們可以參考他的最終結果來決定實作
  2. 根據 SQS 的 maxReceiveCount:「the number of times a consumer can receive a message from a source queue」,我們實作上可以統計交付次數(delivery)而非失敗次數
  3. 根據 Azure Functions 對 poison message 的處理,「up to five times ... including the first try」,也就是一則訊息連第一次一起算最多跑五次

repo 中 internal/relayerExample_poisonJob 用三筆付款示範三種失敗(Example 把 max attempts 縮到 3 次、關掉 jitter,輸出才短又固定)。輸出長這樣:

policy  at most 3 deliveries; back off 5s doubling, capped at 2m0s

worker  pi_0001/settle   #1  poison       (retrying will not help; nothing was sent, so the intent failed)
worker  pi_0002/settle   #1  retry        (send: rpc: connection refused)
worker  pi_0002/settle   #2  retry        (settling for 5s without tx hash, waiting)
worker  pi_0002/settle   #3  poison       (no luck after 3 deliveries; last broadcast unknown, needs review)
worker  pi_0003/settle   #1  retry        (not authorized yet)
worker  pi_0003/settle   #2  retry        (not authorized yet)
worker  pi_0003/settle   #3  poison       (no luck after 3 deliveries; job dropped, intent still created)

pi_0001 failed       hold voided
pi_0002 needs_review hold pending
pi_0003 created      hold -
  • pi_0001 的 Sender 宣告了兩件事:這筆確定沒發送出去,而且重試不會好
  • pi_0002 只是一直逾時,沒有人宣告任何事,只能 backoff 到 attempts 用完為止
  • pi_0003 從頭到尾還沒 authorized,relayer 一個 byte 都還沒寫過

三份 job 最後都停止重試,但最下面那三行的結果完全不一樣。

進 poison 的兩種情形

poison 是訊息佇列世界的既有術語「poison message(毒訊息)」:一則 consumer 永遠無法處理成功的訊息。它會一再地被重新交付,卻永遠不會成功,所以也永遠不會消失。

有兩種情形會進 poison,但各自都有點瑕疵:

左邊要求呼叫端事先想得到,可是寫 Sender 的人不可能列得完鏈上會回什麼;右邊不需要任何人事先分類,缺點是一個一眼就沒救的錯誤也要陪跑完整輪,中間每一次都吃掉一份 lease 與一個 worker 的一圈。

所以最正確的判定是兩種條件要依照順序進行判定:

宣告擺在最前面,是因為它是唯一「知道為什麼」的一條,知道的時候就不該再浪費一次交付去確認。ErrPoison 的用法跟昨天的 ErrNotSent 一樣,包在錯誤裡讓 errors.Is 拆得開,但預設的方向剛好相反:昨天預設偏保守,沒宣告的一律當成「不知道有沒有發送出去」;今天預設偏樂觀,沒宣告的一律當成重試會好。因為猜錯的代價不一樣,昨天猜錯是拿同一個 nonce 去撞自己那筆躺在 mempool 的交易,今天猜錯只是多試幾次,而且 max attempts 能 cover。

backoff 的下限比上限重要

固定五秒改成 exponential backoff:5、10、20、40 秒,每次加倍,封頂在兩分鐘。加倍是為了讓一個真的壞掉的節點有時間被修好,封頂是為了不要讓一份 job 隔幾個小時才被看一次。這裡有一個很容易寫錯的地方:加倍要用迴圈,不能用位移,因為 attempt 是 queue 給的數字,一份 job 被交付幾百次完全有可能,而位移 64 次就繞回零,backoff 會安靜地變成不 backoff。

真正需要挑的是 jitter。N 個 worker 同時撞上同一個壞掉的節點時,它們的重試會排在同一秒,醒來又一起撞一次,所以算出來的時間還要打散。AWS 那篇把三種 jitter 量過一遍,結論是 full jitter(在 0 到 d 之間隨機)最省事,而我這裡選的是 equal jitter:

func EqualJitter(d time.Duration) time.Duration {
	if d <= 0 {
		return 0
	}
	half := d / 2
	return half + time.Duration(rand.N(int64(d-half)+1))
}

一半固定、一半隨機,下限就守住了。full jitter 可以退到接近零,而我們每一次重新交付都要吃掉一份 lease 與一個 worker 的一圈,退到零就是白跑一圈。

還有一個數字要對一下:最糟情形的總時長必須撐得過 StuckAfter。repo 裡預設交付 10 次、第一階 5 秒、封頂兩分鐘,加起來大約十分鐘,比 StuckAfter 的五分鐘長。反過來的話,一筆卡在 settling 的付款會在救援發生之前就先被判死,那一整套替換等於沒有接上。

為什麼停止重試不等於宣告失敗

這非常重要:一份 job 只是一張「去看看它」的便條,intent store 才是唯一的真相。所以一份 job 被判 poison,意思是「這張便條再交付幾次都一樣」,不是「這筆付款失敗了」。判完之後要重讀一次 intent,照它現在停在哪一格決定要不要動它:

  • createdauthorized 那條路是 pi_0003 走的:relayer 在這兩格一個 byte 都還沒寫過,而且轉移表上它在這裡只有一條出口,就是往前推到 settling。宣告一筆付款失敗不是它的權力,所以它只丟便條。工作也沒有因此消失,那筆 intent 還在,之後誰再丟一份新的 job 進來,照樣被處理。
  • settling 那條路才是今天新開的:settling → failed 這一列從 Day 4 定案到現在沒有人走過,今天它第一次有主人。條件很嚴,要上一次廣播「確定沒發送出去」才算,也就是三種發送結果裡唯一敢說鏈上乾淨的那一種;其他兩種一律推 needs_review,因為錢可能已經動了。
  • 宣告 failed 之前還要先把那筆 hold 放掉,順序跟記 hold 的時候一樣是帳先動、狀態後走。中間死掉重來一次沒關係,void 重放是 no-op、intent 再走一次 failed;反過來先宣告 failed 的話,那筆 hold 會永遠掛在 pending 上,因為 failed 是 terminal,沒有人回得來收尾它。
  • 那被判死的 job 本身呢?今天直接從 queue 上拿掉了,因為三種處置之後那筆 intent 都停在一個接得下去的地方:兩種等人來看,一種等下一份 job。至於那張便條要不要留起來、留在哪裡、之後怎麼放回去,之後會討論。

小結

今天我們討論的是 job 失敗怎麼判斷:分成 retryable 和 poison 兩類,進 poison 的原因是自己宣告錯誤或重試次數用完;backoff 的策略是每次加倍、封頂、加 equal jitter,下限比上限重要,而重試最糟情形的總時長要撐得過 StuckAfter,不然救援永遠輪不到。最後是判決的範圍:分類判的是 job,relayer 只有在確定什麼都沒發送出去的時候才敢宣告 failed,其他一律交出去。

明天我打算給那些停止重試的 job 一個收容的地方,不會讓它們直接從 queue 上消失。

明天見。


上一篇
Day 11 | Transaction Replacement:卡鏈救援與手續費加速機制
下一篇
Day 13 | DLQ Redrive 設計:不重複打款的保證,與人工介入的邊界
系列文
Web3 支付工程筆記:企業級穩定幣結算系統與多鏈架構實戰16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言