【开发者成长】阿里代码缺陷检测探索与实践
目前PRECFIX技術已經在阿里巴巴集團內部落地并獲得好評;關于“PRECFIX”技術的論文被國際軟件工程大會(ICSE)收錄。
張昕東(別象) 阿里巴巴 云研發事業部 算法工程師
?
【以下為別象分享實錄】
阿里巴巴在缺陷檢測技術方面遇到的三個挑戰
編碼是DevOps的重要一環,我所在的部門主要負責阿里巴巴集團的代碼托管。在平臺業務的背后,我們建設了一系列智能化能力用來賦能前線業務。依賴底層的代碼圖譜和離線數倉(離線數據倉庫)的數據能力,代碼智能衍生出了缺陷檢測、代碼生成、代碼克隆、代碼安全等能力。今天主要介紹一下我們在缺陷檢測領域的初步探索和實踐。
缺陷檢測和補丁推薦幾十年來一直是軟件工程領域的難題,又是研究者和一線開發者最為關心的問題之一。這里講的缺陷不是網絡漏洞、系統缺陷,而是隱藏在代碼中的缺陷,也就是程序員們戲稱的“八阿哥”(即BUG)。每位開發者都希望有一種智能的缺陷檢測技術來提升代碼質量,避免踩坑。所謂“最迷人的最危險”,如此令人著迷的技術自然有著重重阻礙。
在研究了現有的一些解決方法以及阿里巴巴內部數據集的特征后,我們將缺陷檢測技術在阿里巴巴產品落地上的挑戰歸納為三個方面:
復雜的業務環境
首先阿里巴巴經濟體業務種類繁多,有底層系統中間件的代碼,有物流的代碼,也有安全、人工智能等各個領域的代碼,業務在變,代碼缺陷類型也在變。代碼數據只增不減,代碼缺陷類型捉摸不透,缺陷檢測的困難不言自明。
以Java語言為例,學術界的Java缺陷公開數據集常用的是Defect4J。Defect4J包含6個代碼庫,有將近400個人工確認的缺陷和補丁。這些代碼庫是比較基礎常規的代碼庫,缺陷種類容易劃分,一部分研究者也做了特定缺陷種類的研究,對癥下藥效果尚可,然而換了一種缺陷類型,那么檢測的效果就很難令人滿意了。
在阿里巴巴數據集中,有眾多的缺陷類型難以定義,希望能有一種對缺陷類型泛化能力強的缺陷檢測與補丁推薦的方法,不說“包治百病藥到病除”,但希望能夠適應不同的“病癥”,提高代碼“免疫力”。
有限的輔助資源
第二大挑戰來源于有限的輔助資源,這也是導致許多學術界相關成果無法直接復現利用的原因。
何謂輔助資源,常用者有三:測試用例,缺陷報告,缺陷標簽。
雖然大多數線上代碼都已經達到了很高的測試覆蓋率,但是由于代碼質量參差,有一大部分代碼庫是缺乏測試用例的,我們也缺少足夠的缺陷報告去直接定位缺陷。我們也想嘗試過通過缺陷標簽來學習缺陷模式,然而自動打標簽的方法準確率不高,而人工給如此龐大的數據集打標簽是不太現實的。
產品落地的要求
而最難現實的是第三大挑戰,來自產品的落地要求。
作為一項技術,需要在產品中尋找落地的機會。而真實的落地又對技術有極高的要求:我們構想的主要落地場景是代碼評審中的缺陷靜態掃描及補丁推薦。產品經理說:“檢測過程要高效,盡量不要給誤報,定位缺陷還不夠,補丁方案還得讓用戶知道。”
業界和學術界較為流行的缺陷檢測手段和其局限性
搞研究做創新自然不能固步自封,閉門造車。我先來給大家簡單地介紹一些相關領域的一些現有成果。
主要從缺陷檢測,補丁推薦,以及其他相關的技術應用三個方面做介紹。
缺陷定位技術
關于缺陷定位技術的這些歸納總結主要來自于熊英飛老師(北京大學新體制副教授)的論文。主要方法大類有:基于光譜的缺陷定位,基于突變的缺陷定位,堆棧分析等等。
比如基于光譜的缺陷定位是基于測試用例,通過的測試用例經過的代碼行給予正向分數,失敗的測試用例經過的代碼路徑給予負面分數,類似于光譜的形式將分數較低的一些代碼行歸類為潛在缺陷行。
這些缺陷定位手段大多關注特定缺陷,在定位某些特定缺陷時準確率明顯高于其它缺陷,比如Predicate Switching主要用于檢測條件語句中的bug。
第二個局限在于誤報率較高,以Defect4J數據集測試結果為例,以上方法中準確率最高的是基于光譜的定位,但是TOP1的命中率也只有37%左右。有研究工作將這些各有所長的缺陷定位手段整合起來一起判斷,但誤報率仍然高于50%。
最重要的一點是,上述的缺陷定位手段不提供補丁信息,這一點在實際應用過程中是很致命的,比如基于光譜的定位會返回多個潛在缺陷行,但是沒有明確的修復方案,用戶會比較迷茫。
補丁推薦技術
關于補丁推薦技術,比較具有代表性的研究是Generate-and-validate approach,這種類型下的研究成果大體思路是基于失敗的測試用例,定位到代碼上下文,然后通過隨機替換代碼元素,或基于語義不斷嘗試改變抽象語法樹上的結點,并利用測試用例或其它可驗證信息去驗證修改的結果,直到測試用例或其他驗證手段跑通。
這些補丁生成的方法主要有三大局限,首先是準確率低,主要體現在Overfitting(過度擬合)問題上,意思是生成的修復片段和現實中工程師實際的修復方式不同,有些修復甚至是面向測試用例的修復而不是面向真實缺陷的修復。左圖是某論文中一個Overfitting的例子,“Generate-and-validate”方法將if條件修改為了一個無意義的恒等于true的條件,使得該方法每次安全地return,這樣的修改確實能跑通測試用例,但是對真實的bug是無濟于事的。
第二個明顯的局限是耗時長,消耗的計算資源較多,這種修復方法往往是小時級的,而且他是基于編譯的,需要不斷地測試運行,效率較低。
此外,這種方法對測試用例完備性的要求非常高,它既考驗測試的覆蓋率,又考驗了測試用例設計的合理性。
其它應用技術
還有一些缺陷檢測或補丁推薦技術,可能大家有所耳聞,特別是Facebook和Google的,我也簡單地介紹下。
? Simfix和CBCD主要是基于缺陷報告的補丁生成,利用代碼克隆把缺陷報告和補丁遷移到新代碼上。
? Ubisoft的CLEVER首先基于特征做了commit級別的缺陷預測,對風險較大的一些commit做二次檢測,二次檢測的方法是將缺陷報告根據代碼的dependency聚類起來,然后做抽象語法樹的節點相似度比較,游戲代碼往往有一些相似的缺陷。
? Bugram是將代碼解析成token序列,利用Ngram算法來預測出現某一個token的概率,概率低的token可能是個缺陷點,這種方法當代碼復雜度變高或代碼詞匯數量過大后,效果就急劇下降。
? Infer,Getafix,Sapfix都是Facebook提出的,做的都很不錯。Sapfix主要是針對移動手機的UI做類似Fuzzing的缺陷檢測,Infer主要針對代碼的NPE問題做了偏規則的檢查,所以準確率較高,Getafix是在Infer的檢測結果的基礎之上,對工程師修復的補丁做了模式聚類,將常用的NPE修復模式統計生成出來。
? Tricorder和Findbugs等工具都是比較成熟的代碼檢測器,開發者可以基于這之上定制自己的檢測機制,但比較依賴規則的人工制定。
我們為什么提出PRECFIX方法
經過調研后,我們發現外部的已有技術方法不能完全解決阿里巴巴面臨的挑戰和問題,于是我們提出了PRECFIX方法(Patch Recommendation by Empirically Clustering)。
我們首先在阿里巴巴數據集中復現了一個基于特征工程的commit(代碼提交)級別缺陷風險檢測,這個在之前講Ubisoft的Clever的時候有提過,具體方法是從代碼數據集中根據托管系統Git建立commit父子關系圖,利用改進的SZZ方法對commit進行自動打標簽,然后抽取出一部分特征如下所示,然后利用Xgboost 或者隨機森林(Random Forest)對特征和標簽進行訓練,將模型用于commit風險的檢測。特征主要分為規模、代碼分布、目的、開發者經驗以及文件修改五大維度,共14個子特征。
以SZZ算法作為標簽數據的模型在準確率上有瓶頸。SZZ算法雖然邏輯合理,在公開數據集上有比較好的表現,然而在我們的代碼數據集上,準確率不高,人工觀察了數百條SZZ算法標注的缺陷commit,其中僅有53%是真實的修復行為。“噪聲點”主要來自以下幾種場景:業務型的修復與代碼無關,注解日志的改動,測試類的調整,代碼風格的優化,代碼改動本身是用于“debug”的所以message中帶有“debug”信息等等。而更進一步,僅有37%的修復型代碼改動可以遷移到新的代碼上,這也就意味著有些代碼改動雖然是真實的修復,但是由于改動量過于復雜,或者只與特定環境,特定上下文相關,沒有可借鑒的價值。
通過對標簽數據的細致分析,我們認為自動化的標簽不滿足我們的需求,而且打缺陷標簽對打標者的技術要求較高,對海量的代碼改動歷史打標簽也是不現實的。
我們開始不盲目尋找和復現方法,而是用心去感受和發現開發者在日常開發過程中的修復行為,我們總結了以下幾點:首先,借鑒于SZZ算法,commit message中往往包含了用戶的修復意圖,可以依據commit message來過濾出一部分數據。另外我們在調研SZZ算法數據時,發現75%的修復提交都有這樣的模式:刪除一些缺陷代碼,然后新增一些代碼,比如修改了一個參數,在diff(差異文件)中便是刪除了一行,新增了一行。
還觀察到一個細節就是一個修復的操作往往涉及的文件數不超過兩個,而一些不太規范的commit里面含有大量的文件,即使里面包含了修復行為,也會被稀釋掉,引入不被歡迎的噪音。
同時我們也調研了在代碼評審階段用戶比較關心的缺陷,如故障點、重構點、代碼風格、性能問題等等,很多問題都有重復出現、重復修改的記錄。我們萌生了從海量的提交歷史中挖掘出重復常見的缺陷,防止開發者再次犯錯的想法。
于是我們提出了PRECFIX,Patch Recommendation by Empirically Clustering,也是這次ICSE收錄的論文中描述的方法,后期會在云效產品中使用。其實思路方法比較直接簡潔,主要分為三步,首先從代碼提交數據中提取“缺陷修復對”,然后將相似的“缺陷修復對”聚類,最后對聚類結果進行模板提取,這個缺陷檢測和補丁推薦技術可以用于代碼評審,全庫離線掃描等等。用戶的反饋以及我們人工的審查可以進一步提高模型推薦質量。
實現PRECFIX方法的技術細節
接下來我們聊一下實現PRECFIX方法的技術細節。首先是“缺陷修復對提取”。有朋友可能會有疑問,何為“缺陷修復對”?“缺陷修復對”如何提取?
缺陷修復對提取
這得先從幾個數字講起。SZZ算法利用Blame信息去追溯引入缺陷的commit以及缺陷行,我們觀察發現,有25%的文件的缺陷行源自多個文件,這就意味著利用blame信息去溯源可能會將一個缺陷追溯到兩個源頭,而將一個缺陷的代碼行一分為二很有可能是沒有意義的。
通過blame信息去追溯缺陷代碼行不夠準確,而我們想要的缺陷相關的代碼上下文實際上就是這次修復型commit提交前該文件最后一次提交的內容,我們可以直接通過本次提交的文件和對應的diff還原出本次提交前的文件內容。
我們發現,大多數的修復行為是以方法為單位的,所以我們提出了以方法為單位的“缺陷修復對”提取方法,即將方法體內的diff chunks(差異文件區塊)合并,生成一個缺陷修復對。當然也可以不以方法體為范圍,直接以diff chunk為單位去提取缺陷修復對,這些都各有利弊,如果以diff chunk為單位去提取,那就不需要源代碼信息了,直接解析diff即可。
在提取過程中有個小tips就是將缺陷片段和修復片段歸一化,比如將空格和換行符去掉,然后比較兩者,將相同的缺陷修復對過濾掉,這樣能過濾掉一部分代碼格式修改。
我們發現60%的commit僅包含了1-2個文件,但也存在小部分的不規范commit包含了數十個甚至上百個文件。如之前所說,我們認為一次修復行為關聯的文件往往在三個以下,為了減小噪聲的引入,在提取缺陷修復對的過程中建立過濾機制。其實這個commit文件數量的限制是準確率和召回率的權衡,在真實實踐中我們為了召回率略微放寬了限制,將閾值設為了5。
通過SZZ算法標注的代碼缺陷47%不夠準確。我們沿用了SZZ的利用commit message的數據采集步驟,所以這個缺陷修復對的提取過程還是會存在大量的噪聲難以去除。我們想到了聚類,將常見的缺陷與補丁聚類起來,總結成模板,一些噪聲或沒有借鑒意義的缺陷修復會被自然地過濾。
缺陷修復對聚類
提取完缺陷修復對后,為了盡量減少噪音,并且我們的目的是提取共性缺陷修復記錄,于是采用了聚類的方法,將相似的缺陷修復對聚類在一起,得到一些共性的缺陷。
由于事前無法預測類簇數量,我們沒有使用Kmeans聚類算法,采用了目前比較成熟的基于密度的DBSCAN算法,當然也可以使用pairwise的比較和合并,也就是所有情況都比一遍。
聚類方式上,我們嘗試過很多種,有單獨聚類缺陷片段的,效果不錯,但是修復片段的分析成為了難點,因為同樣一段缺陷片段有不同的修復方案,很難自動化地分析。我們最后嘗試了同時聚類缺陷和修復片段,這樣聚類的方式可以在匹配上缺陷片段時直接給出修復片段,不用再另外考慮如何做修復推薦。
直接使用DBSCAN效率較低,以我們的實驗數據量來講,大概需要70個小時。于是,我們在DBSCAN的基礎上做了一定的優化,主要有圖表上的幾種方式。我們基于MapReduce實現聚類,Mapper階段做缺陷修復對的預處理,Reducer階段跑DBSCAN。第一種優化方式是在Mapper階段采用KDTree或者SimHash算法將比較相近的缺陷修復對分發到一個Reducer中做并行聚類,時間性能大概提升了4倍。類簇損失率主要是和基礎版的DBSCAN算法相比,大概損失了6%。
大多數的缺陷修復對互相之間是沒有任何關聯的,而我們利用代碼克隆技術比較兩個片段又是最耗時的部分,圖上的APISEQ便是我們優化“不必要比較”的方法。我們洞察了這批缺陷修復對數據集,發現幾乎所有的片段都或多或少包含了方法調用,沒有方法調用的片段大概率是一些無意義的噪聲,所以我們可以在聚類比較的過程中先比較兩個片段是否含有相同的API,如果有的話再進行比較,通過這個方法時間性能又提高了四倍。
我們也嘗試了比較新穎的并行DBSCAN算法,速度非常快,但是類簇損失相對較大。這個數據處理的過程是定期的離線計算,頻率較低。最終權衡之下,我們選擇了耗時相對較短,損失率較小的KDTree或APISEQ+KDTREE的聚類方法。
至于聚類過程中的兩個片段的代碼克隆比較方式,我們發現兩種互補的計算方式的結果明顯優于單個計算方式,我們的最佳實踐是“編輯距離”和“Jaccard”的加權平均,因為“編輯距離”能夠捕捉到token(代碼元素)的順序關系,而Jaccard能計算token重合比例。
模版提取與匹配
最后是“模版提取”:為了提升用戶體驗,我們希望將同一類簇的片段聚合起來提取出模板,降低用戶的理解成本。
如上圖所示,同一類簇的兩個片段非常相似,我們先遞歸地使用最長子序列算法,黃色部分為匹配的內容。一些不匹配的內容除了左邊的片段多了一句以外,其他部分實際上是變量名不同,我們將不同的變量名分析提取出來,存儲為“@Para”的格式方便后期匹配。
當掃描到新的缺陷片段時,我們基于模板識別出它的變量名,并直接用新的變量名替換補丁模板中的“@Para”參數,然后自動推薦出帶有新參數的修復建議。
下面來看幾個PRECFIX聚類得到的模板。第一大類是“合理性檢查”,修復片段做了長度的判斷,合理性檢查也包括了空值檢查。
第二個類是“API變更”, API的參數發生了改變,增加了Gpu id,第一個參數的來源也做了修改。API變更還包括了方法名的改動,參數的增刪改,這是非常常見的類型。
還有一個大類是做查看聚類結果之前沒想到的,就是“API封裝”,工程師往往會把功能獨立,經常復用的代碼段封裝起來,減少代碼重復度和維護成本,而且工具類會將方法寫的比較完善,減少開發者在編寫時產生的不必要的失誤。
當然Precfix也不是代碼缺陷的特效藥,只是提供了一種從代碼庫中挖掘缺陷的思路。模板數量和誤報率需要持續地跟進和維護。
PRECFIX方法已經在阿里巴巴集團內部落地,在內部公開庫中掃描出了800多種缺陷類型,3萬多個缺陷,并將結果對用戶進行了訪談,獲得了普遍的好評。后續,該方法也會在“云效”產品中應用,供更多開發者使用。
以上內容來自別象在“云效開發者交流群”中的視頻直播課程,您可以釘釘搜索群號(23362009)入群,收看視頻回放。
關于云效:
云效,企業級一站式DevOps平臺,源于阿里巴巴先進的管理理念和工程實踐,致力于成為數字企業的研發效能引擎!云效提供從“需求 ->開發->測試->發布->運維->運營”端到端的在線協同服務和研發工具,通過人工智能、云原生技術的應用助力開發者提升研發效能,持續交付有效價值。
原文鏈接
本文為云棲社區原創內容,未經允許不得轉載。
總結
以上是生活随笔為你收集整理的【开发者成长】阿里代码缺陷检测探索与实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 万师傅使用云产品,上手简单、开箱即用、省
- 下一篇: 性能为MySQL 10倍!阿里云推出云原