睡覺的時候,程序能不能自動查 bug?

作者 | 杜沁園 等

出品 | CSDN(ID:CSDNnews)

曾在 Hacker News 上看到過一個 Oracle 工程師處理 bug 的 日常:

先花兩週左右時間來理解 20 個參數如何通過神奇的組合引發 bug。

改了幾行代碼,嘗試對 bug 進行修復,提交測試集群開始跑近百萬個測試 case,通常要 20~30 小時。

運氣好的話會有 100 多個 case 沒過,有時候上千個也有可能,只好挑選幾個來看,發現還有 10 個參數之前沒有注意到。

又過了兩週,終於找到了引起 bug 的真正參數組合,並跑通了所有測試。並增加 100 多個測試 case 確保覆蓋他的修改。

經過一個多月的代碼 review,他的修改終於合併了,開始處理下一個 bug……

後來這個工程師感慨說:“I don't work for Oracle anymore. Will never work for Oracle again!”

Oracle 12.2 有將近 2500 萬行 C 代碼,複雜系統的測試是一件艱難、艱苦和艱鉅的事情,而測試一個分佈式數據庫的情況就更復雜了。我們永遠不知道用戶可能寫出什麼樣的 SQL,表結構和索引有多少種組合,此外還要考慮集群在什麼時候節點發生宕機,以及受到網絡抖動、磁盤性能退化等因素的影響——可能性幾乎是無限的。

那麼有沒有一種方法能讓程序自動幫我們查 bug?

如何做到「睡覺的時候讓程序自動查 bug」?

項目的思路其實很簡單,如果在每次跑 case 的時候能用統計學的方法對足夠多次實驗的代碼路徑進行分析,就可以找出疑似 bug 的代碼,最終結果以代碼染色的方式由前端可視化呈現,就得到了如下圖展示的效果:

「顏色越深,亮度越高」表示包含錯誤邏輯的可能性越大。該方法不僅適用於數據庫系統的測試,同樣適用於其他任何複雜的系統。

背後的原理

項目最初是受到 VLDB 的一篇論文的啟發 APOLLO: Automatic Detection and Diagnosis of Performance Regressions in Database Systems,該論文主要圍繞如何診斷引發數據庫性能回退的代碼,其核心思想也同樣適用於排查 bug。論文中提到的自動診斷系統由 SQLFuzz,SQLMin 和 SQLDebug 三個模塊組成。

SQLFuzz:負責隨機生成 SQL,並利用二分查找定位到性能回退的前後兩個版本,傳遞給 SQLMin 模塊。

SQLMin:通過剪枝算法將 SQLFuzz 生成的 SQL 進行化簡,得出能夠復現該問題的最小 SQL ,傳遞給 SQLDebug 模塊。目的是減少無關的代碼路徑,降低噪音。

SQLDebug:對源碼進行插樁,使其在執行 SQL 時能夠輸出代碼的執行路徑。然後對兩個版本的代碼路徑進行分析,建立一個統計模型來定位問題的位置。

最終系統自動生成測試報告,內容包含:

哪一次的代碼 commit 引入了性能回退。

存在問題的代碼源文件。

具體的函數位置。

而實際上,考慮到併發、循環、遞歸等帶來的影響,代碼執行路徑分析會非常複雜。為了保證能夠在 Hackathon 那麼短的時間內展示出效果,我們又參考了另一篇論文 Visualization of Test Information to Assist Fault Localization,其核心思想是通過統計代碼塊被正確和錯誤測試用例經過次數,再基於分析算法來塗上不同的顏色,簡單而實用。

其實藉助這個思路也可以應用到其他領域,後面我們將展開來介紹。接下來我們先來看看 SQLDebug 是如何實現的。

聊聊細 (gān) 節 (huò)

如何自動產生測試 case?

由於是基於統計的診斷,我們需要先構建足夠多的測試用例,這個過程當然最好也由程序自動完成。事實上,grammar-based 的測試在檢驗編譯器正確性方面有相當長的歷史,DBMS 社區也採用類似的方法來驗證數據庫的功能性。比如:微軟的 SQL Server 團隊開發的 RAGS 系統對數據庫進行持續的自動化測試,還有社區比較出名的 SQLSmith 項目等等。今年 TiDB Hackathon 的另一個獲獎項目 sql-spider 也是實現類似的目的。

