Qunar 酒店 NodeJS 覆蓋率收集實踐

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"概述"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般來講我們是通過寫單元測試來驗證程序在執行過程中的代碼覆蓋。覆蓋率結果可以從代碼行、邏輯判斷及函數方法等維度進行分析。得到的數值可以用來檢驗我們對系統功能的實現程度,也可以反饋出程序設計的完整性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然而對於一個沒有維護單元測試的舊系統,想通過收集覆蓋率來檢驗系統功能和熟悉系統結構不是一件容易的事情。爲此我們進行了諸多思考與嘗試最終完成階段性目標。接下來給大家分享下我們的實現方案。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"實現原理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不同語言的覆蓋率收集,在實現機制甚至語法規範層面都大同小異。先將特定的標記按照一定規則插入到代碼行中,這一步我們稱爲“代碼插樁“,然後在執行 case 的過程中收集這些標記的執行情況,最終計算輸出覆蓋率然後格式化輸出結果。大體流程如圖所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/00\/001819bcde15c5ebe056c66d42b8343b.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"源碼編譯是可選的,根據源碼語言特性進行編譯。在 Javascript 的生態中,代碼插樁、覆蓋率統計這些基礎的操作已經有較爲完善的第三方類庫可以使用,我們選用的是 "},{"type":"text","marks":[{"type":"strong"}],"text":"IstanbulJS"},{"type":"text","text":"。在方案設計時爲便於擴展我們沒有直接使用它提供的命令行工具:nyc,而是基於 IstanbulJS 的接口 API 進行了重新設計和開發。開發的過程中我們先後使用過 IstanbulJS API 1.0和2.0兩個版本,雖然在使用方法上有些差別,但功能大體一致。具體可以參考其官網說明,這裏不再贅述 API 的差異性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"工具有了之後接下來的問題就是如何指定 case ?如果是初建項目,功能比較少的情況下手動編寫相對完善的 case 還比較可控。如果面對的是功能不熟悉的系統或者邏輯複雜的舊系統呢?由於我們本次針對的 "},{"type":"text","marks":[{"type":"strong"}],"text":"NodeJS "},{"type":"text","text":"工程是運行在服務端的項目,參考公司內部其它服務端工程 case 的收集方法,最終確定通過日誌回放、定時任務等形式來整理 case 。儘管在數量上會有一定的冗餘,但是相較於補充單元測試來講成本更可控。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"方案細節"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大致瞭解了實現原理之後,接下來把我們具體實踐的方案細節介紹下。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"代碼插樁"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代碼插樁是覆蓋率收集的前提,這一步主要是對現有代碼進行語法層面的分析,並在行內指定的位置加入預設標記。咱們通過一段代碼看下處理前後的對比:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文件:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/97\/97cb00a373fd231339bb3eccd660b597.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"插樁後文件:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/46\/468e286932ce8e26a8d62e952633a0e1.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到代碼當中多了一些額外的邏輯,其實是針對代碼進行不同維度的計數,具體分析這裏先不展開。整個過程有幾點需要注意:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"· 插樁文件的範圍,具體範圍是針對項目的物理文件目錄進行遍歷得出,不會分析代碼行內的文件依賴關係;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"· 是否保留源文件目錄,這個需要從工程化層面考慮,最終取決於後續步驟是否在部署機器上完成?最好能有集中的平臺處理後續步驟,可以提高部署流程的效率,而且去除源碼還能減少 size;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"· 源文件插樁時 path 路徑的設置,這個路徑用於最終回溯源碼生成報告使用。要想提高可移植性最好使用相對路徑,生成報告時源碼路徑可以不受絕對路徑的限制。這一點在 IstanbulJS API 2.0 的版本中很容易指定;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"· 插樁過程的性能,這個涉及到選擇同步還是異步 I\/O,對於文件數量比較多或者體積較大的工程,可以根據實際情況嘗試使用多線程處理(這個要根據實際情況,有的工程文件不超過10個,有的則有上千文件)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"收集數據"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們收集 NodeJS 覆蓋率數據的過程是動態的,服務啓動後不同的外部請求訪問可以實時的更新覆蓋率數據。下面仍以前文的 demo 爲例,通過展開被摺疊的代碼部分一探究竟!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/09\/092e9a37536bd324a413f42180b3ca2e.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"結合插樁部分的代碼,基本可以瞭解這個文件的覆蓋率收集邏輯。程序運行的過程中,不同的請求 case 會執行不同的代碼邏輯,同時會執行覆蓋率計數邏輯,如此反覆執行最終完成覆蓋率的統計。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"順便說下,這些用於覆蓋率計數的節點其實和不同維度的抽象語法樹集合一一對應。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a7\/a723b69b48997648be3682e093c88ad8.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"感興趣的可以深入瞭解下 JS 語法解析相關的知識。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從前文得知每個模塊的數據保持在各自的模塊中,然後掛在全局命名空間上實現所有文件共享。那麼當程序運行的時候如何獲得這些數據呢?我們進行了兩個方向的嘗試:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先是"},{"type":"text","marks":[{"type":"strong"}],"text":"內存共享"},{"type":"text","text":",由於我們的服務一般是通過 PM2 實現的進程守護,所以這個方案是第一時間考慮到的。通過 Message Bus 機制,將不同進程中的覆蓋率數據以消息的形式進行傳遞。數據交互如圖所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ea\/ea18608ad4951ca4591fea4af2fd1fc9.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從內存中讀取、處理數據可以保證極高的實時性,但是也帶來一些問題:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"· 可靠性低,內存中的數據一旦丟失不易找回;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"· 要注意穩定性,主要表現在當多進程服務傳遞的數據集較大時(覆蓋率數據以MB計數很普遍),PM2 內部的消息反序列化消耗很大,消息頻次控制不好極易造成較大的硬件壓力;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"· 耦合性高,功能實現強依賴於 PM2,耦合度太高,無法移植到其它應用場景。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其次是"},{"type":"text","marks":[{"type":"strong"}],"text":"文件存儲"},{"type":"text","text":",把每個進程的內存數據序列化後寫入文件,文件按"},{"type":"text","marks":[{"type":"strong"}],"text":"進程ID"},{"type":"text","text":"命名避免衝突。數據交互變化如圖:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/fa\/fa85caa7eb3792a5f1c90e20888f9532.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文件存儲的方式明顯優化了之前的一些問題:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"· 可靠性變高,即便是服務出現問題,我們依然可以從數據文件中恢復之前的狀態。就如同斷點續傳,效率上的提升顯而易見;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"· 穩定性依然要注意,既然涉及到 I\/O 操作,那麼讀寫文件時都需要經過周密的設計。尤其是寫入頻次和讀取時機以及同步異步的選擇,最常見的一個問題就是頻繁操作一個數據文件導致系統 I\/O 死鎖,瞬間消耗大量資源;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"· 耦合性大大降低,文件存儲的方式擺脫了對進程守護工具的依賴,理論上可以移植到任意的服務上。經過一段時間的項目實踐之後我們決定採用第二種方案!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事實上無論哪種方案還需要一個前提來完成數據收集,就是在服務啓動的時候需要預加載一個指定的模塊。爲了實現任意工程的零成本接入,我們可以採用預設環境變量 "},{"type":"text","marks":[{"type":"strong"}],"text":"NODE_OPTIONS"},{"type":"text","text":" 的方式來引入預加載模塊(因爲這個設置會影響全局,建議服務啓動後移除)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"輸出報告"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這一步是將之前收集的數據,以摘要或者 HTML 等格式化文檔的形式輸出結果。如圖所示是一種格式:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/c5\/c519eaeaa3511bd057f4a35f416d7a45.jpeg","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"報告的輸出格式是多樣性的,生成後可以方便的移動和存儲。一般來講報告改動的場景比較少,如果有需求也可以根據覆蓋率數據集合中的文件行級別數據進行二次開發。報告內容裏有一點需要注意,"},{"type":"text","marks":[{"type":"strong"}],"text":"凡是沒有被服務啓動腳本引用的文件這裏不會輸出索引!"},{"type":"text","text":"這個和插樁不一樣,報告是根據程序運行時,實際執行到的文件產生的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我覺得覆蓋率是工程質量的一個重要指標,無論開發還是測試都需要關注到這一點,尤其是工程面臨比較大的改動的時候。而且從某種意義上講,覆蓋率收集的數據是不是還可以用來做性能監控、代碼優化等,這些都值得去深入挖掘。"}]},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"頭圖:Unsplash"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者:馬濤"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:https:\/\/mp.weixin.qq.com\/s\/w303m03ZO0qlBUFPs6dCvA"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:Qunar 酒店 NodeJS 覆蓋率收集實踐"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來源:Qunar技術沙龍 - 微信公衆號 [ID:QunarTL]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"轉載:著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章