iT邦幫忙

2025 iThome 鐵人賽

DAY 26
0
Software Development

消除你程式碼的臭味系列 第 26

Day 26- 防禦性設計:處理外部例外狀況

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250903/20124462P1N8QjGguI.png

消除你程式碼的臭味 Day 26- 防禦性設計:處理外部例外狀況

剛開始寫程式時,我們都活在一個美好的世界裡:網路永遠暢通,API 總是秒回,伺服器從不宕機。

這種「天真的信任」,就是程式碼裡最重的一股臭味

看看這段程式碼,是不是很熟悉?

// 🔴 臭味:過於信任的網路呼叫
async function getUserProfile(userId) {
  // 天真地相信,這行程式碼永遠會成功
  const response = await api.fetch(`/users/${userId}`); 
  return response.data;
}

在語法上,它完全沒有錯。它看起來很乾淨、很直接,也確實能完成「取得使用者資料」這個任務。

問題不在於程式碼「寫錯了」,而在於它 沒寫的部分
根植於一個隱藏的、天真的假設:「await api.fetch(...) 這一行永遠會順利地在短時間內回傳我們想要的資料。」

在實際情況裡,背後可能藏著無數個會讓你的應用崩潰的陷阱:
https://ithelp.ithome.com.tw/upload/images/20250928/20124462tcYFBrNyNn.png

  • 如果對方服務很慢怎麼辦? 你的程式會卡在這裡,一直等到天荒地老,佔用著資源,讓後面的使用者也跟著一起等。

  • 如果對方服務剛好掛了怎麼辦? 你的程式會直接崩潰,使用者會看到一個醜陋的錯誤頁面。

  • 如果只是網路抖了一下,失敗了千分之一秒呢? 你的程式還是會失敗,但其實只要再試一次就好了。

首先:停止寫程式

專業的工程師,會預設所有外部依賴都會「背叛」他。

在寫任何 try/catch 或重試邏輯之前,先區分錯誤:「這是哪種失敗?」

失敗只有兩種,必須像對待不同物種一樣對待它們:

  1. 永久性錯誤 (Permanent Errors):

    • 定義: 重試一萬次也沒用的錯誤。

    • 例子: HTTP 404 Not Found, 403 Forbidden, 401 Unauthorized。你用錯誤的 ID 去請求一個不存在的用戶,再試幾次它也不會 magically appear。

    • 處理方式: 立即失敗。 記錄日誌,拋出例外,讓呼叫方知道「這件事沒戲了」。不要重試,那是在浪費資源。

  2. 暫時性錯誤 (Transient Errors):

    • 定義: 現在不行,但等一下可能就好了。

    • 例子: HTTP 503 Service Unavailable, 500 Internal Server Error, 504 Gateway Timeout, 網路連線逾時。服務可能正在重啟,網路可能只是抖動了一下。

    • 處理方式: 只有這種情況,才值得重試。

https://ithelp.ithome.com.tw/upload/images/20250928/20124462ojLrX4F7PE.png

防禦機制步驟

第 1 步:碼錶 (Timeout) - 設定底線

臭味:無止盡的等待。
動作:為每一次外部呼叫設定一個合理的等待時間,比如 2-3 秒。時間一到,我們就直接「掛電話」,防止整個應用程式被拖垮。

第 2 步:重試 (Retry) - 再給一次機會

臭味:因為暫時的小問題就徹底放棄。
動作:只對「暫時性」錯誤(如網路不穩)進行有限次數的、有禮貌的(等待間隔逐漸拉長)重試。

「有禮貌的」重試包含兩個關鍵:

  • 指數退避 (Exponential Backoff): 不要每隔 1 秒就重試一次。第一次失敗後等 1 秒,第二次等 2 秒,第三次等 4 秒... 這種逐漸拉長間隔的方式,能給對方服務喘息的空間,避免在它最脆弱的時候壓垮它。

  • 抖動 (Jitter): 如果有個情況是,你的 100 個服務實例都因為同一個問題失敗了,然後它們都按照完全相同的指數退避演算法,在同一毫秒甦醒,同時發起重試。這會形成可怕的「重試風暴 (Thundering Herd)」。為了解決這個問題,我們需要在等待時間上加入一個微小的隨機值,也就是「抖動」。這能將重試請求在時間上打散,避免形成流量洪峰。

https://ithelp.ithome.com.tw/upload/images/20250928/20124462C7MLiUPIAo.png

第 3 步:備案 (Fallback) - 優雅地處理失敗

