Episode 3 covered detection — how a system finds out something broke. Episode 4 is the next link: detection told you something's wrong, now what?


星期六,第四回

👦 姪子:叔叔,我按照你說的在專案裡設定了 timeout 和 log。昨天支付閘道的呼叫真的 timeout 了。我偵測到了,也記錄了 log,然後繼續往下走。

👨‍🦳 叔叔:然後呢?

👦 姪子:就這樣了。沒再發生其他事情。

👨‍🦳 叔叔:那是分析用的呼叫,還是扣款?

👦 姪子:……扣款。

👨‍🦳 叔叔:那「記錄 log 然後繼續」不算處理。那只是看著它發生。

👦 姪子:所以只靠偵測還不夠。

👨‍🦳 叔叔:偵測告訴你有東西出錯了。處理則是決定你實際要做什麼。今天有六個工具。

1. Retry
2. Exponential Backoff
3. Fallback
4. Queue
5. Circuit Breaker (preview — full episode later)
6. Graceful Degradation
7. Dead Letter Queue

Enter fullscreen mode Exit fullscreen mode


第一部分 — Retry

👨‍🦳 叔叔:這份清單裡最簡單的概念。呼叫失敗了,你就再試一次。這有什麼問題?

👦 姪子:沒什麼吧?失敗了就再試一次。

👨‍🦳 叔叔:永遠都這樣做嗎?

👦 姪子:……我覺得你有陷阱。

👨‍🦳 叔叔:你昨天那個 timeout 的支付閘道呼叫,如果你立刻重試,閘道那邊實際發生了什麼?

👦 姪子:它 timeout 了,所以……它失敗了?

👨‍🦳 叔叔:真的嗎?還是只是沒及時收到回應?

👦 姪子:等等。這兩件事不一樣。

Uncle: 非常不一樣。Timeout 並不能告訴你扣款到底有沒有成功。

You                    Payment Gateway
 | -- charge request --> |
 |                       | (processes it successfully)
 |  <-- X response lost--| (network drops the response)
 | -- times out, retry-->|
 |                       | (charges again!)

Enter fullscreen mode Exit fullscreen mode

👦 姪子:所以我可能重複向客戶扣款,而我的「處理」反而造成了這個問題。

Uncle: 這就是陷阱。那麼,什麼情況才適合重試?

👦 姪子:……執行兩次也不會改變任何東西的東西?

👨‍🦳 叔叔:沒錯。這個特性有個名字——idempotency,它是 Module 3 的獨立模式。今天先記住:重試安全的東西,碰到錢、庫存或狀態變更時要特別小心。


第二部分 — Exponential Backoff

👨‍🦳 叔叔:假設這裡重試是安全的。你應該立刻重試,還是連續重試三次?

👦 姪子:為什麼不呢?恢復得比較快。

👨‍🦳 叔叔:如果有 10,000 個客戶同時打到一個已經很吃力的服務,而且全部都立刻重試——那個服務會發生什麼事?

👦 姪子:……它會被打得更慘。我反而讓情況變得更糟。

👨‍🦳 叔叔:沒錯。所以每次失敗後,你要等久一點,給服務喘息的空間。

Attempt 1 fails → wait ~2s
Attempt 2 fails → wait ~4s
Attempt 3 fails → wait ~8s
Attempt 4 fails → wait ~16s
Attempt 5 fails → give up, surface the error

Enter fullscreen mode Exit fullscreen mode

👦 姪子:那如果 10,000 個客戶都在同一秒失敗——他們不也會按照完全相同的時間表 back off 嗎?結果不還是會一起重試?

👨‍🦳 叔叔:你剛好找到 jitter 存在的原因——在上面再加一點隨機延遲,讓重試分散成小量流入,而不是一秒內大量湧入。

Without jitter:  1,000 clients retry at EXACTLY the same millisecond
With jitter:     1,000 clients retry spread across a small random window

Enter fullscreen mode Exit fullscreen mode


第三部分 — Fallback

👨‍🦳 叔叔:有時候正確的回應不是「再試一次」,而是「改做別的事」。

👦 姪子:例如?

👨‍🦳 叔叔:你的推薦服務掛了。使用者應該看到什麼?

👦 姪子:錯誤訊息吧?「推薦功能暫時無法使用。」

👨‍🦳 叔叔:你比較想看到錯誤訊息,還是看到通用的「熱門商品」列表?

👦 姪子:……當然是熱門商品列表。沒有人想為了這麼小的事看到錯誤。

👨‍🦳 叔叔:這就是 fallback——用持續運作來換取完美功能。

