差異比對法 (Differential Debugging)#

當你同時有一個正常運作的系統和一個出問題的系統時,透過比較兩者的差異來定位問題,是非常有效的方法。這種情境常見於:新增功能後、升級工具或基礎設施後、部署到新平台後。

這種方法之所以有效,是因為電腦在底層是確定性的 (deterministic)——相同的輸入應該產生相同的輸出。深入探查故障系統,你遲早會發現它與正常系統行為不同的地方。

從哪裡開始比較#

Log 檔案#

很多時候,系統失敗的原因就在 log file 裡面——只要你願意去看。例如設定檔中的語法錯誤:

clients.conf: syntax error in line 92

如果 log 不夠詳細,就提高 log 的 verbosity 等級。

Tracing 工具#

當系統沒有足夠的 logging 機制時,使用 tracing tool 來追蹤執行行為:

  • 系統呼叫追蹤:stracetrussProcmon
  • 動態連結庫追蹤:ltraceProcmon
  • 網路封包追蹤:tcpdumpWireshark
  • 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)檢查動態連結庫
  • 使用 nmdumpbin /exports /importsjavap 檢查 symbols
  • 必要時甚至比較 compiler 產生的 assembly code

縮小範圍的技巧#

精簡測試案例#

投資時間找到能展現 bug 的最簡測試案例。系統地移除元素,直到找到最精簡的設定仍能觸發問題。較小的測試案例意味著更短的 log、trace 和處理時間。

如果差異在原始碼中,可以對兩個版本之間的所有變更進行 binary search。例如正常版本是 v100、故障版本是 v132,先測試 v116,再根據結果測試 v108 或 v124,依此類推。

Git 提供了 git bisect 指令來自動化這個搜尋過程。這也是為什麼你應該每次只 commit 一個獨立變更的原因之一。

比較 Log 檔案#

使用 diff 比較兩個系統的 log 檔案。常用的過濾技巧:

  • cutawk 移除 timestamp 和 process ID 等變動欄位
  • grep 只選取感興趣的事件
  • grep -v 排除雜訊
  • sort + comm 找出只存在於其中一個 log 的項目

重點回顧#

  • 比較已知正常系統和故障系統的行為來找出失敗原因
  • 考量所有可能影響系統行為的因素:程式碼、輸入、呼叫參數、環境變數、服務和動態連結庫