詭異 bug 之所以詭異,多半不是因為程式碼寫錯,而是你對系統的心智模型錯了。修了三天才發現問題在另一個你以為跟它沒關係的元件,這種挫折感工程師都懂。
我們在接手既有系統或處理跨團隊整合時,最常踩到的不是語法錯誤,而是「假設錯誤」——你以為某個 API 永遠回傳 JSON,但它在錯誤時會回 HTML;你以為快取永遠會更新,但某條路徑繞過了它;你以為背景任務每分鐘跑一次,但它其實會塞車。下面分享我們在排查這類問題時,常用的兩個切入角度。
從「時間」與「邊界」切入,比看程式碼更快
大部分難找的 bug 都有一個共通點:它不是隨機的,只是觸發條件你還沒看出來。
第一個有效的提問是「它什麼時候會發生?」假設一個典型情境:客戶回報後台報表「偶爾」會少筆資料。如果你直接跳進 SQL 看,可能查一整天都找不到。但如果先問「少的那幾筆有沒有共通點?」——是不是都集中在某個時段、某個時區的午夜、某個排程跑完前後、月底結算當下——往往一問就破案。時間維度能把「偶發」變成「規律」,規律一出現,bug 就無所遁形。
第二個提問是「邊界在哪裡?」系統與系統的交界處幾乎是 bug 的溫床:
- 字元編碼的邊界(資料庫、API、前端各用不同 collation)
- 時區的邊界(伺服器 UTC、資料庫本地時間、前端使用者時區)
- 型別的邊界(JavaScript 的 number 對上後端的 bigint、decimal 對上 float)
- 重試的邊界(前端 retry 配上後端非冪等 API)
當問題看起來「沒道理」時,先把資料流畫出來,標出每一個跨系統的邊界,bug 大概率就藏在其中一條線上。
可觀測性是省錢的,不是奢侈的
很多中小企業客戶會覺得 logging、tracing、metrics 是「等系統大了再說」的東西。我們的經驗法則剛好相反:愈是預算有限、人手有限的團隊,愈需要可觀測性,因為你沒有本錢花三天去猜一個 bug。
不需要一開始就上完整的 APM。最小可行的可觀測性大概是這樣:
# 示意:實際請依使用的框架與 logger 調整
import logging, time, uuid
logger = logging.getLogger(__name__)
def handle_order(order):
trace_id = str(uuid.uuid4())
start = time.time()
logger.info("order.start", extra={"trace_id": trace_id, "order_id": order.id})
try:
result = process(order)
logger.info("order.done", extra={
"trace_id": trace_id,
"order_id": order.id,
"elapsed_ms": int((time.time() - start) * 1000),
})
return result
except Exception as e:
logger.exception("order.fail", extra={"trace_id": trace_id, "order_id": order.id})
raise
重點不是這段程式碼本身,而是三件事:每一個請求有獨立 trace_id、關鍵節點都有結構化 log、失敗時保留完整堆疊。光是把這三件事做好,下一次客戶回報「剛剛下單失敗」,你不用再請他截圖、不用再猜時間,搜一下 trace_id 或時間區間就能定位。
我們的觀察
排查疑難雜症最大的成本不是修 bug 的時間,而是「不知道從哪裡開始」的時間。能縮短這段時間的,從來不是更聰明的工程師,而是更好的提問習慣與更早建立的觀測點。下一次系統出怪事,先別急著翻程式碼,先問它何時發生、跨了哪些邊界、留下了什麼線索——通常答案會比你想像中近。
