懵逼了!一個歷時五天的Bug......
一個程序員在沒有成長為架構師之前,幾乎都要跟 Bug 為伴,程序員有很多時間都是花在了查找各種 Bug 上。
我印象深刻的一個 Bug,是一個服務器網絡框架無鎖隊列的 Bug 。那個 Bug 連續(xù)查找了五天的時間,才***定位出來。
當時我們的分布式存儲系統出現了性能瓶頸,定位后發(fā)現瓶頸是在服務器網絡框架上,所以我們決定為此替換一個***研發(fā)的網絡框架。這個新的網絡框架為了追求高的性能,采用了無鎖隊列的設計。
***天編碼測試完成后,在測試環(huán)境跑,完全正常,特地搞了一堆 Log 來重放請求,程序跑得特別歡快。
解決了當時的性能瓶頸,感覺特別的開心,但好景不長,服務部署到現網環(huán)境,跑不到一個小時,就 Core Dump 了。
嘗試上線了幾次,每次都是跑半個多小時,就 Core Dump 。
當時的***反應是新網絡框架的問題,這是很直覺的反應,但很快就產生了懷疑,因為這個框架也有其它模塊在用,也沒產生問題,當時覺得詭異了。
仔細查看了代碼的修改記錄,特定檢查了版本管理系統的 Log,做了代碼 Diff,確定確實只有這部分的修改。
憑著經驗,我說服自己,這個時候應該堅信最明確的邏輯,不要走到其他歪路上去。
第二天,我把這個框架單獨拎了出來,特地寫了一個測試的模塊,并用測試代碼生成了一堆的請求數據,發(fā)送給測試模塊。
它瘋狂的運轉起來,跑了一個多小時,跑得很歡快,一切正常,啥 Bug 都沒有。
開大了并發(fā),繼續(xù)壓,依然沒有問題。懵逼了!不知咋回事。再次小心得灰度系統上線,跑不到一個小時還是 Core Dump 了,這個時候,我開始懷疑人生了,這個是咋回事了,都想砸鍵盤了都。
然后我冷靜了下來,經驗告訴我,這時應該按照正常流程完整地跑一遍測試模塊。
于是我把那個測試模塊打包成了現網模塊,切走了一臺現網機器的流量,把測試模塊給上線到了一臺現網機器。
之后用工具往現網機器發(fā)送數據,不到一個小時,Core Dump 了。終于復現了這個 Corde Dump, 那一刻猶如哥倫布發(fā)現了新大陸,簡直欣喜若狂啊。這個時候已經是第三天了。
我復現了 Bug,但依然沒辦法定位出具體的原因。Core Dump 出來的棧是全亂的,沒有任何價值,接下來,就開始用 Log 跟蹤法了。
我依據數據的流轉過程,在每個關鍵點,都打上 Log,Log 包含了所在的函數,行數,程序邏輯的編號,全部的關鍵數據等等。
我仔細地設計了這個 Log,爭取打得不多不少,太多容易看暈頭,而且太多無效的信息,會掩蓋了真正的問題;太少,信息不足,又不足以判斷,所以這種情況下打 Log 也是個藝術活。
通過精心設計的 Log,終于發(fā)現數據在一個特定的環(huán)節(jié)混亂后,程序就一定會 Core Dump。
分析 Log , 發(fā)現數據包在***時刻是完整的,但包似乎出現了亂序和重復。這個時候,才開始意識到可能是無鎖隊列的問題,因為只有隊列出問題,包的進出順序才會亂掉。
然后又花了半天的時間,專門為無鎖隊列寫了測試用例,用數據瘋狂地懟。在測試環(huán)境,依然一切安好,但上線到正式環(huán)境,壓測半小時后,終于掛了。終于看到了勝利的曙光!這個時候已經是第四天了!
到現在已經很明確是無鎖隊列的問題了。但這個數據結構的代碼不到 200 行。我拉了兩個同事一起 Review,都沒發(fā)現問題。但就是 Core Dump 了。
奇葩了,又陷入了人生懷疑,開始懷疑內存,懷疑 CPU,結果換了機器,還是一樣。
后來,仔細地對比了現網環(huán)境和測試環(huán)境的區(qū)別,機器類型,操作系統版本都一樣。然后編譯器?咦!編譯器?上去看了一下,結果發(fā)現編譯器的版本不一樣!
這段時間我所使用的現網編譯環(huán)境升級了新的 GCC 版本,但測試編譯環(huán)境,還是舊的版本的。(這個也比較坑)
當時的直覺是肯定跟編譯器相關,但代碼都一樣,難道是編譯器 Bug?不可能吧 ?!
后來想,不如將它們轉換成匯編看看吧。于是用兩個版本的編譯器將 C 的代碼各自轉換成了匯編。然后 Diff 匯編代碼,哇!發(fā)現真的有一行是不同的!
后來自己分析對比,發(fā)現是因為我們開啟了 GCC ***級別的代碼性能優(yōu)化,不同版本的 GCC 在一些沒有特定依賴的語句上的優(yōu)化是不同的。
說人話,就是有一段代碼,如果加了鎖,兩個版本的編譯器下,都會產生一樣的匯編,如果沒有加鎖,代碼有一行的順序被調整了,當然,從編譯器優(yōu)化的角度講是對的,是我們使用姿勢不對。
但無論怎么樣,終于找出了這個問題。蒼天啊! 找了五天呀!***當然是開開心心地上線了。
查這個 Bug 確實花費了很多的時間,不過也是沒辦法,你不解決 Bug,就不能上線,但中間也收獲不少,特別是對編譯器優(yōu)化有了很深的印象,也算是為自己的查 Bug 能力,又貢獻了一波經驗吧。
對于 Bug , 我分享下自己的一些認識和建議。
面對 Bug 的態(tài)度
只要你持續(xù)地寫代碼,就一定會持續(xù)的產生 Bug,所以***個事情是要擺正對 Bug 的心態(tài)。我遇到過兩個極端。
***個極端
遇到過一個 Leader,對系統質量相當重視,對我們寫的代碼要求很高,每次設計并寫完一個新的系統,他喜歡跟你算這次的系統上線,產生了多少次故障,這半年時間產生了多少個 Bug,每個 Bug 的影響范圍如何。
我們一堆人被搞得特別累,戰(zhàn)戰(zhàn)兢兢,到后面,大家都比較排斥去做優(yōu)化,去重構代碼,只求無過,不求有功了。
所以,我覺得這種方式不好,對 Bug 帶著一種比較包容的態(tài)度去看待,可以減少不少的心理負擔。
第二個極端
后來去了一個新的團隊。新的團隊很重視業(yè)務和工程迭代的速度,所以對代碼質量和 Bug 的容忍度很高。如果是一個新上線的業(yè)務,是默許 Bug 存在的。
這種對質量過于松散的要求,在后面也帶來不好的影響。大家對 Bug 太免疫了,以至于出現 Bug 和故障的時候,大家都不夠緊張。
系統質量有一段時間出現比較大的問題,還因此被部門經理特訓了一番,后面通過各種措施,才慢慢提高了整體的系統質量。
上面兩種極端都不可取,應該很重視 Bug,盡量避免 Bug,但也不應該唯 Bug 多少論業(yè)績。
具體到 Bug 的查找上,我說說我的一些經驗。
Bug 的復現
我把 Bug 分為可重現的 Bug 和不可復現的 Bug 。
對于可重現的 Bug , 查找起來比較容易,比如可以用”二分查找“的方式,從模塊層面開始定位起,每次折半,每次折半地縮小范圍,一直到代碼層面。
在代碼層面,遵循一些常用的原則,比如:
- 看到內存拷貝,直覺上要想到內存越界。
- 看到數組,就要考慮是否索引越界。
- 看到指針,就要考慮是否正確解構。
- 看到多線程,就要考慮是否線程安全。
- 對于不可重現的 Bug,***步就是要把它重現出來。
有時候特別的難,特別是并發(fā)形態(tài)下產生的 Bug,出現的時機和觸發(fā)條件都不清楚。對于這種 Bug,只能通過各種嘗試去復現它。
比如將多并發(fā)調整為單并發(fā)的方式,看能否復現,如果可以復現,就可以轉化為可復現的 Bug,用”二分查找“的方式去排查。
如果不能復現,那極大概率是并發(fā)問題。這個時候***先停止排查,仔細分析程序在并發(fā)狀態(tài)下可能出問題的點。
大部分并發(fā)問題的根源,是互斥數據沒有被正確讀寫,或者一些共享狀態(tài)被錯誤修改。
靜態(tài)代碼檢查
利用 Coverity 等代碼檢查工具進行代碼的靜態(tài)檢查可以發(fā)現很多潛在的問題,而且修復的成本很低。團隊后來引入了這個檢查工具,確實帶來了不錯的效果。類似變量未初始化,疑似的內存越界等都有可能被檢查出來。
編譯器的 Warning。有些同學一開始的時候對 Warning 不重視。我們團隊早期也遇到過這個情況。
那時候產品迭代的速度很快,所以大家寫完代碼,能夠編譯通過,就進行各種測試,然后準備上線了。
一開始的一兩個 Warning,不太理會。后面發(fā)現越積越多,到***終于成為一個不得不解決的問題。
部門還為此特地立項,來消除 Warning。先是在內部多次強調了這個理念,然后從基礎庫,基礎模塊開始實施,基礎代碼部分統一 Fix Warning,然后開啟編譯器把 Warning 當 Error 的開關。
完成之后,再逐步地推業(yè)務模塊進行修改。反正折騰了好一段時間。
工欲善其事必先利其器
代碼 Bug 出現的時候,善用一些排查工具可以極大得提升效率。比如對于 C/C++ 的 GDB 調試,內存泄漏時候 Valgrind 的檢測,Linux 下面用 Perf Top 來析 CPU 的消耗等。
一開始的時候,我對這些工具不重視,老是覺得真正使用的時候,去查文檔就行。
后面才發(fā)現,用工具查著問題時候,遇到不會用的命令或功能,再去查文檔,是個痛苦的事情,來回切換的開銷也使得效率低下。
后面就對這些輔助工具的使用重視了起來,專門花時間去學習和練習使用,反而提升了不少的效率。
打 Log 的藝術
很多時候,出現一個 Bug,未能定位出來,需要打上更多額外的 Log 來輔助排查。一開始的時候,是想到一點,打一個 Log,后面發(fā)現這么做沒有章法,邏輯不清晰, 排查效率低下。
后來學會了,遇到 Bug 后,先在腦子里面分析一番,然后花一兩個小時詳細地設計 Log 的格式和打 Log 的位置。發(fā)現這種方式對查問題的效率提升很大。
所以遇到 Bug 的時候不要急躁,先靜下來心來分析,在腦海里盡力重現出完整的運行邏輯,然后仔細地進行 Log 設計,包括 Log 包含的字段,打 Log 的點等。這樣能極大的提升排查問題的效率。
結語
Bug 是程序員最不愿意面對,但又經常出現的一個 “詭異生物“。對待 Bug 要有正確的心態(tài),***不要跟業(yè)績強綁定,也不能太過于疏忽。
Bug 的排查是個很復雜的事情,每個人都有自己的方式和做法,如果你有好的做法和建議,也歡迎在留言區(qū)分享給大家!