「程式在我電腦上是好的」這句話背後,通常藏著三種鬼。 一種是環境變數不一致、一種是時區與編碼、還有一種是那些沒人動過但預設值不對的設定檔。真正難查的 bug,很少是演算法錯,多半是這類「以為理所當然」的地方。
以下整理幾個實務上反覆出現的排查方向,不是完整清單,只是想提醒:遇到怪現象時,先別急著改程式碼。
訊號往往在邊界,不在邏輯裡
舉個例子,假設一個電商後台在測試環境跑得好好的,一上線就偶發性地把訂單建立時間存成前一天。第一直覺會想是不是排序寫錯、或是資料庫欄位型別不對。但如果多看幾筆資料,會發現「異常訂單」都集中在深夜到清晨那段時間——這時候就要懷疑時區。
常見的坑包括:
- 應用程式用 UTC,資料庫用本地時區,ORM 幫你「貼心地」轉了一次。
- Docker 容器沒設
TZ,預設就是 UTC。 - 前端用瀏覽器時區顯示,後端用伺服器時區寫入,兩邊對不齊。
類似的邊界問題還有字元編碼(客戶資料含中文姓名或特殊符號時特別容易爆)、換行符(Windows 開發、Linux 部署)、以及浮點數精度(金額千萬別用 float)。這些都不是「bug」,而是「你沒設定,系統就幫你選一個」的結果。
排查訣竅:當現象與時間、地區、特定字元、特定數值範圍相關時,先去看設定和環境,不要先看邏輯。
效能問題:慢的通常不是你以為的那段
另一個經典情境:某個 API 反應變慢,工程師直覺去優化最複雜的那段 SQL,改了半天沒改善。這時候如果願意加上 tracing,很可能發現真正的瓶頸在別處——例如連線池被打滿、外部服務 timeout 重試、或是某個中介層做了不必要的序列化。
幾個值得優先檢查的方向:
- N+1 查詢:ORM 用得太順手時最容易發生,看似一行程式碼,實際打了大量 query。
- 連線池大小:預設值通常偏保守,高併發時會變成排隊瓶頸。
- 同步呼叫外部 API:一個慢的第三方服務可以拖垮整條請求鏈路。
- 日誌與監控本身:同步寫入大量 log 或送 metrics 到遠端,也可能是元凶。
下面是個示意用的最小骨架,說明如何把可疑區段用時間標記包起來,不用先導入完整 APM:
import time, logging
def timed(label):
def deco(fn):
def wrap(*a, **kw):
t = time.perf_counter()
try:
return fn(*a, **kw)
finally:
logging.info(f"{label} took {time.perf_counter()-t:.3f}s")
return wrap
return deco
# 示意:實務上建議接 OpenTelemetry 或現成 APM
@timed("fetch_orders")
def fetch_orders(user_id):
...
把可疑的幾個區段都包起來,跑一次真實流量,你會很快看到哪一段吃掉最多時間——通常和你事前的假設不一樣。
我們的觀察
累積下來的經驗法則是這樣:遇到詭異 bug 時,先問「什麼變了」——部署、設定、依賴版本、資料量、使用時段。真正純粹的邏輯錯誤反而是少數,多數的疑難雜症都躲在環境與邊界條件裡。願意花時間讀 log、看 trace、對照設定的人,通常比會寫演算法的人更早找到答案。
