差異比對法 (Differential Debugging)#
當你同時有一個正常運作的系統和一個出問題的系統時,透過比較兩者的差異來定位問題,是非常有效的方法。這種情境常見於:新增功能後、升級工具或基礎設施後、部署到新平台後。
這種方法之所以有效,是因為電腦在底層是確定性的 (deterministic)——相同的輸入應該產生相同的輸出。深入探查故障系統,你遲早會發現它與正常系統行為不同的地方。
從哪裡開始比較#
Log 檔案#
很多時候,系統失敗的原因就在 log file 裡面——只要你願意去看。例如設定檔中的語法錯誤:
clients.conf: syntax error in line 92如果 log 不夠詳細,就提高 log 的 verbosity 等級。
Tracing 工具#
當系統沒有足夠的 logging 機制時,使用 tracing tool 來追蹤執行行為:
- 系統呼叫追蹤:
strace、truss、Procmon - 動態連結庫追蹤:
ltrace、Procmon - 網路封包追蹤:
tcpdump、Wireshark - SQL 追蹤
- Shell script 追蹤:傳入
-x選項
執行環境#
讓兩個系統的環境盡可能相似,以便比較。從最明顯的東西開始檢查:
- 程式的輸入和 command-line 參數(實際比較,不要假設)
- 環境變數 (environment variables)——即使非特權使用者也能透過環境變數造成混亂
- 作業系統版本
- Compiler、development framework、third-party libraries、browser、application server、database 等
記得 verify, don’t assume。實際比較兩個系統的輸入檔案,或比較它們的 MD5 checksum。
原始碼與二進位#
- 比較原始碼,但也要準備更深入檢查
- 使用
ldd(Unix)、dumpbin /dependents(Visual Studio)檢查動態連結庫 - 使用
nm、dumpbin /exports /imports、javap檢查 symbols - 必要時甚至比較 compiler 產生的 assembly code
縮小範圍的技巧#
精簡測試案例#
投資時間找到能展現 bug 的最簡測試案例。系統地移除元素,直到找到最精簡的設定仍能觸發問題。較小的測試案例意味著更短的 log、trace 和處理時間。
Binary Search#
如果差異在原始碼中,可以對兩個版本之間的所有變更進行 binary search。例如正常版本是 v100、故障版本是 v132,先測試 v116,再根據結果測試 v108 或 v124,依此類推。
Git 提供了
git bisect指令來自動化這個搜尋過程。這也是為什麼你應該每次只 commit 一個獨立變更的原因之一。
比較 Log 檔案#
使用 diff 比較兩個系統的 log 檔案。常用的過濾技巧:
- 用
cut或awk移除 timestamp 和 process ID 等變動欄位 - 用
grep只選取感興趣的事件 - 用
grep -v排除雜訊 - 用
sort+comm找出只存在於其中一個 log 的項目
重點回顧#
- 比較已知正常系統和故障系統的行為來找出失敗原因
- 考量所有可能影響系統行為的因素:程式碼、輸入、呼叫參數、環境變數、服務和動態連結庫