漫談系統化程式除錯思維

我花兩週抓蟲同事只花兩天

最近為了處理一個 vm crach issue,前後花了兩週,最後還是靠資深的同事才在茫茫大海裡把蟲抓出來。雖然硬要找藉口的話,可以說中間同時要處理其他專案沒法太專心,但這次也的確有種「即使自己不用做其他專案,這個 issue 的確花了超過自己預期太多的時間」,所以決定痛定思痛梳理一下自己這次在鬼打牆什麼東西,順便聊聊兼推廣一本我一直以來很喜歡的書 "Effective Debugging: 66 Specific Ways to Debug Software and Systems" by Diomidis Spinellis。

除錯的兩種策略

除錯的對象百百種,方式、原則也可以說是百百種。其中一個粗略的分法可以分成「假說與實證」與「大量觀察」。

策略一:假說與實證

這種思維方式,是基於自己的知識與手上握有的資訊,明確假定臭蟲出現的原因,不斷蒐集證實原因的資訊來驗證假說。蒐集資訊的方式最基本也最重要的作法,大概就是要有實驗組與控制組,不斷透過調整實驗組來驗證假說的預測能力;基本上就是實驗室實驗基礎方法。

策略二:大量觀察

嚴格來說這種思維其實也是基於假說與實證下去做實驗與觀察,不過是基於較沒頭緒或是對於自己的假說不是這麼有信心的時候,刻意編織更大的「網」來捕捉有用的線索。常見於縝密與龐大的實驗計畫,或是開啟 verbose debugging mode ,先蒐集大量資訊再說。

兩種策略各有優劣

兩種策略各有優劣,算是互相補足,時間、精力、資源充足時通常會雙管齊下;實務上則是常常側重其中一種,但也不會完全捨棄另外一種策略。個人經驗中,執行對應的策略時有一些關鍵步驟,算是個人的小撇步,一起摘要在下面比較表中。

策略 精確具體的假說 大量觀察
泛用性 較低,往往需要領域專業直覺 較高,對成因不一定有太多想法時也可以用
效率 較高,假說對的時候可以精準快速找出臭蟲 較低,通常較耗費時間與資源
溝通成本 較低,講給別人聽的時候相對容易理解 較高,他人需要花更多精力了解 context
風險 假說方向錯誤時幾乎不會有任何進展 有可能觀察後還是看不出頭緒
執行關鍵 具體寫下自己對臭蟲原因的假說 具體寫下自己擬定的實驗、測試計畫

在「假說法」中,具體寫下自己對臭蟲原因的假說,其實本質上跟「黃色小鴨法」(Rubber duck debugging)是類似的概念,不過除了撰寫的過程中可能可以激發靈感以外,具體寫下原因,也有助於自己在驗證的過程中飄走、注意力不夠集中導致亂無章法。幾個原則可以檢視自己寫下的描述是否夠「具體」:

在「大量觀察法」中,決定實驗方向時停在「功能」與 function test 的層級即可;如果有辦法問到更小的問題,可以考慮回頭採用「假說法」來節省時間與資源。至於「具體」寫下實驗、測試計畫時則是可以依據下面幾個原則來判斷是不是夠「具體」:

和 Effective Debugging 一書互相參照

我在 debugging 比較沒進展或是頭緒的時候,非常喜歡回頭參照 Effective Debugging: 66 Specific Ways to Debug Software and Systems by Diomidis Spinellis 這本書。這本書整理了 66 個程式除錯的策略,其中第一章 High-Level Strategies 也有一些和上述策略可以互相參照的策略。例如 item 3 "Confirm That Preconditions and Postconditions Are Satisfied"、item 4 "Drill Up from the Problem to the Bug or Down from Program's Start to the Bug" 與 item 5 "Find the Difference between a Known Good System and a Failing One"。題外話是這本書真的很棒,除了 high level 策略以外,也還有各種面向下擬定的策略,例如 "minimize the turnaround time from your changes to their result"。非常適合自己在程式除錯撞牆的時候,一條一條檢視自己是不是真的窮盡所有方式來分析問題了。

有機會我也想好好聊聊這本書與整理自己各種策略(嗯我知道自己在立旗)。

檢討時間到惹

扯了很多終於要回到這裡的初衷:檢討這次找蟲鬼打牆的原因。根據上面的列出的原則,我認為我沒有認真做好「我的實驗測試計畫有沒有一個已知可以運作的很好的 golden sample 或是 benchmark 可以參考」這個原則。

選了一個不夠接近的 benchmark

是說當初收到的 bug (叫它問題 bug A 好了)是一個 vm networking 類型的 bug。 networking issue 可說是公認最難處理的 bug 類型之一,理由是重現 bug 的環境常常無法 100% reproduce,導致 bug 很難在另外一方(例如用戶之於 RD)被穩定地可靠地 reproduce、所以也很難 claim 自己真的對症下藥了。

我收到 bug report 的時候,因為對方宣稱能夠在某個版本的 kernel 出現這個問題,但其實他回報的內容是用某個 rc kernel 做的,我基於某些原因直接採用他的說法、使用某版本的 kernel 而沒有自己重編某 rc kernel;除此之外,我好死不死剛好踩到另外一個「長得很像但其實不是同一個 bug」(叫它問題 B)的 bug,所以一開始我以為我的 networking 環境的確重現了對方宣稱的問題,但其實我是重現了問題 B,整個就是雞同鴨講。

當初如果稍微不嫌麻煩的話去重編某 rc kernel 的話我就不用花兩週只要花兩三天啊啊啊啊。

過大的溝通成本

另外當初因為有點雞同鴨講,所以我有採用了「大量觀察法」來試著重現問題,這樣等同於在雞同鴨講的情況下另外再墊高了溝通成本,導致我延遲很多時間才發現「喔原來你其實只用 rc kernel 就回報 bug 了啊啊啊啊啊啊啊」。

小心大腦多工模式

也因為我一開始採用了「大量觀察法」,但又想要縮短得到結論的時間,所以忍不住 multi tasking,結果常常忘了自己「做到哪裡」與「忘了剛才有沒有沒檢查到的項目」,反而浪費更多自己的時間與精力,事後想到「還要再做一次一樣的事情喔?」內心有很大的排斥,而排斥感本身又很吃意志力,最後會打擊到自己的士氣 XD

至於 multi-tasking 的技巧也很值得開一篇來特別聊聊(又在立旗),這篇就先不多囉唆太多。大致上 multi-tasksing 不是不行,而是一定要小心注意力用盡導致「一下忘了自己做到哪裡」的窘境,例如「使用清單」。但最好其實盡量不要 multi-tasking 啦,人類大腦好像是不太適合做多工。