人在江湖飄,哪能不挨刀。
我挨了重重一bug。嚴格來講這可能是我職業生涯以來的首個悲慘經歷,因為憑我的知識儲備和經驗,基本上任何可重現的bug都是可解的。然而這個bug卻困擾了我三個月之久,它具有以下生理特征:
- 后台日志能統計到異常,偶發、低頻
- 報異常的用戶設備不具有規律性,什么手機都有
- 我們自己無法復現,任何設備、任何環境都沒復現
- 打電話回訪報異常的用戶,確實存在問題
- 客服未接到用戶主動反饋這個異常
此bug並不是js報錯,而是一個業務邏輯的錯誤。表現是,用戶提交的數據莫名缺失。場景是以下這個界面
當用戶填滿所有的空之后,提交按鈕變為可用狀態,數據放進一個數組中提交上來。后台有報錯日志顯示,用戶提交上來的有些是空數組,有些是數組中缺了幾項。
問題在於,提交前是有校驗的,用戶不可能提交上來這種未通過校驗的數據。並且還是偶發的啊,如果是邏輯寫錯了,那應該全部會報錯,我們在測試的時候肯定會發現。
最棘手的地方是,我們壓根沒重現過這個情況,找各種同事、各種手機、各種胡亂操作,一次都沒重現出來。這就給調試帶來很大的麻煩,只能是猜測哪里可能出問題,然后去驗證。但是根本沒法去驗證啊。。。重現不了,又如何判斷是成功fix了。
看來能驗證的手段就只剩一個:線上日志。猜問題、上線、看日志。
這是一個痛苦的過程。界面雖然簡單,這卻是一個龐雜的項目。因為題型眾多,抽離了很多組件,為了公用和靈活擴展,組件嵌套深度有五層之多。其架構復雜程度在我的職業生涯中也能排TOP3.
拿題干的渲染來說,就有:公式圖片轉LaTeX、mathjax渲染公式、渲染公式上的空、給空編號、模擬光標、自動focus空、動態計算字體大小等諸多流程。而且下方那個鍵盤還是我們H5模擬的,並不是系統鍵盤。更別提還有校驗邏輯、判分邏輯。
前n次嘗試
看距離上一版有哪些改動,抹去有嫌疑的改動,看日志是否正常。尷尬的是,這是一次重大重構,改動的地方還特別多。於是一場盲人摸象式的遠程debug行動開始了。
一次又一次的上線、觀察日志、下線。不斷排除了一些相關的功能,始終未能診斷到問題所在。甚至連我很確信的地方都嘗試了,還是找不到問題。前前后后嘗試了二十多次吧,改到我都懷疑人生了。領導看了這些上線記錄都怒了,說你這上上下下的搞雞毛呢。我也很崩潰啊。
看來用這個盲人摸象手段是搞不定了,我意識到了情況的嚴重性,暗暗感覺這可能不是輕易能解決的,呂某一定使出畢生所學,為民除害。
第n+1次嘗試
既然有那么多的用戶日志,我們自己為何重現不了?這是我一直糾結的。於是再次進行瘋狂測試。
皇天不負有心人,我竟然真的給重現出來了!操作是這樣的:填好空,兩個手指同時按下提交按鈕和刪除按鈕。這樣的話既通過了校驗,又能在提交之前把數據給刪了。
發現這個騷操作的時候我是很興奮的,但是會有那么多用戶這么操作嗎?顯然不太可能。此時我又想到,提交按鈕和刪除按鈕是挨着的,會不會是用戶按提交的時候誤觸了刪除鍵。這還算比較合理,畢竟用戶是小學生嘛,操作不一定那么精准的。
我興奮不已的進行驗證。在刪除鍵和提交鍵之間加了“下一空”按鈕(通過配置),這樣用戶保證不會誤觸了。
上線,日志依舊。我摔啊,看來並不是誤觸的事。
第n+2次嘗試
隨着bug拖的時間越來越長,我的心態也有點焦躁。但思路還是聚焦在刪除按鈕上,畢竟這是好不容易發現能重現的。
如何能夠既點提交又點刪除呢?這時候我想到了點擊穿透(鍵盤為了響應快,使用了touchstart事件)。因為在點完提交的時候,模擬鍵盤會收起來,而收起的過程中刪除按鈕會經過提交按鈕的位置。根據點擊穿透的原理,如果此時派發的click事件作用到了刪除按鈕上,那豈不是就算點到了?
我都有點佩服我的想象力了,黔驢技窮了啊,試吧。避免點擊穿透有兩種方式,阻止click事件的默認動作,或者是讓元素收起的時間延遲。我選了后者。
上線,日志依舊。我吐血。后來一想,刪除按鈕根本都沒監聽click事件啊,哪來的穿透。真是病急亂投醫了。
第n+3次嘗試
掃代碼,發現一個很重的疑點。答案是個數組,是引用類型。由於復雜的組件關系,這個引用類型的數據可以被多個組件訪問到。
使用可變數據的時候有個隱患,它可能在你不知道的地方被修改。代碼是vue寫的,有些組件中含有watch,搞不好是意外進了哪里的watch,在點完提交的時候也會把數據給更改了。
這個猜測我覺得是合理的,在開發階段我就曾因為未使用immutable數據而隱隱擔心過。好了,快速驗證吧。在點完提交按鈕的時候,我把答案數據給克隆了一份,然后再進行判分和提交的操作。這下就不擔心已經拿到的數據被篡改了。
上線,日志依舊。繼續吐血。
不過這次也縮小了嫌疑范圍,看來數據不是在點完提交的時候被篡改了,而是提交上來的就有問題。匪夷所思的是,用戶是如何繞過校驗把數據提交上來的呢?難不成是我的校驗函數有問題,這個地方把數據給改了?掃了一遍代碼,無果。
第n+4次嘗試
此時聚焦到了用戶在填寫答案的時候發生了什么。我像偵探一樣用放大鏡一遍遍看代碼,然而好多天的追蹤,並沒有找到什么有用線索。
直到有一天,那天陽光明媚天空飄着朵朵白雲,感覺有什么好事要發生。QA在反饋群里發了一張截圖,說公式解析的那個點點點一直不消失(正在解析的狀態),而且空里也輸不進內容去。如下圖:
我敏感的神經頓時嗅到了一絲線索。題干使用了mathjax來解析公式,而mathjax在解析的過程中會按需加載一些字體文件,而且還會掃描頁面節點,並生成大量的DOM節點。這對瀏覽器來說是個壓力不小的事情,更何況是移動端。
我馬上再掃描公式處理的代碼,由於有些空會在公式上出現,所以代碼是在等公式渲染完后統一給空編序號,然后進行自動focus,而且自動focus的時候還會首先給答案賦值。天吶,問題該不會出現在這里吧!公式的渲染過程可能有延時,用戶可能在這個時間進行點什么操作!
首先這符合偶發這個事實,因為公式解析中出現抖動網絡延遲什么的也是偶然現象。再者公司的網絡快,用戶的網絡可能慢,這也符合我們一直未重現的事實。感覺這次很靠譜了!很多偵探電視都是這么演的啊,主人公通過別人無意的一句話聯想到了線索,然后案件破解,真相大白!對對對,就是這個感覺!
趕快在代碼層面做優化,盡可能早地處理沒有公式的空,有公式的地方也確保執行完后用戶才能輸入。
優化完畢,回歸測試,萬事俱備,只等線上驗證,一錘定音!
結果......日志還有啊!啊噗!,電視里都是騙人的啊!
等等!日志雖然還有,但好像少了耶!難道這次的優化是有作用的?雖然從理論上能解釋一些作用,但還存在的日志又表示什么呢?難道造成丟答案的原因不止一個?
第n+5次嘗試
時間一天天過去,我還是沒找到什么有力線索。中陸續有一些猜測,打了一些日志點后還是無果。看着QA同事緊縮的眉頭,領導關切的詢問,我也越發焦慮了起來。因為我這是一個公共組件庫,有其他項目在等着使用,如果我的bug解決不了,將影響其他項目的進度。
又是陽光明媚的一天,天空飄着朵朵白雲。我無意跟另一位后端同事聊到了這個話題,他隨口一說:應該是超時自動提交的吧。
什么?什么!自動提交?!我突然像被閃電擊中。因為我寫的這是個公共組件,同時也對外暴漏了一些API,比如提交答案就是其中一個。我提供的是答題界面的組件,但是別人項目中有倒計時的場景,超時后會調用我的提交API,把用戶答案提交上去。
如果超時的時候,用戶什么也沒填,那豈不是把空答案給提交上去了!!!根本沒有校驗函數什么事,是別人調API提交的啊。
我哭了。難怪沒用戶反饋呢,時間到了自動提交空答案,他們沒理由反饋啊。難怪我們自己沒重現呢,一直沉浸在怎么亂點出來。就算QA同學看到了超時提交的時候,也無法意識此時是空答案。
沒錯,真相就是它了,修改相關邏輯后上線,果然報錯日志沒了。困擾我三個月的bug終於解決了!我閉上眼睛,心里默默放了一把鞭炮。
總結
前前后后三個月時間,總算是找到了問題所在。其實第n+4次是解決了一些問題的,最后一次是徹底解決,我用實際行動證明了,真相不止一個。而這件沸沸揚揚的丟答案bug事件,也給了我很多啟示。
-
做重構的時候要格外小心邏輯更改,重構后一定要跑通所有case。
-
排查問題的方式,這期間我使用了各種對照試驗,各種源碼級別的排查
-
使用vue做復雜項目的時候要格外注意組件的嵌套層數,少寫watch,避免程序執行順序的混亂
-
設計對外API時,要考慮健壯性,不光考慮傳入參數的不穩定,還要考慮當前上下文的不穩定。