那些讓人睡不着覺的 bug,你有沒有遭遇過?

我先講一個小故事,之前在外企工做時的一個親身經歷。前端

當時我所在的team,負責手機上多媒體Library方面的開發。有一天,一個具備最高等級的bug被轉到了個人手上。這個bug很是詭異,光是重現它就須要花很長時間。在公司內部的issue追蹤系統上,測試人員描述了詳盡的重現步驟,大概意思是說,用某個指定產品線的手機去播放一段《黑客帝國》的視頻,大概播放到半個小時左右的時候,程序就會忽然崩潰。程序員

你能夠想象,形成崩潰問題的可能緣由實在太多了,好比某個局部算法的實現發生地址越界了,或者多線程執行的時序混亂了,再或者傳給解碼器的參數傳遞錯了,等等,諸如此類。問題可能出在上層應用,也可能出在中間的Library,或者編解碼器有問題,甚至是底下的內核或硬件不穩定(我當時所在的公司是一家手機制造商,軟件和硬件都是本身設計),總之,你能想到的或想不到的都有可能。算法

事實上,對於這個問題的分析也是按照從上至下的順序進行的。首先,既然播放會崩潰,那至少看起來是播放器的問題啊,OK,issue會先轉給播放器的開發團隊。播放器的開發團隊通過分析以後發現,並非他們的代碼引起的問題,接下來他們把分析結果附在issue的處理歷史上,並把issue轉給下一個團隊處理。那應該轉給哪一個團隊呢?就要看上一個團隊的分析結果了。他們在分析的過程當中,會追蹤到最終崩潰的地方是發生在他們引用的哪一個代碼模塊中,而後就有專門的人負責找到維護相關模塊的團隊。就這樣,這個bug從上層開始,通過層層流轉,終於有一天來到了個人手上。數據庫

一個團隊被分發到這樣一個最高等級的bug,就意味着必須停下正在進行的一些工做,當即分出人手來處理它。這就像一個燙手的山芋,誰也不想讓它在本身的團隊裏待上太長時間。我通過大半天的分析,終於證實了崩潰的精確位置並不在咱們負責維護的代碼區域裏,而是在咱們調用的更下層的一個模塊中。OK,在系統中填上分析結果,附上分析日誌,再起草一封總結郵件,個人處理工做就此愉快地結束。可是,bug依然存在!c#

有人會好奇,這個bug後來怎麼樣了?它就這樣在issue追蹤系統上轉了個把月,最後因爲對應的產品線被cancel了(也就是那個產品線被砍掉了),天然全部相關的bug也就不必再去解決了。這個bug就這樣不了了之了......後端


我所說的這家外企,曾經以完善的工做流程和質量管理體系而著名。無論是開發新特性,仍是解bug,都是靠流程去推進。公司員工衆多,而且分佈在全球範圍,這樣的一套內部管理流程天然是必不可少的。假設當時那個bug,若是最後不是被cancel了,那麼它會不會在流程的推進下最終被解決呢?我以爲,會,必定會。只要時間足夠長,最終它確定會被轉到真正可以解決它的人手裏。只不過,這裏的問題是,總體運轉的效率過低下了。安全

與傳統的IT公司不一樣,互聯網公司通常被認爲是運轉效率更高的。但有些bug其實更加難解,由於互聯網產品運行的環境更加複雜多變。面對一些難解決的問題,好比對於某些用戶報出來的但咱們本身卻沒法重現的問題,咱們有時候會碰到這樣一幕:後端的同窗查完,宣稱後端沒有發現問題;而後客戶端或前端同窗查完,也宣稱沒有發現問題。最後,你們也不能一直耗在這一個問題上,後面還有數不清的開發需求在排隊,因而,問題也一樣不了了之了。等過了一個月,兩個月,甚至是一年,有些「老大難」的問題依然存在。服務器

這顯然不是咱們但願看到的結果。那麼問題到底出在哪呢?微信

首先,沒有人能瞭解全貌。像我開頭講的外企中的那個例子,每一個團隊基本只瞭解本身負責的模塊,沒有人知道問題真正出在哪。這時候最理想的狀況是,公司有一些元老級的技術專家,他們可能在公司初創的時候就在,隨着公司一塊兒成長,既懂業務又懂技術,可以從上層一直分析到底層,最終把問題解決掉,或者至少分析到足夠的細節再轉給真正能解決問題的人。但事實每每事與願違,公司就算有一些元老的員工,他們也每每過早地脫離了技術。他們一般很忙,忙着開各類各樣的會(固然開會並非一個貶義詞)...... 那實際中咱們若是沒有這樣瞭解全盤的人該怎麼辦呢?這就須要責任心極強的人,可以把解決問題的各方串起來。網絡

