cover

Debug 方法論:系統化除錯的藝術

流程概覽

flowchart LR
    A["問題發現"] --> B["重現 Reproduce"]
    B --> C["定位範圍 Isolate"]
    C --> D["建立假設"]
    D --> E["驗證假設"]
    E -->|假設錯誤| D
    E -->|假設正確| F["修復 Fix"]
    F --> G["回歸測試"]
    G -->|測試失敗| D
    G -->|測試通過| H["記錄 & 預防"]

    style A fill:#F44336,color:#fff
    style B fill:#FF9800,color:#fff
    style C fill:#2196F3,color:#fff
    style D fill:#9C27B0,color:#fff
    style E fill:#009688,color:#fff
    style F fill:#4CAF50,color:#fff
    style G fill:#3F51B5,color:#fff
    style H fill:#607D8B,color:#fff

文章概覽

在本篇文章中,你將學習到:

  • 為什麼 console.log 大法撐不到生產環境
  • 系統化除錯的完整流程:重現 → 定位 → 假設 → 驗證 → 修復 → 回歸
  • 二分法定位問題的思維模式(包含 git bisect
  • 前端、後端、系統層各自的 Debug 工具箱
  • Debug 的心法:改一個變數測一次、橡皮鴨除錯法
  • 三個實戰案例拆解
  • 一份可直接使用的 Debug 日誌模板

本文是 測試策略事故管理 之間的橋樑——測試告訴你「有問題」,事故管理告訴你「出事了怎麼辦」,而 Debug 方法論告訴你「問題出在哪裡,怎麼找到它」。


開場:為什麼「console.log 大法」不夠用

你一定遇過這種情況:

程式不 work 了,你的第一反應是加一行 console.log('here 1'),然後 console.log('here 2'),再來 console.log('here 3')… 最後你的程式碼裡面有 50 行 console.log,你盯著 terminal 裡面一堆 here 7here 12undefined[object Object],依然搞不清楚到底是哪裡壞了。

更慘的是,你忘記清掉那些 console.log,結果同事 code review 的時候看到滿螢幕的 console.log('為什麼你不 work')

console.log 不是不能用——它有它的位置,就像螺絲起子有它的位置。但你不會拿螺絲起子去拆引擎。問題出在:

問題為什麼 console.log 搞不定
非同步 Bugconsole.log 的輸出順序可能跟你以為的不一樣
生產環境問題你不可能在 production 加 console.log 然後重新部署
效能問題Log 本身不會告訴你哪行程式碼花了 800ms
間歇性 Bug出現機率 1/100 的 bug,你要跑 100 次才看得到那行 log
跨服務問題前端 log 跟後端 log 對不起來

Debug 是一門學科,不是碰運氣。就像 測試策略 教你用分層的方式設計測試,Debug 方法論教你用系統化的方式追蹤問題。

接下來,我們一步一步來。


第一步:重現(Reproduce)

一句話場景:Bug 報告進來了,你第一件要做的事不是看程式碼,是重現它。

這是 Debug 最重要的一步,也是最多人跳過的一步。

沒辦法重現的 bug,基本上沒辦法修。

為什麼?因為如果你沒辦法穩定重現問題,你就沒辦法確認你的修復是不是真的修好了。你以為你修好了,其實只是那個 bug 剛好沒出現而已。

最小重現步驟(Minimal Reproducible Example)

當 PM 跟你說「那個頁面壞了」,你需要把它變成一組精確的步驟:

## 重現步驟
 
1. 開啟瀏覽器,登入帳號 test@example.com
2. 進入 /dashboard 頁面
3. 點擊「匯出報表」按鈕
4. 選擇日期範圍:2025-01-01 ~ 2025-01-31
5. 點擊「確認匯出」
 
## 預期結果
下載一個 CSV 檔案
 
## 實際結果
頁面顯示白屏,console 有 TypeError: Cannot read property 'map' of undefined

重點在最小——你要把步驟精簡到只剩下觸發 bug 所需的最少操作。如果你可以跳過步驟 4 直接觸發 bug,那日期範圍跟這個 bug 無關,你可以排除一整個方向。

環境差異檢查

有時候 bug 只在特定環境出現。這時候你需要比對:

檢查項目開發環境StagingProduction
Node.js 版本18.17.018.17.016.14.0
資料庫SQLitePostgreSQLPostgreSQL
環境變數.env.local.env.staging.env.prod
資料量100 筆1,000 筆1,000,000 筆
SSL
CDN

「在我電腦上可以跑啊」的正確處理方式

當你聽到這句話(或你自己想說這句話)的時候,正確的做法是:

  1. 比對環境:OS、Runtime 版本、瀏覽器版本、螢幕解析度
  2. 比對資料:你用的測試資料跟出問題的資料一樣嗎?
  3. 比對操作:你的操作順序跟使用者一模一樣嗎?有沒有快取影響?
  4. 比對時間:問題是不是只在特定時間出現?(cronjob、rate limit、token 過期)

如果以上都比對完了還是重現不了,那就用「錄影」——請使用者錄下操作過程,或者在程式碼中加入更詳細的 log(這時候 log 是合理的工具,因為你需要收集資訊)。


第二步:定位範圍(Isolate)

一句話場景:你已經能重現 bug 了,現在要把問題範圍從「整個系統」縮小到「某一行程式碼」。

二分法:把問題範圍縮小一半再一半

這是 Debug 最強大的思維工具。想像你有 1000 行程式碼可能有問題,你不需要一行一行看——你只需要問:「問題在前 500 行還是後 500 行?」然後再問:「在前 250 行還是後 250 行?」10 次二分之後,你就能定位到具體的那一行。

git bisect:找出哪個 commit 引入了 bug

這是二分法最經典的應用。假設你知道上禮拜的版本是好的,今天的版本壞了,中間有 128 個 commit:

# 開始 bisect
git bisect start
 
# 標記目前的版本是壞的
git bisect bad
 
# 標記上禮拜的版本是好的
git bisect good abc1234
 
# Git 會 checkout 到中間的 commit,你測試一下
# 如果這個版本是好的:
git bisect good
 
# 如果這個版本是壞的:
git bisect bad
 
# 重複 7 次(log2(128) = 7),就能找到那個引入 bug 的 commit
# 結束後記得 reset
git bisect reset

128 個 commit,只需要 7 次測試就能定位。這搭配 CommitLint 規範 良好的 commit message,效果更佳——你找到那個 commit 後,清楚的 commit message 會告訴你當時改了什麼、為什麼改。

註解掉一半程式碼

有時候你不是在找哪個 commit 有問題,而是在找哪一段程式碼有問題。同樣用二分法:

// 先註解掉後半段,看問題還在不在
function processData(data) {
  const step1 = transform(data);
  const step2 = validate(step1);
  const step3 = enrich(step2);
  // const step4 = format(step3);
  // const step5 = save(step4);
  // return step5;
  return step3; // 暫時在這裡回傳
}

如果問題消失了,代表問題在後半段(step4 或 step5)。再把後半段二分一次就找到了。

由外而內:Network → Backend → Database → Business Logic

當你不確定問題在哪一層的時候,從最外面開始看:

flowchart TD
    A["使用者看到錯誤"] --> B{"Network tab
有沒有 request 失敗?"}
    B -->|有| C{"Status code
是什麼?"}
    B -->|沒有| D["前端邏輯問題
(沒發出 request)"]
    C -->|4xx| E["前端送錯 request
或是權限問題"]
    C -->|5xx| F{"Backend log
有什麼錯誤?"}
    F --> G{"是 DB 相關
還是邏輯相關?"}
    G -->|DB| H["檢查 query、
connection pool"]
    G -->|邏輯| I["檢查 business logic
和 edge case"]

    style A fill:#F44336,color:#fff
    style D fill:#FF9800,color:#fff
    style E fill:#FF9800,color:#fff
    style H fill:#4CAF50,color:#fff
    style I fill:#4CAF50,color:#fff

由內而外:單元測試 → 整合測試 → E2E

反過來也可以。如果你懷疑某個函式有問題,先寫一個單元測試:

// 先用最小的範圍確認核心邏輯有沒有壞
test('calculateDiscount should apply 10% for orders over 1000', () => {
  expect(calculateDiscount(1500)).toBe(150);  // 如果這個失敗,問題在這個函式
});
 
// 如果單元測試過了,擴大範圍到整合測試
test('POST /api/orders should return discounted price', async () => {
  const res = await request(app).post('/api/orders').send({ amount: 1500 });
  expect(res.body.discount).toBe(150);  // 如果這個失敗,問題在 API 層
});

這個思路跟 測試策略 的分層概念完全一致——用不同粒度的測試來定位問題所在的層級。


工具箱:前端 Debug 工具

一句話場景:使用者回報「畫面怪怪的」,你需要找出是 CSS、JS、API、還是資料的問題。

Chrome DevTools 精華

Chrome DevTools 是前端工程師的瑞士刀。你不需要精通每一個 panel,但你需要知道什麼時候該用哪一個:

Elements Panel — DOM 即時修改

什麼時候用:畫面樣式跑掉、排版不對、某個元素消失了。

- 右鍵 → Inspect,直接看到對應的 DOM 節點
- 即時修改 CSS,不用改程式碼就能測試樣式
- 看 Computed 分頁,了解最終生效的樣式是什麼
- 找到 Box Model,確認 margin / padding / border 的值

Console Panel — 不只是 console.log

什麼時候用:需要快速測試某個表達式、看錯誤訊息、或者跟頁面互動。

// 你知道這些進階用法嗎?
 
// console.table — 把陣列/物件用表格顯示
console.table(users);
 
// console.trace — 印出呼叫堆疊(call stack)
console.trace('誰呼叫了這個函式?');
 
// console.group / console.groupEnd — 把相關的 log 分組
console.group('API 回應處理');
console.log('原始資料:', rawData);
console.log('轉換後:', transformedData);
console.groupEnd();
 
// console.time / console.timeEnd — 測量執行時間
console.time('render');
renderComponent();
console.timeEnd('render');  // render: 234.56ms
 
// console.assert — 條件不成立時才印出
console.assert(user.age > 0, '使用者年齡不應該是負數', user);
 
// $0 — 在 Elements panel 選取的元素
// $_ — 上一個表達式的結果
// copy() — 把值複製到剪貼簿
copy(JSON.stringify(data, null, 2));

Network Panel — 看清楚前後端之間發生了什麼

什麼時候用:API 行為不如預期、回應太慢、CORS 問題、cookie 沒帶上。

重點觀察:
1. Status Code — 200/201/204/301/400/401/403/404/500
2. Request Headers — Authorization 有沒有帶?Content-Type 對不對?
3. Response Body — 回傳的 JSON 結構是不是你預期的?
4. Timing — DNS / Connect / TTFB / Content Download 各花了多久?
5. Waterfall — 哪些 request 是同時發出的?哪些是依序的?
6. 勾選 Preserve log — 頁面跳轉後也保留 log
7. 勾選 Disable cache — 確保每次都拿到最新的資源

Performance Panel — 找出 render bottleneck

什麼時候用:頁面卡頓、滾動不順、動畫掉幀。

1. 按下 Record,操作你覺得卡的功能
2. 停止 Record,看 flame chart
3. 找出佔最多時間的函式
4. 看 FPS 曲線 — 如果掉到 60fps 以下就有問題
5. 看 Layout Shift — 是不是有元素一直在重排

Application Panel — 儲存與快取

什麼時候用:登入狀態消失、資料沒更新、Service Worker 快取了舊版本。

- LocalStorage / SessionStorage:看儲存的資料對不對
- Cookies:Token 有沒有正確設定?HttpOnly? Secure? SameSite?
- Service Workers:有沒有舊的 SW 還在快取舊版 JS?
- Cache Storage:清掉看問題會不會消失

React DevTools / Vue DevTools

什麼時候用:元件的 state 或 props 不對,想看 Component Tree 和資料流。

React DevTools:
- Components tab:看每個元件的 props 和 state
- Profiler tab:錄製 render,找出不必要的 re-render
- Highlight updates:開啟後,每次 re-render 會閃綠框

Vue DevTools:
- Components:看 data、computed、props
- Vuex / Pinia:看 store 的 state 和 mutation 歷史
- Events:看元件之間的事件傳遞
- Timeline:看事件、mutation、route 變化的時間軸

Lighthouse

什麼時候用:整體效能評估、SEO 檢查、無障礙性評估。

搭配 CI 使用的時候,可以設定效能預算——每次部署前自動跑 Lighthouse,分數低於標準就阻擋部署。


工具箱:後端 Debug 工具

一句話場景:API 回傳 500、服務突然變慢、或者資料不一致。

結構化 Log

這是後端 Debug 最重要的基礎。如果你的 log 長這樣:

Error: something went wrong

那跟沒有 log 差不多。好的 log 應該長這樣:

{
  "timestamp": "2025-02-09T10:30:45.123Z",
  "level": "error",
  "service": "order-service",
  "traceId": "abc-123-def-456",
  "userId": "user_789",
  "action": "createOrder",
  "error": "insufficient_stock",
  "details": {
    "productId": "prod_001",
    "requested": 5,
    "available": 3
  }
}

關於結構化 Log 的完整設計,參考 Log 管理通用 Log 設計。這裡只講跟 Debug 直接相關的部分。

Log Level 策略

什麼時候用哪個 level?這是很多人搞混的地方:

Level什麼時候用範例
debug開發期間追蹤程式流程,production 通常關閉debug('Processing item', { itemId, step: 3 })
info正常的業務事件,production 保留info('Order created', { orderId, userId })
warn不正常但可以繼續運作的狀況warn('Retry attempt 3/5', { service, error })
error需要處理的錯誤,應該觸發告警error('Payment failed', { orderId, gateway, error })

我之前踩過一個坑:把所有東西都用 info 記錄。結果 production 的 log 量每天 50GB,找一個特定錯誤要翻好幾分鐘。後來改成嚴格的 level 策略——debug 只在開發環境開、info 記錄業務關鍵事件、error 搭配告警——log 量降了 80%,定位問題的速度反而變快了。

Request Tracing(Correlation ID / Trace ID)

什麼時候用:請求經過多個微服務,你需要追蹤完整的呼叫鏈。

// 在 API Gateway 或第一個接收請求的服務生成 trace ID
const traceId = req.headers['x-trace-id'] || uuid();
 
// 往下游傳遞
await orderService.create(orderData, { headers: { 'x-trace-id': traceId } });
await paymentService.charge(paymentData, { headers: { 'x-trace-id': traceId } });
await notificationService.send(emailData, { headers: { 'x-trace-id': traceId } });

有了 trace ID,你就可以在 log 系統中用一個 ID 串起整個請求鏈:

# 在 Kibana 或 Grafana Loki 中搜尋
traceId: "abc-123-def-456"

# 結果:
10:30:45.100 [api-gateway]      info  Incoming request POST /orders
10:30:45.150 [order-service]     info  Creating order
10:30:45.300 [payment-service]   error Payment gateway timeout
10:30:45.310 [order-service]     warn  Payment failed, rolling back
10:30:45.400 [api-gateway]      error Request failed: payment_timeout

一目了然——問題出在 payment gateway timeout。

Database Query Debugging

什麼時候用:API 回應很慢,懷疑是 DB query 的問題。

-- EXPLAIN:看查詢計畫,找出是不是全表掃描
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE user_id = 123
AND created_at > '2025-01-01';
 
-- 看到 Seq Scan(全表掃描)就要注意了
-- 如果資料量大,考慮加 index
 
-- Slow Query Log(MySQL)
-- 在 my.cnf 中設定
-- slow_query_log = 1
-- long_query_time = 1  -- 超過 1 秒的 query 會被記錄

常見的 DB 效能殺手

問題症狀解法
N+1 Query一個頁面發了 100 個 DB query用 JOIN 或 eager loading
缺少 IndexQuery 很慢,EXPLAIN 顯示 Seq Scan加 index
SELECT *撈了不需要的欄位,浪費記憶體和網路只 SELECT 需要的欄位
沒有分頁一次撈出 100 萬筆資料加 LIMIT/OFFSET 或 cursor pagination
Lock 競爭並發寫入時變慢檢查 transaction isolation level

API 測試工具

什麼時候用:需要手動測試 API、重現後端問題。

# curl — 最基本但最萬用
curl -X POST https://api.example.com/orders   -H "Content-Type: application/json"   -H "Authorization: Bearer token123"   -d '{"productId": "prod_001", "quantity": 5}'   -v  # -v 顯示完整的 request/response header
 
# httpie — 更人性化的 curl
http POST api.example.com/orders   productId=prod_001   quantity:=5   Authorization:"Bearer token123"

Postman 適合團隊協作和保存 API 集合,curl/httpie 適合快速測試和腳本自動化。


工具箱:環境與系統 Debug

一句話場景:程式碼看起來沒問題,但部署上去就壞了——可能是環境的問題。

Docker 容器除錯

# 看容器 log
docker logs <container_id> --tail 100 -f
 
# 進入容器內部
docker exec -it <container_id> /bin/sh
 
# 看容器的環境變數
docker exec <container_id> env
 
# 看容器的資源使用狀況
docker stats <container_id>
 
# 看容器的網路設定
docker inspect <container_id> --format='{{json .NetworkSettings}}'
 
# 最常見的坑:容器裡面的檔案權限
# 你的程式在本機跑得好好的,Docker 裡面卻 Permission denied
# 因為 Docker 預設用 root,但你的檔案可能是 node user
docker exec <container_id> ls -la /app/

網路除錯

# DNS 解析 — 域名有沒有正確解析
nslookup api.example.com
dig api.example.com
 
# 連通性 — 能不能連到目標
ping api.example.com
telnet api.example.com 443
 
# 路由追蹤 — 封包經過了哪些節點
traceroute api.example.com  # Linux/Mac
tracert api.example.com     # Windows
 
# 完整的 HTTP 對話 — 看 TLS handshake、redirect、header
curl -v https://api.example.com/health
 
# 連接埠檢查 — 哪些 port 正在監聽
netstat -tlnp  # Linux
ss -tlnp       # Linux(更現代的工具)

Process 除錯

# 誰在吃 CPU / Memory
top           # 即時監控
htop          # 比 top 更好看、更好用
 
# 看 process 的系統呼叫(Linux)
strace -p <pid> -f    # 追蹤系統呼叫
strace -e trace=network -p <pid>  # 只看網路相關的呼叫
 
# 看 process 打開了哪些檔案
lsof -p <pid>
 
# 看 process 的記憶體使用(Node.js)
node --inspect app.js  # 開啟 Chrome DevTools 遠端除錯
# 然後在 Chrome 打開 chrome://inspect

思維模式:Debug 的心法

工具固然重要,但 Debug 最核心的其實是思維方式。工具會更新換代,但思維模式是一輩子的。

「先讀 Error Message」

有沒有發現你花了兩小時在 debug,結果問題只是一個 typo?

80% 的人遇到 error 的第一反應是直接把 error message 貼到 Google。 但其實大部分 error message 已經告訴你問題在哪了——你只需要認真讀它。

TypeError: Cannot read properties of undefined (reading 'map')
    at UserList (UserList.jsx:15:23)
    at renderWithHooks (react-dom.development.js:14985:18)

這個 error message 告訴你:

  1. 錯誤類型:TypeError — 存取了 undefined 的屬性
  2. 哪個屬性:.map — 你對某個東西呼叫了 .map()
  3. 哪個檔案、哪一行:UserList.jsx:15
  4. 在哪個元件:UserList

你甚至不需要 Google,直接去 UserList.jsx 第 15 行,看那個被 .map() 的變數為什麼是 undefined 就好了。可能是 API 還沒回來、可能是回傳結構變了、可能是 optional chaining 忘記加。

「改一個變數測一次」

這是新手最常犯的錯誤:同時改了三個東西,然後問題消失了,但你不知道是哪個改動修好的。

黃金原則:每次只改一個變數,測試,確認效果,再改下一個。

這跟科學實驗的控制變因是一模一樣的概念。你不會在實驗中同時換試劑、溫度、和濃度——你一次只換一個,才能確定因果關係。

「寫下你的假設」

把你的假設寫在紙上(或任何地方),逐一排除:

## 假設清單
 
- [ ] 可能是 API 回傳格式變了(檢查 Network tab)
- [ ] 可能是快取問題(清 cache 試試)
- [ ] 可能是 race condition(加 loading state 試試)
- [x] 可能是環境變數沒設定(已排除,env 都有)
- [x] 可能是第三方套件更新了(已排除,lock 檔沒變)

寫下來有三個好處:

  1. 防止你在同一個方向鑽牛角尖
  2. 避免重複驗證已經排除的假設
  3. 事後回顧的時候知道你試過什麼

「解釋給橡皮鴨聽」— Rubber Duck Debugging

這個方法論出自《The Pragmatic Programmer》。做法很簡單:

  1. 拿一隻橡皮鴨(或任何東西,甚至是同事)放在桌上
  2. 一行一行地向它解釋你的程式碼在做什麼
  3. 當你解釋到某一行的時候說「等等,這裡不對」——恭喜你,找到 bug 了

為什麼有效?因為你腦中的模型和實際的程式碼之間有落差。當你被迫用語言描述每一步的時候,你會發現自己對某些地方的理解是錯的。

我自己的經驗是:大約 30% 的 bug,在我「準備要問同事」的時候就自己找到答案了。整理問題的過程本身就是 Debug。

「休息一下」

這不是在開玩笑。

有時候你盯著螢幕三個小時,腦中已經形成了固定的思考模式,你會不自覺地忽略某些可能性。這時候最好的做法是:

  • 站起來走一走
  • 去喝杯咖啡
  • 跟同事聊聊天(不一定要聊 bug)
  • 甚至去睡一覺

很多工程師都有這樣的經驗:散步回來看一眼就發現問題了。因為你的大腦在「背景模式」中重新整理了資訊。

Debug 的效率不是用時間衡量的,是用方向正確度衡量的。 花 30 分鐘在正確的方向比花 3 小時在錯誤的方向有效率得多。


實戰案例

案例一:API 回傳 200 但前端顯示錯誤

症狀:使用者回報「訂單列表空白」,但 Network tab 看到 API 回傳 200。

Debug 過程

1. 重現:登入測試帳號 → 進入訂單頁面 → 列表空白    可重現
 
2. 定位:
   - Network tab:API 回傳 200,有資料
   - Console:TypeError: orders.map is not a function
 
3. 假設:API 回傳格式變了
 
4. 驗證:
   前端預期:{ "orders": [...] }
   API 實際回傳:{ "data": { "orders": [...] } }
 
   後端加了一層 wrapper,但前端還在用舊的路徑取資料。
 
5. 修復:
   // Before
   const orders = response.orders;
   // After
   const orders = response.data.orders;
 
6. 預防:
   - 加上 API response 的型別定義(TypeScript interface)
   - 加上 API contract test,確保前後端對 response 格式有共識

學到的教訓:Status code 200 不代表資料結構跟你預期的一樣。永遠要看 response body。

案例二:只在 Production 出現的 Race Condition

症狀:使用者偶爾回報「付款成功但訂單狀態還是未付款」,在 staging 完全無法重現。

Debug 過程

1. 重現:在 staging 怎麼測都正常,無法重現
   → 加入更詳細的 log(帶 trace ID 和 timestamp)
   → 等 production 再次出現問題
 
2. 分析 log:
   10:30:45.100 [order-service]   info  Order #123 created (status: pending)
   10:30:45.200 [payment-service] info  Payment for #123 started
   10:30:45.800 [payment-service] info  Payment for #123 succeeded
   10:30:45.810 [payment-service] info  Updating order #123 status to paid
   10:30:45.150 [order-service]   info  Order #123 cache refreshed (status: pending)
 
   注意最後一行的時間戳!
   問題在於:order-service 在 10:30:45.150 重新讀取了快取,
   但 payment-service 要到 10:30:45.810 才更新狀態。
   快取拿到的是舊的 pending 狀態。
 
3. 根因:
   前端在建立訂單後立刻查詢訂單狀態(polling),
   但付款回調是非同步的。在高延遲的 production 環境中,
   快取更新和付款回調的順序會互相競爭。
 
4. 修復:
   - 付款成功後使用 WebSocket 推播通知,不靠 polling
   - 快取更新加上 invalidation 機制
   - 訂單狀態查詢加上 read-after-write consistency
 
5. 為什麼 staging 重現不了:
   staging 只有一台機器,延遲極低,快取更新幾乎是即時的。
   Production 有多台機器 + CDN + 分散式快取,延遲更大。

學到的教訓:如果只在 production 出現,通常跟延遲、並發、或資料量有關。加更多 log(帶時間戳)是唯一的辦法。

案例三:記憶體 Leak 的追蹤

症狀:Node.js 服務運行 2-3 天後記憶體用量從 200MB 緩慢爬升到 1.5GB,然後 OOM 被 kill。

Debug 過程

1. 確認問題:
   - 監控圖表顯示記憶體緩慢上升,沒有回落(GC 沒有回收)
   - 重啟後恢復正常,2-3 天後又爬上去
 
2. 抓 Heap Snapshot:
   // 在程式碼中加上 endpoint 來抓 heap snapshot
   app.get('/debug/heap', (req, res) => {
     const v8 = require('v8');
     const snapshot = v8.writeHeapSnapshot();
     res.json({ file: snapshot });
   });
 
   // 分別在啟動後 1 小時和 24 小時各抓一次
 
3. 用 Chrome DevTools 分析 Heap Snapshot:
   - Memory tab → Load snapshot
   - 比較兩個 snapshot 的 Comparison view
   - 發現大量的 EventListener 沒有被移除
 
4. 定位:
   在一個 WebSocket handler 中,每次連線都會 .on('data', handler),
   但斷線時沒有 .off('data', handler)。
 
   // Bug
   socket.on('connection', (conn) => {
     eventBus.on('update', (data) => conn.send(data));
   });
   // 斷線後 eventBus 上的 listener 還在,而且越積越多
 
5. 修復:
   socket.on('connection', (conn) => {
     const handler = (data) => conn.send(data);
     eventBus.on('update', handler);
     conn.on('close', () => eventBus.off('update', handler));  // 清除
   });
 
6. 預防:
   - 加入 EventEmitter.listenerCount 的監控
   - 設定 maxListeners 警告
   - 定期檢查記憶體使用趨勢

學到的教訓:記憶體 leak 通常不會立刻出現,需要長時間觀察。Heap Snapshot 的 Comparison view 是最有效的工具。


Debug 日誌模板

每次 Debug 完都應該留下紀錄。不是為了別人,是為了未來的你。三個月後遇到類似問題的時候,你會感謝當初的自己。

# Debug 日誌
 
## 基本資訊
- **日期**:2025-02-09
- **報告者**:PM / 使用者 / 監控告警
- **嚴重程度**:P0 / P1 / P2 / P3
- **影響範圍**:全部使用者 / 特定使用者 / 特定功能
 
## 問題描述
(用一句話描述問題,包含「預期行為」和「實際行為」)
 
## 重現步驟
1. ...
2. ...
3. ...
 
## 環境資訊
- 瀏覽器 / OS:
- 前端版本(commit hash):
- 後端版本(commit hash):
- 環境:dev / staging / production
 
## 嘗試過的方法
| # | 假設 | 驗證方式 | 結果 |
|---|------|----------|------|
| 1 | API 回傳格式錯誤 | 檢查 Network tab | 格式正確 |
| 2 | 快取問題 | 清除瀏覽器快取 | 問題依舊 |
| 3 | Race condition | 加 loading state | 問題消失 |
 
## 根因分析(Root Cause)
(詳細說明問題的根本原因)
 
## 修復方式
(說明你怎麼修的,附上 PR 連結)
 
## 預防措施
- [ ] 加上相關的自動化測試
- [ ] 更新監控告警規則
- [ ] 更新文件 / Runbook
- [ ] 分享給團隊(Post-mortem 會議或 Slack)
 
## 花費時間
- 定位問題:__ 小時
- 修復 + 測試:__ 小時
- 總計:__ 小時
 
## 學到的事
(一句話總結,未來遇到類似問題可以更快解決)

這個模板跟 事故管理 中的 Post-mortem 是互補的。Debug 日誌記錄的是技術細節,Post-mortem 記錄的是流程和組織改進。


總結:Debug 不是碰運氣

mindmap
  root((Debug 方法論))
    流程
      重現 Reproduce
      定位 Isolate
      假設 Hypothesize
      驗證 Verify
      修復 Fix
      回歸測試
    工具
      前端
        Chrome DevTools
        React/Vue DevTools
        Lighthouse
      後端
        結構化 Log
        Trace ID
        DB EXPLAIN
        curl / httpie
      系統
        Docker
        網路工具
        Process 監控
    心法
      先讀 Error Message
      一次改一個變數
      寫下假設
      橡皮鴨除錯
      休息一下
    記錄
      Debug 日誌模板
      Root Cause Analysis
      預防措施

Debug 的核心不是工具,不是技巧,而是紀律

  • 不要跳過重現步驟
  • 不要同時改三個變數
  • 不要只靠直覺
  • 不要忘記記錄

就像 測試策略 告訴我們測試需要分層,Debug 也需要分步驟。就像 事故管理 告訴我們事故需要 Post-mortem,Debug 也需要記錄和反思。

最後一個建議:下次遇到 bug 的時候,先深呼吸,打開這篇文章,走一遍流程。你會發現,大部分的 bug 都沒有你想像的那麼難——只要你不是在碰運氣。


延伸閱讀

  • 測試策略 — 用分層測試預防 bug,減少 debug 的機會
  • 事故管理 — Bug 到了 production 變成事故,怎麼處理
  • Log 管理 — 集中式日誌收集,讓你在一個地方搜尋所有 log
  • 通用 Log 設計 — Log 的欄位設計和格式規範
  • CommitLint 規範 — 好的 commit message 讓 git bisect 事半功倍