這裡我們暫時採用 PingCAP 開源的隨機測試框架 go-randgen 實現 SQL fuzzing,它需要用戶寫一些規則文件來幫助生成隨機的 SQL 測試用例。規則文件由一些產生式組成。randgen 每次從 query 開始隨機遊走一遍產生式,生成一條 SQL,產生一條像下圖紅線這樣的路徑。

我們將每個產生式生成正確與錯誤用例的比例作為該產生式的顏色值,繪製成一個頁面,作為 SQLFuzz 的展示頁面。通過該頁面,可以比較容易地看出哪條產生式更容易產生錯誤的 SQL。

代碼跟蹤

為了跟蹤每一條 SQL 在運行時的代碼執行路徑,一個關鍵操作是對被測程序進行插樁 (Dynamic Instrumentation)。VLDB 論文中提到一個二進制插樁工具 DynamoRIO,但是我們不確定用它來搞 Go 編譯的二進制能否正常工作。換一個思路,如果能在編譯之前直接對源碼進行插樁呢?

參考 go cover tool 的實現,我們寫了一個專門的代碼插樁工具 tidb-wrapper。它能夠對任意版本的 TiDB 源碼進行處理,生成 wrapped 代碼。並且在程序中注入一個 HTTP Server,假設某條 SQL 的摘要是 df6bfbff(這裡的摘要指的是 SQL 語句的 32 位 MurmurHash 計算結果的十六進制,主要目的是簡化傳輸的數據),那麼只要訪問 http://<tidb-server-ip>::43222/trace/df6bfbff 就能獲得該 SQL 所經過的源碼文件和代碼塊信息。/<tidb-server-ip>

<code>// http://localhost:43222/trace/df6bfbff

