iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Software Development

30 天 Python 專案工坊:環境、結構、測試到部署全打通系列 第 14

Day 14 - 失敗即常態:例外分層、重試與降級(tenacity)

  • 分享至 

  • xImage
  •  

在前 13 天,我們已經讓專案具備了環境一致性、型別契約、測試藍圖,以及結構化日誌。

專案「能跑、能測、能記錄」。

但現實世界告訴我們一件事:再完美的程式,也不可能零錯誤。

網路 API 可能 timeout、資料庫連線可能失敗、雲端服務可能短暫掛掉。

這不是「例外狀況」,而是日常。

因此,工程化的下一步,就是要讓專案能帶著「失敗」繼續運作。


為什麼要設計「失敗處理」?

沒有設計的錯誤處理,往往只有:

try:
    call_api()
except Exception:
    print("Something went wrong")

問題是:

  • 一刀切:所有錯誤都被同等對待,無法分辨可恢復與致命錯誤。
  • 不可觀測:只會輸出 print(),無法追蹤問題。
  • 不可恢復:沒有 retry 或替代方案,系統直接崩潰。

要真正「工程化」,我們需要一套完整的策略:例外分層 → 重試 → 降級


一、例外分層:區分錯誤種類

良好的錯誤處理,第一步是「分層」。

常見的分類方式:

  • 可恢復錯誤(Retryable)

    例如:HTTP 503、資料庫連線中斷、暫時性網路抖動。

  • 不可恢復錯誤(Fatal)

    例如:程式碼 bug(KeyError、TypeError)、資料遷移失敗、無法解析配置。

  • 業務錯誤(Business Logic Error)

    例如:使用者餘額不足、權限驗證失敗。

設計時應避免一律用 except Exception,而是針對不同層級自訂例外:

class ExternalServiceError(Exception): ...
class TemporaryNetworkError(ExternalServiceError): ...
class FatalSystemError(Exception): ...

這樣在 log 與監控時,就能區分「需要重試」還是「立即告警」。


二、重試策略:用 tenacity 優雅處理失敗

tenacity 是 Python 社群常用的「重試框架」。

它透過裝飾器,把複雜的 retry/backoff/jitter 策略包裝好。

基本範例

from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def fetch_data():
    print("calling API ...")
    raise TimeoutError("service not responding")

fetch_data()

執行時會自動:

  • 最多重試 3 次。
  • 每次失敗後等待 2 秒、4 秒、8 秒(指數回退)。

👉 好處:

  • 不必手寫 try/except + sleep。
  • 可以透過 logging 輕鬆整合日誌。
  • 適合處理「暫時性錯誤」。

三、降級策略:系統不能完全掛掉

即使重試,還是可能失敗。

這時候要問自己:如果外部服務不可用,系統能怎麼「降級」?

常見做法:

  1. 回傳快取結果:例如天氣 API 掛掉,就回傳最後一次快取的值。
  2. 回傳預設值:比完全崩潰更好,例如回傳「系統忙碌中」。
  3. 局部停用功能:非關鍵模組暫停,核心功能仍可繼續。

範例:

from tenacity import retry, stop_after_attempt, RetryError

@retry(stop=stop_after_attempt(3))
def call_payment_api():
    raise TimeoutError("payment service down")

def safe_call_payment():
    try:
        return call_payment_api()
    except RetryError:
        return {"status": "fallback", "message": "Payment service unavailable"}

👉 降級不是偷懶,而是讓「非核心」失敗不會拖垮整個系統。


四、專案整合:錯誤處理 × 日誌 × 測試

延續 Day 11(測試策略)與 Day 13(日誌),我們可以把錯誤處理整合進去。

logging + tenacity

import structlog
from tenacity import retry, stop_after_attempt, before_sleep_log

logger = structlog.get_logger()

@retry(stop=stop_after_attempt(3), before_sleep=before_sleep_log(logger, "warning"))
def fetch_user():
    logger.info("fetching user")
    raise TimeoutError("service timeout")

輸出 JSON 日誌時,會記錄:

  • 第幾次 retry。
  • 錯誤原因。
  • 相關 context(user_id、service_name)。

測試:確保降級路徑

def test_payment_fallback():
    result = safe_call_payment()
    assert result["status"] == "fallback"

這樣能保證 fallback 策略不會被遺漏。



結語

錯誤不是例外,而是常態。

工程化的 Python 專案,必須在設計上把 「失敗」當成一等公民,才能在不穩定的世界裡保持穩健。

透過 例外分層、重試與降級,我們不只是「寫能跑的程式」,而是「寫能帶著錯誤活下去的系統」。

明天 Day 15,我們將進一步探討 序列化與設定格式:orjson、YAML、TOML 實務,看看如何在資料交換與設定管理上,做到效能與工程化的平衡。 🚀


上一篇
Day 13 - 結構化日誌:logging/structlog 與 JSON Log
系列文
30 天 Python 專案工坊:環境、結構、測試到部署全打通14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言