本文深入探討了調試的核心哲學,強調通過系統化數據收集而非盲目猜測來解決問題。文章詳細介紹了調試的四個關鍵步驟,包括理解正常系統行為、承認未知、數據收集和根本原因修復,為開發人員提供了實用的調試方法論。
調試的基本哲學
作者:Max Kanat-Alexander
發佈日期:2017年7月17日
有時候人們很難進行調試。主要是那些認為調試系統需要思考而不是觀察的人。
讓我舉個例子。假設你有一個網絡服務器,在5%的情況下會靜默地無法向用户提供頁面。你對這個問題"為什麼?"的反應是什麼?
你會立即嘗試想出答案嗎?你會開始猜測嗎?如果是這樣,你就做錯了。
對這個問題的正確答案是:"我不知道。"
這給我們帶來了成功調試的第一步:開始調試時,要意識到你並不知道答案。
人們很容易認為自己已經知道答案。有時候你猜對了。這種情況並不經常發生,但發生的頻率足以讓人們誤以為猜測答案是調試的好方法。然而,大多數時候,你會花費數小時、數天或數週猜測答案並嘗試不同的修復方法,除了使代碼複雜化外毫無結果。事實上,一些代碼庫中充滿了對"bug"的"解決方案",這些實際上只是猜測——而這些"解決方案"是代碼庫複雜性的重要來源。
實際上,作為旁註,我要告訴你一個有趣的原則。通常,如果你很好地修復了一個bug,你實際上會讓系統的某部分消失、變得更簡單、設計更好等等。我可能會在某個時候更詳細地討論這個問題,但現在就是這樣。很多時候,修復bug的最佳方法實際上是刪除代碼或簡化系統。
但回到調試過程本身,你應該做什麼?猜測是浪費時間,想象問題的原因是浪費時間——基本上,當你第一次遇到問題時,腦海中發生的大部分活動都是浪費時間。你唯一需要做的事情是:
- 記住正常系統的行為是什麼樣的
- 弄清楚你需要查看什麼以獲得更多數據
因為這給我們帶來了調試的最重要原則:調試是通過收集數據直到你理解問題原因來完成的。
你收集數據的方式幾乎總是通過觀察某些東西。對於不提供頁面的網絡服務器,你可能會查看其日誌。或者你可以嘗試重現問題,以便在問題發生時觀察服務器的情況。這就是為什麼人們經常想要一個"重現案例"(一系列允許你重現確切問題的步驟)——這樣他們就可以在bug發生時觀察發生了什麼。
有時候你需要收集的第一個數據是bug實際上是什麼。通常用户提交的bug報告數據不足。例如,假設用户提交了一個bug:"當我加載頁面時,網絡服務器沒有返回任何東西。"這不是足夠的信息。他們試圖加載什麼頁面?"沒有返回任何東西"是什麼意思?只是一個空白頁面?你可能會假設這就是用户的意思,但很多時候你的假設是錯誤的。你的用户在編程或計算機技術方面的經驗越少,他們就越難在沒有你詢問的情況下具體表達發生了什麼。在這些情況下,除非是緊急情況,否則我做的第一件事就是向用户發送具體請求以澄清他們的bug報告,並在他們回覆之前不做任何處理。在他們澄清之前,我完全不會調查。如果我在完全理解問題之前就去嘗試解決問題,我可能會浪費時間調查系統中與問題完全無關的隨機角落。更好的做法是在等待用户回覆時把時間花在更有成效的事情上,然後當我有了完整的bug報告時,再去研究現在已理解的問題的原因。
不過要注意,不要因為用户提交了不完整的bug報告就對他們粗魯或不友好。你對系統瞭解較多而他們對系統瞭解較少,這並不會使你成為一個優越的存在,可以從"比你聰明山"閃閃發光的頂峯上的高城堡中輕蔑地俯視所有用户。相反,以友善或直接的方式提問並獲取信息。提交bug的人很少是故意愚蠢的——他們只是不知道,而幫助他們提供正確信息是你工作的一部分。如果人們經常不提供正確的信息,你甚至可以在bug提交頁面上包含一個小問卷或表格,讓他們填寫正確的信息。關鍵是要幫助他們,這樣他們才能幫助你,你才能輕鬆解決出現的問題。
一旦你澄清了bug,你就需要去查看系統的各個部分。查看系統的哪些部分取決於你對系統的瞭解。通常是日誌、監控、錯誤消息、核心轉儲或系統的其他輸出。如果你沒有這些東西,你可能需要在完全調試系統之前啓動或發佈一個提供信息的新版本系統。雖然僅僅為了修復一個bug而這樣做似乎很麻煩,但實際上,發佈一個提供足夠信息的新版本通常比在沒有信息的情況下在系統中四處尋找和猜測發生了什麼要快。這也是支持快速、頻繁發佈的另一個好論點——這樣你就可以快速發佈一個提供新調試信息的新版本。有時候,你也可以將系統的新版本只發布給遇到問題的用户,作為獲取所需信息的快捷方式。
現在,還記得我上面提到你必須記住正常系統是什麼樣子嗎?這是因為調試還有另一個原則:調試是通過將你擁有的數據與你所知道的正常系統數據應該是什麼樣子進行比較來完成的。
當你看到日誌中的消息時,那是正常消息還是實際上是錯誤?也許日誌説:"警告:所有用户數據都丟失了。"這看起來像是一個錯誤,但實際上你的網絡服務器每次啓動時都會打印這個。你必須知道正常工作的網絡服務器會這樣做。你要尋找正常系統不會顯示的行為或輸出。此外,你必須理解這些消息的含義。也許網絡服務器可選地有一些你沒有使用的用户數據庫,這就是你收到該警告的原因——因為你希望所有"用户數據"都丟失。
最終你會找到正常系統不會做的事情。但是,當你看到這個時,你不應該立即假設你已經找到了問題的原因。例如,也許它記錄了一條消息説:"錯誤:昆蟲正在吃掉所有的cookies。"你可以"修復"這種行為的一種方法是刪除日誌消息。現在行為正常了,對吧?不,錯了——實際的bug仍然在發生。這是一個相當愚蠢的例子,但人們會做不那麼愚蠢的版本,這些版本並不能修復bug。他們沒有深入問題的根本原因,而是用一些變通方法掩蓋了bug,這些變通方法永遠存在於代碼庫中,併為之後在該代碼區域工作的每個人帶來複雜性。甚至説"你會知道你已經找到了真正的原因,因為修復它就能修復bug"也不夠準確。這很接近真相,但更準確的説法是:"當你確信修復它會使問題永遠不會再出現時,你就會知道你找到了一個真正的原因。"這不是一個絕對的陳述——bug的"修復程度"有一個尺度。一個bug可以被更多地修復或更少地修復,通常基於你希望解決方案"深入"到什麼程度,以及你想在上面花費多少時間。通常當你找到了問題的合適原因並可以宣佈bug已修復時,你會知道——這是相當明顯的。但我想警告你不要通過消除症狀但不處理原因來掩蓋bug。
當然,一旦你找到了原因,你就修復它。如果你其他事情都做對了,這實際上是最簡單的步驟。
所以基本上,這給了我們調試的四個主要步驟:
- 熟悉正常系統的行為
- 理解你還不知道問題的原因
- 查看數據直到你知道問題的原因
- 修復原因而不是症狀
這聽起來很簡單,但我看到人們一直違反這個公式。根據我的經驗,大多數程序員在面對bug時,想要坐着思考或討論可能的原因——這兩種都是猜測的形式。與其他可能瞭解系統信息或建議在哪裏尋找有助於調試的數據的人交談是可以的。但是坐着集體猜測可能導致bug的原因並不比你自己坐着做這件事好多少,除非也許你可以與同事聊天,如果你喜歡他們的話這可能不錯。但大多數情況下,你在這種情況下是在浪費一羣人的時間,而不僅僅是浪費你自己的時間。
所以不要浪費人們的時間,也不要在代碼庫中創建不必要的複雜性。這種調試方法是有效的。它在每個代碼庫、每個系統上每次都有效。有時候"數據收集"步驟相當困難,特別是對於無法重現的bug。但在最壞的情況下,你可以通過查看代碼並嘗試查看是否能發現bug,或者繪製系統行為圖並查看是否能發現問題來收集數據。我只建議將其作為最後的手段,但如果你必須這樣做,它仍然比猜測出了什麼問題或假設你已經知道要好。
有時候,僅僅通過查看正確的數據直到你知道,bug就解決了,這幾乎是神奇的。自己試試看。實際上這可能很有趣。