{
"sql": "show databases",
"trace": [
{
"file": "executor/batch_checker.go",
"line":
},
{
"file": "infoschema/infoschema.go",
"line": [
[
113,
113
],
[
261,
261
],
//....
}
],
}
/<code>

line 字段輸出的每個二元組都是一個基本塊的起始與結束行號(左閉右閉)。基本塊的定義是絕對不會產生分支的一個代碼塊,也是我們統計的最小粒度。那是如何識別出 Go 代碼中基本塊的呢?其實工作量還挺大的,幸好 Go 的源碼中有這一段,我們又剛好看到過,就把它裁剪出來,成為 go-blockscanner。

因為主要目標是正確性診斷,所以我們限定系統不對 TiDB 併發執行 SQL,這樣就可以認為從 server/conn.go:handleQuery 方法被調用開始,到 SQLDebug 模塊訪問 trace 接口的這段時間所有被執行的基本塊都是這條 SQL 的執行路徑。當 SQLDebug 模塊訪問 HTTP 接口,將會同時刪除該 SQL 相關的 trace 信息,避免內存被撐爆。

基本塊統計

SQLDebug 模塊在獲取到每條 SQL 經過的基本塊信息後,會對每個基本塊建立如下的可視化模型。

首先是顏色,經過基本塊的失敗用例比例越高,基本塊的顏色就越深。

然後是亮度,經過基本塊的失敗用例在總的失敗用例中佔的比例越高,基本塊的亮度越高。

已經有了顏色指標,為什麼還要一個亮度指標呢?其實亮度指標是為了彌補“顏色指標 Score”的一些偏見。比如某個代碼路徑只被一個錯誤用例經過了,那麼它顯然會獲得 Score 的最高分 1,事實上這條路徑不那麼有代表性,因為這麼多錯誤用例中只有一個經過了這條路徑,大概率不是錯誤的真正原因。所以需要額外的一個亮度指標來避免這種路徑的干擾,

只有顏色深,亮度高的代碼塊,才是真正值得懷疑的代碼塊。

上面的兩個模型主要是依據之前提到的 Visualization 的論文,我們還自創了一個文件排序的指標,失敗用例在該文件中的密度越大(按照基本塊),文件排名越靠前:

前端拿到這些指標後,按照上面計算出的文件排名順序進行展示,越靠前的文件存在問題的風險就越高。

當點擊展開後可以看到染色後的代碼塊:

我們經過一些簡單的實驗,文件級別的診斷相對比較準確,對於基本塊的診斷相對還有些粗糙,這跟沒有實現 SQLMin 有很大關係,畢竟 SQLMin 能去除不少統計時的噪聲。

還能不能做點別的?

看到這裡,你可能覺得這個項目不過是針對數據庫系統的自動化測試。而實際上藉助代碼自動調試的思路,可以給我們更多的啟發。

源碼教學

閱讀和分析複雜系統的源碼是個頭疼的事情,基於源碼的運行時可視化跟蹤能否做成一個通用工具呢?這樣在程序執行的同時就可以直觀地看到代碼的運行過程,對快速理解源碼一定會大有幫助。更進一步,配合源碼在線執行有沒有可能做成一個在線 web 應用呢?

全鏈路測試覆蓋統計

語言本身提供的單測覆蓋統計工具已經比較完備了,但一般測試流程中還要通過 e2e 測試、集成測試、穩定性測試等等。能否用本文的方法綜合計算出各種測試的覆蓋度,並且與 CI 系統和自動化測試平臺整合起來。利用代碼染色技術,還可以輸出代碼執行的熱力圖分析。結合 profiler 工具,是不是還可以輔助來定位代碼的性能問題?

Chaos Engineering

在 PingCAP 內部有諸多的 Chaos 測試平臺,用來驗證分佈式系統的魯棒性,譬如像 Schrodinger,Jepsen 等等。混沌測試有個弊端就是,當跑出問題之後想再次復現就很難,所以只能通過當時的情形去猜代碼可能哪裡有問題。如果能在程序運行時記錄代碼的執行路徑,根據問題發生時間點附近的日誌和監控進一步縮小範圍,再結合代碼路徑進行分析就能精確快速的定位到問題的原因。

與分佈式 Tracing 系統集成

Google 有一篇論文是介紹其內部的 分佈式追蹤系統 Dapper ,同時社區也有比較出名的項目 Open Tracing 作為其開源實現,Apache 下面也有類似的項目 Skywalking。一般的 Tracing 系統主要是跟蹤用戶請求在多個服務之間的調用關係,並通過可視化來輔助排查問題。但是 Tracing 系統的跟蹤粒度一般是服務層面,如果我們把 trace_id 和 span_id 也當作標註傳遞給代碼塊進行打樁,那是不是可以在 Tracing 系統的界面上直接下鑽到源碼,聽起來是不是特別酷?

接下來的工作

以上我們只完成了一個非常簡單的原型,距離真正實現睡覺時程序自動查 bug 還有一段路要走,我們計劃對項目持續的進行完善。

接下來,首先要支持並行執行多個測試用例,這樣才能在短時間得到足夠多的實驗樣本,分析結果才能更加準確。另外,要將注入的代碼對程序性能的影響降低到最小,從而應用於更加廣泛的領域,比如性能壓測場景,甚至在生產環境中也能夠開啟。

看到這裡可能你已經按耐不住了,附上項目的完整源碼:

https://github.com/fuzzdebugplatform/fuzz_debug_platform

Welcome to hack!

黃寶靈,PingCAP 前端開發工程師,喜歡 React 和 TypeScript。

滿俊朋, 效率工具工程師, 目前在 PingCAP 從事 Benchmark, Stability 相關工具的研發。

杜沁園,中科大研究生,曾在 PingCAP 實習,從事數據庫測試工具的研發。

韓玉博,中科大研究生,在 Tradeshift 實習,從事前端開發。

【End】

一招生成定製版二次元人臉頭像,還能“模仿”你的表情!

物聯網大變局:LoRa 正式獲批!

C++控制檯打飛機小遊戲 | CSDN 博文精選

☞Twitter 有權刪除去世用戶的賬號嗎?

我收集了12款自動生成器,無聊人士自娛自樂專用

阿里程序員常用的 15 款開發者工具