其次,缺乏足夠的分析問題的手段和工具。對於知道如何重現的問題,通常來講都比較容易解決,工程師經過調試,一步步跟蹤,總能找到問題所在。但對於那些很差重現的問題,每每使人束手無策,由於咱們不知道問題發生時的真實狀況,也就是抓不到「現場」。

記得剛開始出來創業那會兒,咱們的服務器發生了一件奇怪的事。每隔一兩天,就會有臺Web服務器莫名地死掉。當時的報警機制也不太完善,問題發生時又可能是在深夜,等問題出現時去看的時候,服務器已經登陸不了了,因而只能重啓解決,而重啓以後問題也就消失了。經過一些監控工具去觀察,只能看到機器重啓前CPU暴漲,跑到了100%,多是因爲用的是虛擬機的緣故,那個時候機器就陷入「假死」了。通過反覆追蹤,終於有一次抓到「現場」了,在CPU跑滿以前把流量從出問題的機器上卸了下來,結果那臺機器的CPU竟依然居高不下。最後使用jstack分析了半天,發現有一些線程出現了死循環(仔細看才能看出來),原來是有一個HashMap被用在了多線程的環境下,結果內部的數據結構發生混亂了,在JDK內部對Map進行遍歷操做的時候出現了死循環,最後把CPU跑滿了。原本是個線程安全問題,表現出來倒是一個性能問題。如今回想起來,若是當時有更完善的監控工具,就能儘早地發現問題;若是對程序的棧結構和jstack工具備更深的瞭解,就能更快地分析出問題緣由。

另外,對於互聯網產品上常常出現的那種用戶側有問題,而咱們卻沒法重現的狀況,技術同窗感受到解決困難的緣由,也每每是供他分析的「資料」不足。

第三,也是最重要的,咱們須要的是持之以恆的精神。頑固的bug就像狡猾的獵物,它會激起出色獵手的興趣,而普通的獵手則會輕易放棄。出色的獵手會一直追蹤它,直到最終捕獲。對待頑固的那些bug,真正的解決之道其實只有一個,那就是你要比它們更加頑固。

不少人會產生這樣一種想法,認爲解bug純粹是個體力活,不值得長時間投入。實際上,對於技術專業自己的進階來講,這倒是打怪升級,使得技藝登堂入室的必要一步。一方面,當你一直在留心觀察某一個問題的時候,你對於系統相關的運行模式會愈發地熟悉。你知道正常狀況下的參數水平,也能識別出每個異常狀況。幾乎沒有別的方式能讓你對系統的瞭解達到如此深刻和敏感的程度。另外一方面,我以前在《技術攻關:從零到精通》一文中也提到過,研究某個具體問題自己就可能引起整個架構的調整。 當舊的架構怎麼修補也沒法解決問題的時候,它最終將化繭成蝶、浴火重生,全部這些因素逼迫系統的架構向着更高的層次進化。


實際中咱們通常會碰到哪些比較頑固的問題呢?至少有這麼三類:

  • 不必定何時出現的;
  • 跟性能有關的(找性能瓶頸);
  • 只在特定環境中出現的。

我前面提到的那個CPU跑滿的例子,就屬於第一類。對這種問題,一方面,要仔細研究代碼,另外一方面,就是在問題出現以前作好充分的準備,記錄下足夠多的日誌信息,這樣才能在問題真正出現時「抓住」它。

跟性能有關的問題,它的難點就在於當問題出現時它所表現出來的各個因素相互影響,分不清哪一個是因哪一個是果。咱們有時須要進行復雜的Profiing(動態的性能分析)才能找到緣由。客戶端的問題相對單純一點,有不少成熟的Profiling的工具,而服務器的狀況相對複雜一些。忽然想到了胡峯同窗在他的公衆號「瞬息之間」上翻譯過的一篇文章《認清性能問題》,寫得很好,值得一讀。文章對於響應時間和吞吐量的關係,以及性能拐點的描述,使人印象深入,頗有指導意義。原文地址以下:

mp.weixin.qq.com/s/-M2EfUc_X…

