回到文章列表
    疑難雜症·2026-05-22

    那個只在週一早上爆掉的 API

    詭異 bug 通常不是程式碼的錯,而是環境、時間、快取與假設交織出來的結果。這篇聊聊一個典型的「只在特定時間出現」的錯誤情境,怎麼從現象一路追到根因,以及為什麼最該被檢討的往往是當初寫下的那個假設。

    那個只在週一早上爆掉的 API

    「在我電腦跑都正常」這句話,工程師都聽到耳朵長繭。但比這更折磨人的,是「在 production 也都正常,只有禮拜一早上會壞」。

    這類間歇性錯誤是疑難雜症裡最難處理的一群。它不會在你盯著螢幕時出現,不會在 staging 環境重現,log 裡只有一段沒頭沒尾的 timeout,然後一切又恢復正常,像什麼事都沒發生過。

    從現象往回推,而不是從程式碼往前猜

    面對這類 bug,最常見的錯誤是直接打開可能出問題的那支 service,盯著程式碼看半小時,試圖用「讀」的方式找出問題。這幾乎不會成功,因為如果用看的就看得出來,當初就不會寫成那樣。

    比較有效的順序是反過來:先把現象描述到極致精確,再往回推。

    舉個例子,假設今天有個訂單同步 API 在週一早上九點左右會頻繁 timeout,其他時間都正常。值得先問的問題是:

    • 它是「比較慢」還是「完全沒回應」?
    • 是某幾筆訂單壞,還是全部訂單都慢?
    • 上游服務(資料庫、第三方金流、ERP)在那個時間點的指標長什麼樣?
    • 那個時間點有沒有任何排程在跑?備份?報表?cache 失效?

    光是把這些問題列出來,通常就能砍掉一半的可能性。週一早上的特殊性,多半不是程式碼的問題,而是「那個時間點環境跟其他時間不一樣」。

    真正的 bug 通常藏在假設裡

    回到剛才那個假設的情境。如果繼續追,可能會發現原因是這樣的鏈條:週日晚上有個全量報表排程,把資料庫的某張表鎖了一段時間;週一早上九點開店,前端的快取剛好過期,瞬間湧入大量查詢;查詢卡在那張還沒解鎖的表上,連鎖造成 connection pool 用盡,於是後續所有請求都 timeout。

    這條鏈裡每一段單獨看都沒錯。報表要跑、快取要過期、查詢要查資料庫,這些都是合理的設計。錯的是當初寫這套系統的人,假設了「報表跑完前不會有人查那張表」,而這個假設從沒被寫進文件,也沒被任何測試保護。

    # 示意:常見的「看起來沒問題」的程式碼
    def get_orders(user_id):
        cached = cache.get(f"orders:{user_id}")
        if cached:
            return cached
        # 快取沒命中就打 DB —— 但沒考慮同時失效時的 thundering herd
        rows = db.query("SELECT ... FROM orders WHERE ...")
        cache.set(f"orders:{user_id}", rows, ttl=3600)
        return rows
    

    這段程式碼在 99% 的時間都跑得很好。問題只會在快取大規模同時失效、且下游剛好被別的東西卡住時才浮現。它不是 bug,它是一個被環境放大的隱性假設。

    排查這類問題的關鍵,不是去責備寫程式的人,而是把那條沒被寫下來的假設挖出來、寫進系統的行為裡——例如加上 jitter 讓快取不要同時過期、給查詢設超時、給 connection pool 加上監控告警。

    我們的觀察

    做客製化系統做久了,我們的經驗法則是:絕大多數「詭異 bug」不是哪一行程式寫錯,而是某個從沒被講清楚的假設,在某個特殊時刻被環境戳破。除錯的價值,一半在修好那次故障,另一半在順手把那個假設明文化,讓下一個維護的人不用再從頭踩一次。

    #除錯#後端開發#系統整合#工程思維

    下一步

    需要把這裡的觀點落地到自家系統?

    協和數位專做客製化電商、AI Agent 與雙平台 APP。第一次諮詢免費,會幫你拆解可行性與優先順序——即使最後沒合作也沒關係。

    寫信給我們 →