認識到bug的重要性,有的bug會給公司帶來巨大的經濟損失,有的bug會給用戶帶來人身安全。如果航天飛機的操作系統出了問題怎么辦?如果銀行交易系統出了問題怎么辦?
# 確認問題的存在
不能重現的bug無法修復。如果能重現,必然某個地方出錯了。
能重現,看看版本庫和團隊,是否已經被修復過,是否有解決方案。
沒解決過,大致分析下解決要多少時間成本。如果你有很多bug修復,先從簡單的開始,一個一個解決。
# 分析問題可能是什么原因導致的
首先,問題往往有其表象。而真正導致錯誤的原因才是我們想知道的。有的時候bug很簡單,錯的地方就是其表象那樣。有的時候,你看到的錯誤不是問題真正的錯誤。
在我們,解決問題前,我們必須知道我們要找的是什么問題。我們要弄清“發生了什么?應該發生什么?”。我們寫的每行代碼都是一個指令,告訴計算機軟件運行(web應用是訪問url時)程序應該給予什么處理和輸出的結果。所以我們應該知道當可能有問題的現象出現,系統發生了什么,以及正常情況下應該發生什么。
其次,我們根據錯誤的發生,來排查簡單的情況。比如《東半球最先進的debug技巧》中提到的:
- 你改錯了文件
- 你改對了文件,但卻是在別人的機器上
- 你改對了文件,但忘了保存
- 你改對了文件,但忘了重新編譯
- 你認為你把那個東西開啟了,但實際上你把它關閉了
- 你認為你把那個東西關閉了,但實際上你把它開啟了
- 會議中,你應該用心聽。
- 你運行了錯誤的版本
- 你運行了正確的版本,但卻是在別人的機器上
- 你改正了問題,但忘了提交
- 你改正了問題,也提交了,但忘了push到版本庫中
- 你改正了問題,也提交了,也push了。然而,很多用戶的工作都依賴于之前有問題的版本,于是你必須回滾。
本文屬翻譯作品,英文原文標題是:Cutting edge debugging。
文章轉載自:開源中國社區 [http://www.oschina.net]
本文標題:東半球最先進的 debug 技巧
本文地址:http://www.oschina.net/news/54244/cutting-edge-debugging
盡管不同語言的報錯可能不太一樣,但是他們基本上分為兩種結構:
1. 系統級報錯
2. 應用自定義級報錯
## 系統級報錯
一般是 `可能錯誤的原因 + 錯誤文件路徑 + 錯誤行號 [+錯誤級別]`
像下面的是最簡單的PHP報錯:

## 應用自定義級報錯
這就頭疼了,應為作為應用來說,是不希望用戶看到明顯的程序錯誤的,那樣不友好,且不安全(因為源碼路徑暴露了)。
所以應用會在代碼中將默認不處理的情況手動判斷了報出人性化的錯誤,比如上傳文件時超過大小限制了 ,會提示`“您上傳的文件過大,超過2M”`之類的。并且肯定是人話,本國語言的。
像TP默認非調試模式時的任意出錯都會到404頁面 提示系統出錯了。
這樣的時候我們只能反向去找錯誤了。通過錯誤提示去源碼里搜索報錯的這個可能位置。
當然TP肯定會記錄錯誤的,PHP應用出錯了都會記錄錯誤日志,方便我們去排查,像上面的情況,我們就要看Runtime 目錄下的logs目錄下的日志文件了。
后面一章節,我已經把PHP常見錯誤總結了下,下次看到報錯信息,不要不認識了。
當然,以前TP錯誤不是很友好,模板錯誤直接白頁了。讓我們去猜。現在好多了。
框架有時候也不能做的盡善盡美,所以我們會依賴一些組件,來提高我們的排錯流程。
# 在可能問題的地方斷點、輸出數據進行觀察
其實,這個標題的意思是,重現問題的方法。我們解決問題時,一定要先重現問題,再去解決問題。
> 不能重現的問題是無法修復的。
比方說愛因斯坦只有一個, 你無法在沒有愛因斯坦的時候,去推測他對于現在的問題的觀點。
有的時候我們解決問題的優先級也是 先一般,再特殊。
80%的問題都解決了,剩下的都是小打小鬧了哈。
而且,重現問題才能幫助我們取得解決問題的進展。
如果問題,不能100%的按照某些指定流程出現,你怎么證明這問題真是存在。
有的時候特例的問題,往往時操作人員的不當操作。
如果你連問題怎么重現實現不了,如何證明你的修復行為,已經解決問題了。
一旦我們能重現問題了,我們就能大概知道正確處理這塊業務的代碼在哪,以及可以提供推測提現這個問題的影響因素了。
> 有因必有果。
從程序的角度來說,錯誤的輸出必定經過了錯誤的輸入。
輸入也有可能來自上層,傳遞過來。
當我們確定一個大概的報錯地點時,我們要進行斷點排查。
系統級別的錯誤信息有時,我們只需定位行就行了。
應用級別的錯誤,我們查找到報錯提示(有可能這個提示是動態的),
我們先利用強大編輯器工具的文件搜索,進行直接搜索。
搜到了,如果只有一個位置,那就是他了。多個需要我們去看代碼的上下文,確定當前出錯的提示會不會在這個位置出現。懶一點的辦法是加一個報錯。
比方有兩個錯誤位置,我們可以在錯誤位置上加上一行輸出:“錯誤1”,另外的出錯提示代碼前加上“錯誤2”
這樣,我們測試環境里運行時,看到底是錯誤1 還是錯誤2 就可以精確定位了。
當然,有時候系統會過濾一切輸出,只輸出應用定義的錯誤,這時候我們可以借助工具,如寫文件,用**Socketlog**這樣的工具,將錯誤信息輸出到瀏覽器里。
如果錯誤提示搜索不到,我們可以將 錯誤提示分詞,先搜關鍵的錯誤,比如上傳,和大小分別搜。 搜上傳,我們找到上傳類,搜大小我們找到判斷大小的地方。然后要做的就是改提示和配置了。
有的時候,我們看似無解的問難,實際上都能解決。以前實習的時候,有個html頁面亂了,我找半天沒找到什么原因呢,最后在同學的排除下,將一個頁面一塊一塊的刪除,定位了,錯誤的樣式發生位置,解決了那個問題。
有的時候查找問題很吃力,幾個小時沒進展,我們可能會沮喪,但是不要放棄,
任何問題都是能找到并解決的。只是難易程度罷了。
# 解決問題
## 一次只解決一個問題。
由于軟件運行是個復雜的過程。如果我們看到一些簡單的問題,就撲上去解決掉。這樣有可能會有問題。解決多個問題的前提是,確保多個問題是獨立的。不會相互影響的。
在精確定位的基礎上,我們回顧那個代碼片段的作用,應該發生什么,正確結果,以及現在的錯誤結果,怎么改讓其返回正確結果。怎么改讓用戶接受錯誤提示。
確保你現在的處理符合設計,不確定時問一下產品經理。
## 將問題最小化
有時候,我們找到一個問題代碼范圍,想要修復了,但是運行到這個代碼塊,要經過好多步驟。比方說,新增用戶,修改資料,某些行為等。
Wait,既然我們已經確定位置了,我們何不把錯誤代碼拿出來,只取一次的過程數據作為輸入, 寫個小測試代碼文件,在這基礎上修復問題呢?只要我的測試文件跑沒問題,相應修改丟入系統中運行也沒問題,其他人員測試頁沒問題,這個問題基本上就解決了。
# 測試問題是否解決
修改后,再次運行程序,到那一步。看看本地的輸出是否正確。
再交給測試人員看各種場景是否到那一步結果都正確。
確保大家的輸出結果都一致,問題就解決了。
# 反思自己為什么犯這種錯誤
解決問題了,你認為就該走開,該干嘛就干嘛去了嗎?
不行,你得停下來思考,我為什么會犯這個錯,這個錯是屬于什么類型的。
最好有個記事本,將自己犯的錯記錄下來。
還要深層次的思考,這個錯是不是還有其他問題。
比方說我們寫代碼,開始寫插入數據,有一個表:要求標題title不能重復。我寫了沒做判斷。
測試人員測試出來了,我們解決了。查詢判斷了。然后我們要想的是,標題唯一是一種驗證,我們是不是缺少數據插入前的全部驗證判斷,然后向產品詢問,其他地方插入數據的條件,最后自己再補上其他模型的自動驗證,這才是將一類問題解決掉了。
# 錯誤是什么煉成的
對于當前系統來說, 錯誤的產生由三個地方引入:
## 1. 上層系統引入的非法參數。 對于非法參數引入的錯誤, 可以通過參數校驗和前置條件校驗來截獲錯誤;
##2. 與下層系統交互產生的錯誤。 與下層交互產生的錯誤, 有兩種:
a. 下層系統處理成功了,但是通信出錯了, 這樣會導致子系統之間的數據不一致;
對于這種情況, 可以采用超時補償機制,預先將任務記錄下來,通過定時任務在后續將數據訂正過來。更好的設計方案?
b. 通信成功了,但是下層處理出錯了。
對于這種情況, 需要與下層開發人員溝通, 協調子系統之間的交互;
需要根據下層返回的錯誤碼和錯誤描述做適當的處理或給予合理的提示信息。
無論哪一種情況, 都要假設下層系統可靠性一般, 做好出錯的設計考慮。
## 3. 本層系統處理出錯。
本層系統產生錯誤的原因:
原因一: 疏忽導致。疏忽是指程序員能力完全可避免此類錯誤但實際上沒做到。比如將 && 敲成了 & , == 敲成了 = ; 邊界錯誤, 復合邏輯判斷錯誤等。 疏忽要么是程序員注意力不夠集中, 比如處于疲倦狀態、加班通宵、邊開會邊寫程序; 要么是急著實現功能,沒有顧及程序的健壯性等。
改進措施: 使用代碼靜態分析工具,通過單元測試行覆蓋可有效避免此類問題。
原因二: 錯誤與異常處理不夠周全導致的。比如輸入問題。 計算兩個數相加, 不僅要考慮計算溢出問題, 還要考慮輸入非法的情形。對于前者,可能通過了解、犯錯或經驗就可以避免, 而對于后者,則必須加以限定,以使之處于我們的智商能夠控制的范圍內,比如使用正則表達式過濾掉不合法的輸入。對于正則表達式必須進行測試。對于不合法輸入, 要給出盡可能詳細、易懂、友好的提示信息、原因及建議方案。
改進措施: 盡可能周全地考慮各種錯誤情形和異常處理。在實現主流程之后,增加一個步驟:仔細推敲可能的各種錯誤和異常,返回合理錯誤碼和錯誤描述。每個接口或模塊都有效處理好自己的錯誤和異常,可有效避免因場景交互復雜導致的bug. 譬如,一個業務用例由場景A.B.C交互完成。實際執行A.B成功了,C失敗了,這時B需要根據C返回合理的代碼和消息進行回滾并返回給A合理的代碼和消息,A根據B的返回進行回滾,并返回給客戶端合理的代碼和消息。這是一種分段回滾的機制,要求每個場景都必須考慮異常情況下的回滾。
原因三: 邏輯耦合緊密導致。由于業務邏輯耦合緊密, 隨著軟件產品一步步發展, 各種邏輯關系錯綜復雜, 難以看到全局狀況, 導致局部修改影響波及到全局范圍,造成不可預知的問題。
改進措施: 編寫短函數和短方法, 每個函數或方法最好不超過 50 行。 編寫無狀態函數和方法, 只讀全局狀態, 相同的前提條件總是會輸出相同的結果, 不會依賴外部狀態而變更自己的行為; 定義合理的結構、 接口和邏輯段, 使接口之間的交互盡可能正交、低耦合; 對于服務層, 盡可能提供簡單、正交的接口; 持續重構, 保持應用模塊化和松耦合, 理清邏輯依賴關系。對于有大量業務接口相互影響的情況, 必須整理各個業務接口的邏輯流程及相互依賴關系, 從整體上進行優化; 對于有大量狀態的實體, 也需要梳理相關的業務接口, 整理狀態之間的轉換關系。
原因四: 算法不正確導致。
改進措施: 首先將算法從應用中分離出來。 若算法有多種實現, 可以通過交叉校驗的單元測試找出來, 比如排序操作; 如果算法具有可逆性質, 可以通過可逆校驗的單元測試找出來, 比如加密解密操作。
原因五: 相同類型的參數,傳入順序錯誤導致。比如,modifyFlow(int rx, int tx), 實際調用為 modifyFlow(tx,rx)
改進措施: 盡可能使類型具體化, 該用浮點數就用浮點數, 該用字符串就用字符串, 該用具體對象類型就用具體對象類型; 相同類型的參數盡可能錯開; 如果上述都無法滿足, 就必須通過接口測試來驗證, 接口參數值務必是不同的。
原因六: 空指針異常。空指針異常通常是對象沒有正確初始化, 或者使用對象之前沒有對對象是否非空做檢測。
改進措施: 對于配置對象, 檢測其是否成功初始化; 對于普通對象, 獲取到實體對象使用之前, 檢測是否非空。
原因七: 網絡通信錯誤。網絡通信錯誤通常是因為網絡延遲、阻塞或不通導致的錯誤。網絡通信錯誤通常是小概率事件, 但小概率事件很可能會導致大面積的故障、 難以復現的BUG。
改進措施: 在前一個子系統的結束點和后一個子系統的入口點分別打 INFO 日志。 通過兩者的時間差提供一點線索。
原因八: 事務與并發錯誤。事務與并發結合在一起, 很容易產生非常難以定位的錯誤。
改進措施:對于程序中的并發操作, 涉及到共享變量及重要狀態修改的, 要加 INFO 日志。更有效的做法???
原因九: 配置錯誤。
改進措施: 在啟動應用或啟動相應配置時, 檢測所有的配置項, 打印相應的INFO日志, 確保所有配置都加載成功。
原因十: 業務不熟悉導致的錯誤。在中大型系統, 部分業務邏輯和業務交互都比較復雜, 整個的業務邏輯可能存在于多個開發同學的大腦里, 每個人的認識都不是完整的。這很容易導致業務編碼錯誤。
改進措施: 通過多人討論和溝通, 設計正確的業務用例, 根據業務用例來編寫和實現業務邏輯; 最終的業務邏輯和業務用例必須完整存檔; 在業務接口中注明該業務的前置條件、處理邏輯、后置校驗和注意事項; 當業務變化時, 需要同步更新業務注釋; 代碼REVIEW。 業務注釋是業務接口的重要文檔, 對業務理解起著重要的緩存作用。
原因十一: 設計問題導致的錯誤。比如同步串行方式會有性能、響應慢的問題, 而并發異步方式可以解決性能、響應慢的問題, 但會帶來安全、正確性的隱患。異步方式會導致編程模型的改變, 新增異步消息推送和接收等新的問題。使用緩存能夠提高性能, 但是又會存在緩存更新的問題。
改進措施: 編寫和仔細評審設計文檔。 設計文檔必須闡述背景、需求、所滿足的業務目標、要達到的業務性能指標、可能的影響、設計總體思路、詳細方案、預見該方案的優缺點及可能的影響; 通過測試和驗收, 確保改設計方案確實滿足業務目標和業務性能指標。
原因十二: 未知細節問題導致的錯誤。比如緩沖區溢出、 SQL 注入攻擊。 從功能上看是沒有問題的, 但是從惡意使用上看, 是存在漏洞的。 再比如, 選擇 jackson 庫做 JSON 字符串解析, 默認情況下, 當對象新增字段時會導致解析出錯。必須在對象上加@JsonIgnoreProperties(ignoreUnknown=true) 注解才能正確應對變化。如果選用其他 JSON 庫就不一定有這個問題。
改進措施: 一方面要通過經驗積累, 另一方面, 考慮安全問題和例外情況, 選擇成熟的經過嚴格測試的庫。
原因十三: 隨時間變化而出現的bug。有些解決方案在過去看來是很不錯的,但在當前或者未來的情景中可能變得笨拙甚至不中用,也是常見的事情。比如像加密解密算法, 在過去可能認為是完善的, 在破解之后就要慎重使用了。
改進措施: 關注變化以及漏洞修復消息,及時修正過時的代碼、庫、行為。
原因十四: 硬件相關的錯誤。比如內存泄露, 存儲空間不足, OutOfMemoryError 等。
改進措施: 增加對應用系統的 CPU / 內存 / 網絡等重要指標的性能監控。
系統出現的常見錯誤:
1. 實體在數據庫中的記錄不存在, 必須指明是哪個實體或實體標識;
2. 實體配置不正確, 必須指明是哪個配置有問題,正確的配置應該是什么;
3. 實體資源不滿足條件, 必須指明當前資源是什么,資源要求是什么;
4. 實體操作前置條件不滿足, 必須指明需要滿足什么前置條件,當前的狀態是什么;
5. 實體操作后置校驗不滿足, 必須指明需要滿足什么后置校驗, 當前的狀態是什么;
6. 性能問題導致超時, 必須指明是什么導致的性能問題,后續如何優化;
7. 多個子系統交互通信出錯導致之間的狀態或數據不一致?
一般難以定位的錯誤會出現在比較底層的地方。 因為底層無法預知具體的業務場景, 給出的錯誤消息都是比較通用的。
這就要求在業務上層提供盡可能豐富的線索。錯誤的產生一定是多個系統或層次交互的過程中在某一層棧上不滿足前置條件導致。在編程時, 在每一層棧中盡可能確保所有必須的前置條件滿足,盡可能避免錯誤的參數傳遞到底層, 盡可能地將錯誤截獲在業務層。
大多數錯誤都是由多種原因組合產生。 但每一種錯誤必定有其原因。 在解決錯誤之后, 要深入分析錯誤是如何發生的, 如何避免這些錯誤再次發生。 努力就能成功, 但是:反思才能進步 !
最后,錯誤的代碼,有時是由錯誤的人錯誤的認知導致的,如果發現一個人經常犯低級錯誤,是否應該先停下他的工作,讓他重新將知識復習一遍。確保他的知識沒問題再回來修復錯誤。
> 不要低估一個低級程序員的破壞力
- 序
- 前言
- 內容簡介
- 目錄
- 基礎知識
- 起步
- 控制器
- 模型
- 模板
- 命名空間
- 進階知識
- 路由
- 配置
- 緩存
- 權限
- 擴展
- 國際化
- 安全
- 單元測試
- 拿來主義
- 調試方法
- 調試的步驟
- 調試工具
- 顯示trace信息
- 開啟調試和關閉調試的區別
- netbeans+xdebug
- Socketlog
- PHP常見錯誤
- 小黃鴨調試法,每個程序員都要知道的
- 應用場景
- 第三方登錄
- 圖片處理
- 博客
- SAE
- REST實踐
- Cli
- ajax分頁
- barcode條形碼
- excel
- 發郵件
- 漢字轉全拼和首字母,支持帶聲調
- 中文分詞
- 瀏覽器useragent解析
- freelog項目實戰
- 需求分析
- 數據庫設計
- 編碼實踐
- 前端實現
- rest接口
- 文章發布
- 文件上傳
- 視頻播放
- 音樂播放
- 圖片幻燈片展示
- 注冊和登錄
- 個人資料更新
- 第三方登錄的使用
- 后臺
- 微信的開發
- 首頁及個人主頁
- 列表
- 歸檔
- 搜索
- 分頁
- 總結經驗
- 自我提升
- 進行小項目的鍛煉
- 對現有輪子的重構和移植
- 寫技術博客
- 制作視頻教程
- 學習PHP的知識和新特性
- 和同行直接溝通、交流
- 學好英語,走向國際
- 如何參與
- 瀏覽官網和極思維還有看云
- 回答ThinkPHP新手的問題
- 嘗試發現ThinkPHP的bug,告訴官方人員或者push request
- 開發能提高效率的ThinkPHP工具
- 嘗試翻譯官方文檔
- 幫新手入門
- 創造基于ThinkPHP的產品,進行連帶推廣
- 展望未來
- OneThink
- ThinkPHP4
- 附錄