在創業的這幾年中,隨着訪問量的增大,性能問題一個接着一個(特別是數據庫的性能問題)。但真正印象深入的仍是創業初期碰到的那些問題,也許是由於當時經驗不足,因此才感觸比較深吧。記得有一天早上流量高峯期,幾臺Web服務器相繼宕機。重啓以後內存漸漸走高,堅持不了幾分鐘,內存就又爆掉。你們簡單地分析以後,仍是不肯定是什麼具體緣由。因而我跟坐在旁邊的李甫同窗商量,要不你負責把服務器按期地提早重啓一下,別等它本身OOM了......至少線上服務不會一點都不可用,而我來負責用工具分析一下。而後用jmap把整個heap都dump了出來,把dump文件拷貝到一臺空閒機器上,再啓動jhat來觀察。結果一會兒看得比較明顯了,是源於ConcurrentHashMap相關的一些數據結構形成了內存泄露。分析到這裏,若是沒有相關的經驗,可能仍然不知道是怎麼回事。可是結合網上的資料,就能把懷疑指向一點:可能代碼中使用了HttpSession,而HttpSession是由ConcurrentHashMap來管理的。查找一下工程代碼,果真,用到了request.getSession()。在分佈式的Web架構中,HttpSession就是個雞肋,不少有開發經驗的公司都會禁止程序員使用它,但總有人忘了這件事。還好,這些調用的地方通常都比較容易消除。改掉以後內存問題也就隨之消失了。

第三類「只在特定環境中出現的」問題,更是難以解決。一般它們隻影響少部分用戶,因此人們的重視通常也不夠。這種問題客戶端開發遭遇的會比較多,特別是安卓客戶端,主要是執行環境太複雜了。它們有時候只在某些特定機型上出現,有時候只對於某些特定用戶出現,還有時候甚至只在特定的網絡環境下才出現。

好比有一次,有些用戶報告咱們的App裏某個遊戲打不開,緣由是資源下載老是失敗,並且只在手機鏈接WIFI的時候下載失敗,若是換成了3G信號就能夠下載成功了。咱們天然是重現不了,直到後來拿到了一些用戶手機上下載的部分文件才弄清怎麼回事。原來是咱們的遊戲資源中包含一個XML配置文件,而這個文件被插入了一段JS代碼,因而對於這個文件咱們的下載程序就校驗不過去了。那是誰插入的JS代碼呢?答案是運營商。爲何要插入呢?是爲了顯示一個廣告......沒錯,這就是傳說中的被運營商「流量劫持」了。運營商的程序把這個XML文件誤認爲是一個網頁了。

還有一次,忽然有用戶報告在某些vivo機型上,QQ登陸失敗。咱們本身重現不了,咱們拿現有的vivo手機也重現不了,甚至嘗試把設置裏的「不保留活動」選項打開,仍然重現不了。記得當時是皇甫同窗在解決這個問題,花了很多力氣,最終想辦法拿到了用戶手機上的執行日誌,才搞清了是什麼情況。原來是在跳轉到QQ去登陸以前,咱們的程序在內存裏保存了一個變量,等從QQ登陸完跳回來以後,這個變量的值消失了。這個變量保存在一個單例的實例裏面,按說不該該被釋放。但多是因爲那個手機型號上系統資源嚴重不足,或者是系統有些特殊的設置,致使跳轉到QQ以後,咱們的進程被系統KILL了(這在安卓系統上應該屬於正常的行爲),天然全部內存的值也都保存不住了。

你們可能已經看出來了,解決在用戶側發生的特定問題,關鍵是可以收集到用戶的本地運行日誌。好比微信開放出來的Mars Xlog,就是作這個事情的,是很好的一個工具。聽說做者最近還會放出一些新的特性。若是你用的是iOS版微信,那麼在添加朋友的時候輸入「:up」,就能看到微信的日誌上報界面(安卓版微信我不知道怎麼能點出來,有知道的同窗能夠在下面留言)。這裏上報的日誌聽說就是經過Xlog打印出來的。

總之,當用戶報此類「只在特定環境中出現的」問題時,因爲咱們通常都沒法重現,開發人員首先會以爲很是奇怪。但奇怪自己並解決了不了任何問題。當客戶端技術人員在排查完以後宣稱代碼沒有問題的時候,極可能他並不瞭解用戶側真正發生的狀況,而他得出的結論也只是一種「猜想」而已。用事實說話,而不是憑藉主觀臆斷,應該是技術人員行事的基本原則。若是必定要違背這個原則才能作出論斷,那咱們寧肯不作這個論斷。


事情永遠不可能完美,這個世界也沒有完美的狀態。程序在運轉的過程當中也總會出錯。

記得之前在一個測試技術的培訓會上,有一位講師說過,「 哪怕只碰到一次的問題,也是問題。」關鍵在於,咱們能認可和接受這種不完美,而不是去逃避或視而不見。要知道,工程技術的核心,就是設法讓完美的邏輯模型在不完美的世界中可以暢快運行的一項技藝

只要咱們持續努力,就必定會比過去作得更好。

(完)

其它精選文章

相關文章
相關標籤/搜索