臭味:把醜陋的系統錯誤直接丟給使用者。
動作:當確定外部服務不行了,提供預設資料、使用舊快取,或明確告知使用者部分功能暫時無法使用,優雅降級 (Graceful Degradation)

具體的備案場景可以很彈性:

  • 回傳快取資料: 如果之前有快取過這位用戶的資料,即使是幾分鐘前的舊資料,也比顯示錯誤頁面好。

  • 提供預設值: 前端請求使用者頭像失敗時,Fallback 可以是顯示一個預設的灰色頭像,而不是讓圖片區域出現一個破圖示。

  • 簡化版回應: 在電商網站,如果個人化推薦服務掛了,Fallback 可以是回傳一個固定的、所有人都一樣的「熱銷商品列表」,確保頁面功能依然完整。

第 4 步:保險絲 (Circuit Breaker) - 保護你的家

臭味:明知山有虎,偏向虎山行。
動作:當偵測到某個服務在短時間內大量失敗,就暫時「跳閘」,讓後續的呼叫立即失敗(直接走 Fallback 流程),不再浪費資源去嘗試,給自己和對方喘息的空間。

保險絲內部是精巧的狀態機,通常包含三個狀態:

  • Closed (閉合): 正常狀態,所有請求都直接通過。

  • Open (斷開): 當失敗率超過閾值,保險絲「跳閘」,在接下來的一段時間內,所有請求都會被立即拒絕,直接執行備案 (Fallback)。

  • Half-Open (半開): 斷開狀態持續一段時間後,保險絲會進入此狀態,放行一筆「探測」請求。如果成功,代表服務已恢復,狀態切回 Closed;如果失敗,則重新回到 Open 狀態繼續等待。

https://ithelp.ithome.com.tw/upload/images/20250928/20124462yVMiLWRpUE.png

防禦流程藍圖

這個機制把所有可能的失敗都當成了正常流程的一部分來處理。
https://ithelp.ithome.com.tw/upload/images/20250928/201244621DL02ZFEVc.png

從理論到實踐

看到最上面那張複雜的圖,你可能會想:「天啊,我要自己寫這麼多 if/else 嗎?」

答案是:絕對不要。

別急著自己去實現那個重試迴圈和指數退避,輪子已經有了,而且比你造的更好。
使用成熟的函式庫來執行這些策略。

我們的責任是理解這些策略,並為函式庫提供正確的設定。

// // 🟢 好味道:真正的專業手法,是「聲明」你的策略,讓函式庫去執行那張複雜的流程圖
const professionalPolicy = createPolicy([
  Timeout.of(1500), 
  Retry.of({ limit: 3, backoff: 'exponential' }),
  CircuitBreaker.of({ failureRate: 0.5 }),
  Fallback.of(getProfileFromFallback)
]);

// 業務程式碼保持極度乾淨,只關心它該關心的事
async function getProfessionalProfile(api, id) {
  return await professionalPolicy.execute(() => api.fetch(`/users/${id}`));
}

檢察建議

  1. 第一步永遠是區分錯誤類型。 在寫任何 catch 之前,先問自己:這個錯誤是 403 還是 503?如果是前者,直接失敗,記錄日誌。

  2. 用函式庫,別自己發明輪子。 這些模式的狀態管理很容易出錯。找一個小而美、經過檢驗的函式庫來做這件事。

  3. 從簡單的開始。 不是所有呼叫都需要一套完整的 Circuit Breaker。先從 Timeout 和針對「暫時性錯誤」的有限次數重試(帶 Exponential Backoff)開始。只有當這個依賴的故障會引發連鎖反應(雪崩)時,才需要引入 Circuit Breaker。

  4. Fallback 的回傳值必須是「合法」的。 它回傳的資料結構,必須和成功時的回傳一模一樣,即使是空的。回傳一個空的使用者物件,也比回傳一個 null 導致呼叫方 TypeError 要好一萬倍。

今日重點

  • 把外部失敗視為常態,提前設計保護。
  • 防禦性設計讓故障時的行為可預測。

成為一個讓你同事信賴的工程師

消除程式碼的臭味,不只是為了好看,更是為了責任感

  • 菜鳥的程式碼,在風和日麗時能跑。

  • 專業的程式碼,在狂風暴雨中也能存活。

把每一次外部呼叫都當成一個潛在的叛徒,用這四個步驟為你的程式碼建立防禦。
Timeout、Retry、Fallback、Circuit Breaker。


上一篇
Day 25- 資源管理:打開的東西就要關掉
下一篇
Day 27- 同步問題:管理多執行緒與競爭條件
系列文
消除你程式碼的臭味27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言