
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 7、here 12、undefined、[object Object],依然搞不清楚到底是哪裡壞了。
更慘的是,你忘記清掉那些 console.log,結果同事 code review 的時候看到滿螢幕的 console.log('為什麼你不 work')。
console.log 不是不能用——它有它的位置,就像螺絲起子有它的位置。但你不會拿螺絲起子去拆引擎。問題出在:
| 問題 | 為什麼 console.log 搞不定 |
|---|---|
| 非同步 Bug | console.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 只在特定環境出現。這時候你需要比對:
| 檢查項目 | 開發環境 | Staging | Production |
|---|---|---|---|
| Node.js 版本 | 18.17.0 | 18.17.0 | 16.14.0 |
| 資料庫 | SQLite | PostgreSQL | PostgreSQL |
| 環境變數 | .env.local | .env.staging | .env.prod |
| 資料量 | 100 筆 | 1,000 筆 | 1,000,000 筆 |
| SSL | 無 | 有 | 有 |
| CDN | 無 | 無 | 有 |
「在我電腦上可以跑啊」的正確處理方式
當你聽到這句話(或你自己想說這句話)的時候,正確的做法是:
- 比對環境:OS、Runtime 版本、瀏覽器版本、螢幕解析度
- 比對資料:你用的測試資料跟出問題的資料一樣嗎?
- 比對操作:你的操作順序跟使用者一模一樣嗎?有沒有快取影響?
- 比對時間:問題是不是只在特定時間出現?(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 reset128 個 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 |
| 缺少 Index | Query 很慢,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 告訴你:
- 錯誤類型:
TypeError— 存取了undefined的屬性 - 哪個屬性:
.map— 你對某個東西呼叫了.map() - 哪個檔案、哪一行:
UserList.jsx:15 - 在哪個元件:
UserList
你甚至不需要 Google,直接去 UserList.jsx 第 15 行,看那個被 .map() 的變數為什麼是 undefined 就好了。可能是 API 還沒回來、可能是回傳結構變了、可能是 optional chaining 忘記加。
「改一個變數測一次」
這是新手最常犯的錯誤:同時改了三個東西,然後問題消失了,但你不知道是哪個改動修好的。
黃金原則:每次只改一個變數,測試,確認效果,再改下一個。
這跟科學實驗的控制變因是一模一樣的概念。你不會在實驗中同時換試劑、溫度、和濃度——你一次只換一個,才能確定因果關係。
「寫下你的假設」
把你的假設寫在紙上(或任何地方),逐一排除:
## 假設清單
- [ ] 可能是 API 回傳格式變了(檢查 Network tab)
- [ ] 可能是快取問題(清 cache 試試)
- [ ] 可能是 race condition(加 loading state 試試)
- [x] 可能是環境變數沒設定(已排除,env 都有)
- [x] 可能是第三方套件更新了(已排除,lock 檔沒變)寫下來有三個好處:
- 防止你在同一個方向鑽牛角尖
- 避免重複驗證已經排除的假設
- 事後回顧的時候知道你試過什麼
「解釋給橡皮鴨聽」— Rubber Duck Debugging
這個方法論出自《The Pragmatic Programmer》。做法很簡單:
- 拿一隻橡皮鴨(或任何東西,甚至是同事)放在桌上
- 一行一行地向它解釋你的程式碼在做什麼
- 當你解釋到某一行的時候說「等等,這裡不對」——恭喜你,找到 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 事半功倍