我印象深刻的一個(gè) Bug挤渐,是一個(gè)服務(wù)器網(wǎng)絡(luò)框架無鎖隊(duì)列的 Bug 苹享。那個(gè) Bug 連續(xù)查找了五天的時(shí)間,才最后定位出來浴麻。
當(dāng)時(shí)我們的分布式存儲(chǔ)系統(tǒng)出現(xiàn)了性能瓶頸得问,定位后發(fā)現(xiàn)瓶頸是在服務(wù)器網(wǎng)絡(luò)框架上,所以我們決定為此替換一個(gè)最新研發(fā)的網(wǎng)絡(luò)框架软免。這個(gè)新的網(wǎng)絡(luò)框架為了追求高的性能宫纬,采用了無鎖隊(duì)列的設(shè)計(jì)。
第一天編碼測試完成后膏萧,在測試環(huán)境跑漓骚,完全正常,特地搞了一堆 Log 來重放請求向抢,程序跑得特別歡快认境。
解決了當(dāng)時(shí)的性能瓶頸,感覺特別的開心挟鸠,但好景不長,服務(wù)部署到現(xiàn)網(wǎng)環(huán)境亩冬,跑不到一個(gè)小時(shí)艘希,就 Core Dump 了。
嘗試上線了幾次硅急,每次都是跑半個(gè)多小時(shí)覆享,就 Core Dump 。
當(dāng)時(shí)的第一反應(yīng)是新網(wǎng)絡(luò)框架的問題营袜,這是很直覺的反應(yīng)撒顿,但很快就產(chǎn)生了懷疑,因?yàn)檫@個(gè)框架也有其它模塊在用荚板,也沒產(chǎn)生問題凤壁,當(dāng)時(shí)覺得詭異了。
仔細(xì)查看了代碼的修改記錄跪另,特定檢查了版本管理系統(tǒng)的 Log拧抖,做了代碼 Diff,確定確實(shí)只有這部分的修改免绿。
憑著經(jīng)驗(yàn)唧席,我說服自己,這個(gè)時(shí)候應(yīng)該堅(jiān)信最明確的邏輯,不要走到其他歪路上去淌哟。
第二天迹卢,我把這個(gè)框架單獨(dú)拎了出來,特地寫了一個(gè)測試的模塊徒仓,并用測試代碼生成了一堆的請求數(shù)據(jù)婶希,發(fā)送給測試模塊。
小編是一個(gè)有著6年工作經(jīng)驗(yàn)的工程師蓬衡,關(guān)于C++喻杈,編程,自己有做材料的整合狰晚,一個(gè)完整的C++編程學(xué)習(xí)路線筒饰,學(xué)習(xí)資料和工具,能夠進(jìn)我的群7253壁晒,-91790收取瓷们,免費(fèi)送給大家,希望你也能憑著自己的努力秒咐,成為下一個(gè)優(yōu)秀的程序員
它瘋狂的運(yùn)轉(zhuǎn)起來谬晕,跑了一個(gè)多小時(shí),跑得很歡快携取,一切正常攒钳,啥 Bug 都沒有。
開大了并發(fā)雷滋,繼續(xù)壓不撑,依然沒有問題。懵逼了晤斩!不知咋回事焕檬。再次小心得灰度系統(tǒng)上線,跑不到一個(gè)小時(shí)還是 Core Dump 了澳泵,這個(gè)時(shí)候实愚,我開始懷疑人生了,這個(gè)是咋回事了兔辅,都想砸鍵盤了都腊敲。
然后我冷靜了下來,經(jīng)驗(yàn)告訴我幢妄,這時(shí)應(yīng)該按照正常流程完整地跑一遍測試模塊兔仰。
于是我把那個(gè)測試模塊打包成了現(xiàn)網(wǎng)模塊,切走了一臺(tái)現(xiàn)網(wǎng)機(jī)器的流量蕉鸳,把測試模塊給上線到了一臺(tái)現(xiàn)網(wǎng)機(jī)器乎赴。
之后用工具往現(xiàn)網(wǎng)機(jī)器發(fā)送數(shù)據(jù)忍法,不到一個(gè)小時(shí),Core Dump 了榕吼。終于復(fù)現(xiàn)了這個(gè) Corde Dump, 那一刻猶如哥倫布發(fā)現(xiàn)了新大陸饿序,簡直欣喜若狂啊。這個(gè)時(shí)候已經(jīng)是第三天了羹蚣。
我復(fù)現(xiàn)了 Bug原探,但依然沒辦法定位出具體的原因。Core Dump 出來的棧是全亂的顽素,沒有任何價(jià)值咽弦,接下來,就開始用 Log 跟蹤法了胁出。
我依據(jù)數(shù)據(jù)的流轉(zhuǎn)過程型型,在每個(gè)關(guān)鍵點(diǎn),都打上 Log全蝶,Log 包含了所在的函數(shù)闹蒜,行數(shù),程序邏輯的編號(hào)抑淫,全部的關(guān)鍵數(shù)據(jù)等等绷落。
我仔細(xì)地設(shè)計(jì)了這個(gè) Log,爭取打得不多不少始苇,太多容易看暈頭砌烁,而且太多無效的信息,會(huì)掩蓋了真正的問題埂蕊;太少往弓,信息不足,又不足以判斷蓄氧,所以這種情況下打 Log 也是個(gè)藝術(shù)活。
通過精心設(shè)計(jì)的 Log槐脏,終于發(fā)現(xiàn)數(shù)據(jù)在一個(gè)特定的環(huán)節(jié)混亂后喉童,程序就一定會(huì) Core Dump。
分析 Log , 發(fā)現(xiàn)數(shù)據(jù)包在最后時(shí)刻是完整的顿天,但包似乎出現(xiàn)了亂序和重復(fù)堂氯。這個(gè)時(shí)候,才開始意識(shí)到可能是無鎖隊(duì)列的問題牌废,因?yàn)橹挥嘘?duì)列出問題咽白,包的進(jìn)出順序才會(huì)亂掉。
然后又花了半天的時(shí)間鸟缕,專門為無鎖隊(duì)列寫了測試用例晶框,用數(shù)據(jù)瘋狂地懟排抬。在測試環(huán)境,依然一切安好授段,但上線到正式環(huán)境蹲蒲,壓測半小時(shí)后,終于掛了侵贵。終于看到了勝利的曙光届搁!這個(gè)時(shí)候已經(jīng)是第四天了!
到現(xiàn)在已經(jīng)很明確是無鎖隊(duì)列的問題了窍育。但這個(gè)數(shù)據(jù)結(jié)構(gòu)的代碼不到 200 行卡睦。我拉了兩個(gè)同事一起 Review,都沒發(fā)現(xiàn)問題漱抓。但就是 Core Dump 了表锻。
奇葩了,又陷入了人生懷疑辽旋,開始懷疑內(nèi)存浩嫌,懷疑 CPU,結(jié)果換了機(jī)器补胚,還是一樣码耐。
后來,仔細(xì)地對比了現(xiàn)網(wǎng)環(huán)境和測試環(huán)境的區(qū)別溶其,機(jī)器類型骚腥,操作系統(tǒng)版本都一樣。然后編譯器瓶逃?咦束铭!編譯器?上去看了一下厢绝,結(jié)果發(fā)現(xiàn)編譯器的版本不一樣契沫!
這段時(shí)間我所使用的現(xiàn)網(wǎng)編譯環(huán)境升級(jí)了新的 GCC 版本,但測試編譯環(huán)境昔汉,還是舊的版本的懈万。(這個(gè)也比較坑)
當(dāng)時(shí)的直覺是肯定跟編譯器相關(guān),但代碼都一樣靶病,難道是編譯器 Bug会通?不可能吧 ?娄周!
后來想涕侈,不如將它們轉(zhuǎn)換成匯編看看吧。于是用兩個(gè)版本的編譯器將 C 的代碼各自轉(zhuǎn)換成了匯編煤辨。然后 Diff 匯編代碼裳涛,哇木张!發(fā)現(xiàn)真的有一行是不同的!
后來自己分析對比调违,發(fā)現(xiàn)是因?yàn)槲覀冮_啟了 GCC 最高級(jí)別的代碼性能優(yōu)化窟哺,不同版本的 GCC 在一些沒有特定依賴的語句上的優(yōu)化是不同的。
說人話技肩,就是有一段代碼且轨,如果加了鎖,兩個(gè)版本的編譯器下虚婿,都會(huì)產(chǎn)生一樣的匯編旋奢,如果沒有加鎖,代碼有一行的順序被調(diào)整了然痊,當(dāng)然至朗,從編譯器優(yōu)化的角度講是對的,是我們使用姿勢不對剧浸。
但無論怎么樣锹引,終于找出了這個(gè)問題。蒼天啊! 找了五天呀唆香!最后當(dāng)然是開開心心地上線了嫌变。
查這個(gè) Bug 確實(shí)花費(fèi)了很多的時(shí)間,不過也是沒辦法躬它,你不解決 Bug腾啥,就不能上線,但中間也收獲不少冯吓,特別是對編譯器優(yōu)化有了很深的印象倘待,也算是為自己的查 Bug 能力,又貢獻(xiàn)了一波經(jīng)驗(yàn)吧组贺。
對于 Bug , 我分享下自己的一些認(rèn)識(shí)和建議凸舵。
面對 Bug 的態(tài)度
只要你持續(xù)地寫代碼,就一定會(huì)持續(xù)的產(chǎn)生 Bug失尖,所以第一個(gè)事情是要擺正對 Bug 的心態(tài)贞间。我遇到過兩個(gè)極端。
第一個(gè)極端
遇到過一個(gè) Leader雹仿,對系統(tǒng)質(zhì)量相當(dāng)重視,對我們寫的代碼要求很高整以,每次設(shè)計(jì)并寫完一個(gè)新的系統(tǒng)胧辽,他喜歡跟你算這次的系統(tǒng)上線,產(chǎn)生了多少次故障公黑,這半年時(shí)間產(chǎn)生了多少個(gè) Bug邑商,每個(gè) Bug 的影響范圍如何摄咆。
我們一堆人被搞得特別累,戰(zhàn)戰(zhàn)兢兢人断,到后面吭从,大家都比較排斥去做優(yōu)化,去重構(gòu)代碼恶迈,只求無過涩金,不求有功了。
所以暇仲,我覺得這種方式不好步做,對 Bug 帶著一種比較包容的態(tài)度去看待,可以減少不少的心理負(fù)擔(dān)奈附。
第二個(gè)極端
后來去了一個(gè)新的團(tuán)隊(duì)全度。新的團(tuán)隊(duì)很重視業(yè)務(wù)和工程迭代的速度筐带,所以對代碼質(zhì)量和 Bug 的容忍度很高给猾。如果是一個(gè)新上線的業(yè)務(wù)乓序,是默許 Bug 存在的钧栖。
這種對質(zhì)量過于松散的要求洛二,在后面也帶來不好的影響奸绷。大家對 Bug 太免疫了埂伦,以至于出現(xiàn) Bug 和故障的時(shí)候顿涣,大家都不夠緊張漩符。
系統(tǒng)質(zhì)量有一段時(shí)間出現(xiàn)比較大的問題一喘,還因此被部門經(jīng)理特訓(xùn)了一番,后面通過各種措施嗜暴,才慢慢提高了整體的系統(tǒng)質(zhì)量凸克。
上面兩種極端都不可取,應(yīng)該很重視 Bug闷沥,盡量避免 Bug萎战,但也不應(yīng)該唯 Bug 多少論業(yè)績。
具體到 Bug 的查找上舆逃,我說說我的一些經(jīng)驗(yàn)蚂维。
Bug 的復(fù)現(xiàn)
我把 Bug 分為可重現(xiàn)的 Bug 和不可復(fù)現(xiàn)的 Bug 。
對于可重現(xiàn)的 Bug , 查找起來比較容易路狮,比如可以用”二分查找“的方式虫啥,從模塊層面開始定位起,每次折半奄妨,每次折半地縮小范圍涂籽,一直到代碼層面。
在代碼層面砸抛,遵循一些常用的原則评雌,比如:
看到內(nèi)存拷貝树枫,直覺上要想到內(nèi)存越界。
看到數(shù)組景东,就要考慮是否索引越界砂轻。
看到指針,就要考慮是否正確解構(gòu)斤吐。
看到多線程搔涝,就要考慮是否線程安全。
對于不可重現(xiàn)的 Bug曲初,第一步就是要把它重現(xiàn)出來体谒。
有時(shí)候特別的難,特別是并發(fā)形態(tài)下產(chǎn)生的 Bug臼婆,出現(xiàn)的時(shí)機(jī)和觸發(fā)條件都不清楚抒痒。對于這種 Bug,只能通過各種嘗試去復(fù)現(xiàn)它颁褂。
比如將多并發(fā)調(diào)整為單并發(fā)的方式故响,看能否復(fù)現(xiàn),如果可以復(fù)現(xiàn)颁独,就可以轉(zhuǎn)化為可復(fù)現(xiàn)的 Bug彩届,用”二分查找“的方式去排查。
如果不能復(fù)現(xiàn)誓酒,那極大概率是并發(fā)問題樟蠕。這個(gè)時(shí)候最好先停止排查,仔細(xì)分析程序在并發(fā)狀態(tài)下可能出問題的點(diǎn)靠柑。
大部分并發(fā)問題的根源寨辩,是互斥數(shù)據(jù)沒有被正確讀寫,或者一些共享狀態(tài)被錯(cuò)誤修改歼冰。
靜態(tài)代碼檢查
利用 Coverity 等代碼檢查工具進(jìn)行代碼的靜態(tài)檢查可以發(fā)現(xiàn)很多潛在的問題靡狞,而且修復(fù)的成本很低。團(tuán)隊(duì)后來引入了這個(gè)檢查工具隔嫡,確實(shí)帶來了不錯(cuò)的效果甸怕。類似變量未初始化,疑似的內(nèi)存越界等都有可能被檢查出來腮恩。
編譯器的 Warning梢杭。有些同學(xué)一開始的時(shí)候?qū)?Warning 不重視。我們團(tuán)隊(duì)早期也遇到過這個(gè)情況秸滴。
那時(shí)候產(chǎn)品迭代的速度很快式曲,所以大家寫完代碼,能夠編譯通過,就進(jìn)行各種測試吝羞,然后準(zhǔn)備上線了。
一開始的一兩個(gè) Warning内颗,不太理會(huì)钧排。后面發(fā)現(xiàn)越積越多,到最后終于成為一個(gè)不得不解決的問題均澳。
部門還為此特地立項(xiàng)恨溜,來消除 Warning。先是在內(nèi)部多次強(qiáng)調(diào)了這個(gè)理念找前,然后從基礎(chǔ)庫糟袁,基礎(chǔ)模塊開始實(shí)施,基礎(chǔ)代碼部分統(tǒng)一 Fix Warning躺盛,然后開啟編譯器把 Warning 當(dāng) Error 的開關(guān)项戴。
完成之后,再逐步地推業(yè)務(wù)模塊進(jìn)行修改槽惫。反正折騰了好一段時(shí)間周叮。
工欲善其事必先利其器
代碼 Bug 出現(xiàn)的時(shí)候,善用一些排查工具可以極大得提升效率界斜。比如對于 C/C++ 的 GDB 調(diào)試仿耽,內(nèi)存泄漏時(shí)候 Valgrind 的檢測,Linux 下面用 Perf Top 來析 CPU 的消耗等各薇。
一開始的時(shí)候项贺,我對這些工具不重視,老是覺得真正使用的時(shí)候峭判,去查文檔就行开缎。
后面才發(fā)現(xiàn),用工具查著問題時(shí)候朝抖,遇到不會(huì)用的命令或功能啥箭,再去查文檔,是個(gè)痛苦的事情治宣,來回切換的開銷也使得效率低下急侥。
后面就對這些輔助工具的使用重視了起來,專門花時(shí)間去學(xué)習(xí)和練習(xí)使用侮邀,反而提升了不少的效率坏怪。
打 Log 的藝術(shù)
很多時(shí)候,出現(xiàn)一個(gè) Bug绊茧,未能定位出來铝宵,需要打上更多額外的 Log 來輔助排查。一開始的時(shí)候,是想到一點(diǎn)鹏秋,打一個(gè) Log尊蚁,后面發(fā)現(xiàn)這么做沒有章法,邏輯不清晰, 排查效率低下侣夷。
后來學(xué)會(huì)了横朋,遇到 Bug 后,先在腦子里面分析一番百拓,然后花一兩個(gè)小時(shí)詳細(xì)地設(shè)計(jì) Log 的格式和打 Log 的位置琴锭。發(fā)現(xiàn)這種方式對查問題的效率提升很大。
所以遇到 Bug 的時(shí)候不要急躁衙传,先靜下來心來分析决帖,在腦海里盡力重現(xiàn)出完整的運(yùn)行邏輯,然后仔細(xì)地進(jìn)行 Log 設(shè)計(jì)蓖捶,包括 Log 包含的字段地回,打 Log 的點(diǎn)等。這樣能極大的提升排查問題的效率腺阳。
結(jié)語
Bug 是程序員最不愿意面對落君,但又經(jīng)常出現(xiàn)的一個(gè) “詭異生物“。對待 Bug 要有正確的心態(tài)亭引,最好不要跟業(yè)績強(qiáng)綁定绎速,也不能太過于疏忽。
Bug 的排查是個(gè)很復(fù)雜的事情焙蚓,每個(gè)人都有自己的方式和做法纹冤,如果你有好的做法和建議,也歡迎在留言區(qū)分享給大家购公!