Ideal path:    User → Personalized Recommendations   (best experience)
Failure path:  User → Popular Items fallback          (good enough experience)
Worst case:    User → Error page                      (avoid this if at all possible)

Enter fullscreen mode Exit fullscreen mode

👦 姪子:那是不是每個東西都值得有 fallback?

👨‍🦳 叔叔:你會想要在支付失敗時有 fallback 嗎——「無法驗證扣款,所以我們隨機扣你一筆金額」?

👦 姪子:天啊,不要。

👨‍🦳 叔叔:沒錯。Fallback 是用在「不完美」勝過「什麼都沒有」的情況。有些失敗就必須直接失敗,而且要清楚、正確地失敗。


第四部分 — Queue

👨‍🦳 叔叔:你的註冊流程是同步發送歡迎信,也就是在請求裡面一起處理。Email 服務掛了五分鐘。註冊會發生什麼事?

👦 姪子:他們……會失敗?還是卡住,等 Email 服務?

👨‍🦳 叔叔:註冊真的需要等信發出去嗎?

👦 姪子:……不需要。帳號在那之前就已經建立好了。

👨‍🦳 叔叔:所以不要讓它等。把 Email 工作推到安全的地方,馬上回應使用者,讓 worker 在 Email 服務恢復後再發送。

Without a queue:
Signup request → wait for email service → respond    (slow, and fails if email is down)

With a queue:
Signup request → push job to queue → respond immediately
                        |
                   Worker processes it whenever the email service is ready

Enter fullscreen mode Exit fullscreen mode

👦 姪子:所以 queue 把「必須現在成功」變成「最終會成功」。

👨‍🦳 叔叔:沒錯——單單這個轉變,就消除了整個類別的失敗。這就是 roadmap 裡的 BullMQ + Redis 組合——BullMQ 負責 queue 和 retry 邏輯,Redis 存放 job 資料。


第五部分 — Circuit Breaker(預覽)

👨‍🦳 叔叔:再來一個快速預覽——完整內容會在 Module 3 後面說明。下游服務完全掛了。每個請求還是會去試它、等待、timeout、失敗。這個畫面有什麼問題?

👦 姪子:你……每一次、每一個請求都在浪費等待時間?

👨‍🦳 叔叔:沒錯。所以連續失敗幾次之後,系統就直接停止嘗試——暫時停止。

First few requests → try, fail, fail → breaker "trips" (OPEN)
        |
All further requests → fail IMMEDIATELY, no wasted waiting
        |
After a cooldown → breaker allows ONE test request through
        |
If it succeeds → breaker closes, normal traffic resumes
If it fails → breaker stays open, wait longer

Enter fullscreen mode Exit fullscreen mode

👦 姪子:所以它是故意、暫時放棄,而不是無止盡地抱著希望?

👨‍🦳 叔叔:快速且低成本地放棄,勝過每一個請求都緩慢且高成本地失敗。完整狀態和實作會有獨立的一集。


第六部分 — Graceful Degradation

👨‍🦳 叔叔:這其實不算新工具。它是前面三個工具背後的哲學。當某個東西失敗時,你願意失去的最小東西是什麼?

Full system:
[ Core Checkout ] [ Recommendations ] [ Reviews ] [ Live Chat Support ]

If Recommendations service fails:
[ Core Checkout ] [ (fallback: skip it) ] [ Reviews ] [ Live Chat Support ]
   ↑ still works, users can still buy things

Enter fullscreen mode Exit fullscreen mode

👦 姪子:所以這就是 fallback 背後的「為什麼」——即使 20% 的 nice-to-have 功能壞掉,也要讓重要的 80% 繼續運作。

👨‍🦳 叔叔:沒錯。Fallback、timeout、circuit breaker——它們都是為了回答同一個問題的工具:「如果這個元件失敗了,我的系統要怎麼以損害最小的方式繼續運作?」


第七部分 — Dead Letter Queue

👨‍🦳 叔叔:回到你的 queue。一個 job 失敗了。重試一次、兩次、三次。還是失敗。現在它會怎麼樣?

👦 姪子:……它會一直試下去嗎?

👨‍🦳 叔叔:你會希望它這樣嗎?

👦 姪子:不會——那會永遠浪費資源在明明永遠不會成功的東西上。

👨‍🦳 叔叔:那就直接丟掉、不留痕跡嗎?

👦 姪子:那感覺更糟。你會失去 job,卻永遠不知道發生過什麼。

👨‍🦳 叔叔:沒錯——這兩個選項都不可以接受。所以它會去一個特定的地方:Dead Letter Queue。不是被刪除,也不是無止盡重試——而是被保留下來,等待人類實際查看。

Job fails → retry 1 → retry 2 → retry 3 → still failing
        |
Moved to Dead Letter Queue (not deleted, not silently dropped)
        |
Engineer reviews later: "why did THIS specific job keep failing?"

Enter fullscreen mode Exit fullscreen mode

👦 姪子:所以 DLQ 基本上就是穿著 queue 衣服的偵測工具。

👨‍🦳 叔叔:完全正確。它把處理和偵測之間的迴路關閉了。


第八部分 — 為失敗配對正確的工具

👨‍🦳 叔叔:最後一個。我給你失敗情境,你告訴我該用什麼工具。單一次網路閃斷,而且這個呼叫可以安全重複。

👦 姪子:Retry with backoff。

👨‍🦳 叔叔:下游服務完全、徹底掛掉。

👦 姪子:Circuit breaker。沒必要一直去敲一扇已經壞掉的門。

👨‍🦳 叔叔:一個非核心功能——例如推薦功能——剛剛失敗。

👦 姪子:Fallback。或者說 graceful degradation,其實是同一個概念。

👨‍🦳 叔叔:不需要在這一毫秒內完成的工作。

👦 姪子:把它排進 queue。

👨‍🦳 叔叔:不管你怎麼做都會一直失敗的 job。

👦 姪子:Dead letter queue——停止盲目重試,讓人類來看。

👨‍🦳 叔叔:你剛剛自己把整張表都寫完了。

Situation Right tool
A single network blip on a safe-to-repeat call Retry + backoff
A downstream service is completely down Circuit Breaker
A non-critical feature fails Fallback / Graceful Degradation
Work that doesn't need to happen instantly Queue
A job that keeps failing no matter what Dead Letter Queue
An operation that changes money/state and might get retried Idempotency (Module 3)

👨‍🦳 叔叔:而今天整個課程真正可以用一句話總結:最可怕的重試,不是那個又失敗一次的重試,而是那個悄悄成功兩次的重試。


Practical Node.js Implementation

今天提到的六個工具,濃縮在一個地方的真實程式碼。

Retry

async function callPaymentGateway(payload) {
  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      return await paymentApi.charge(payload);
    } catch (err) {
      if (attempt === 3) throw err;
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Exponential backoff with jitter

async function retryWithBackoff(fn, maxAttempts = 5) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === maxAttempts) throw err;
      const delay = Math.min(1000 * 2 ** attempt, 30000);
      const jitter = Math.random() * 300;
      await new Promise(r => setTimeout(r, delay + jitter));
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Fallback

async function getRecommendations(userId) {
  try {
    return await recommendationService.getFor(userId);
  } catch (err) {
    logger.warn({ userId }, 'Recommendation service down, using fallback');
    return getPopularItems();
  }
}

Enter fullscreen mode Exit fullscreen mode

Queue

const { Queue } = require('bullmq');
const emailQueue = new Queue('emails', { connection: redisConnection });

app.post('/signup', async (req, res) => {
  const user = await createUser(req.body);
  await emailQueue.add('welcome-email', { userId: user.id });
  res.status(201).json(user);
});

Enter fullscreen mode Exit fullscreen mode

Dead letter queue (via BullMQ's built-in failed state)

const emailQueue = new Queue('emails', {
  connection: redisConnection,
  defaultJobOptions: {
    attempts: 3,
    backoff: { type: 'exponential', delay: 2000 }
  }
});

const failedJobs = await emailQueue.getFailed();

Enter fullscreen mode Exit fullscreen mode


👦 姪子:好的。所以我們已經偵測到問題,現在也處理了。

👨‍🦳 叔叔:沒錯。

👦 姪子:那接下來呢?危險已經被控制住了——系統要怎麼真正恢復到完全健康的狀態?

👨‍🦳 叔叔:那就是 recovery 要開始的地方。

👦 姪子:星期六?

👨‍🦳 叔叔:星期六。


Episode 4 我們涵蓋了什麼

  • Retry — 強大,但用在 payment 這類非 idempotent 操作時很危險
  • Exponential backoff + jitter — 在不讓已經過載的服務變得更糟的情況下重試
  • Fallback — 用持續運作來換取完美功能
  • Queue (BullMQ + Redis) — 把「必須現在成功」變成「最終會成功」
  • Circuit Breaker — 停止對已經掛掉的服務浪費呼叫的預覽
  • Graceful Degradation — fallback 和 circuit breaker 背後的設計原則
  • Dead Letter Queue — 永久失敗的 job 該去的地方,而不是消失或永遠重試
  • 為不同類型的失敗配對正確的處理工具