您好,登錄后才能下訂單哦!
protocol buffers[1]是google提供的一種將結構化數據進行序列化和反序列化的方法,其優點是語言中立,平臺中立,可擴展性好,目前在google內部大量用于數據存儲,通訊協議等方面。PB在功能上類似XML,但是序列化后的數據更小,解析更快,使用上更簡單。用戶只要按照proto語法在.proto文件中定義好數據的結構,就可以使用PB提供的工具(protoc)自動生成處理數據的代碼,使用這些代碼就能在程序中方便的通過各種數據流讀寫數據。PB目前支持Java, C++和Python3種語言。另外,PB還提供了很好的向后兼容,即舊版本的程序可以正常處理新版本的數據,新版本的程序也能正常處理舊版本的數據。
筆者在項目的測試過程中,遇到了一個protocal buffer使用不當倒是的模塊內存不斷上漲的問題。這里和大家分享一下問題的定位、分析以及解決過程。
5月,出現問題的模塊(以下成為模塊)內存有泄露的嫌疑,表現為程序在啟動后內存一直在緩慢的上漲。由于該模塊每天都存在重啟的操作,因此沒有帶來較大的影響。
8月,發現線上模塊的內存上漲速度加快。
9月,模塊線上出現內存報警。內存使用量從啟動時的40G,在70小時左右上漲到50G,由于會出現OOM的風險,模塊不得不頻繁重啟。
9月底,模塊的某個版本上線后,由于內存使用量稍有增加,導致程序在啟動后不到24小時內就出現內存報警,線上程序的穩定受到非常大的影響。線上程序回滾,并且停止該模塊的所有功能迭代,直到內存問題解決為止。
模塊是整個系統最核心的模塊,業務的停止迭代對產品的研發效率影響巨大。問題亟需解決!
出現這種問題后,首先要做的就是在線下復現問題,這樣才能更好的定位問題,并且能夠快速的驗證問題修復的效果。但是經過多天的嘗試,在QA的測試環境中,模塊的內存表現情況均與線上不一致。具體表現為:
1)線上模塊的內存一直在上漲,直到機器內存耗盡,模塊重啟;線下模塊的內存在壓力持續若干小時后就趨于穩定,不再上漲。
2)線下環境中,模塊的內存上漲速度沒有線上快。
出現這兩種情況的原因后面再解釋。線上線下表現的不一致給問題的復現和效果驗證帶來了一定的困難。但好在在線下環境中內存使用量依然是上漲的,可以用來定位問題。
小版本間升級點排查。對于這個內存上漲已存在數月的模塊來說,要直接定位問題的難度是非常大的,而且投入會十分巨大。為了使模塊的功能迭代盡快開始,最初我們將定位的焦點聚焦于近期模塊上線的功能排查。寄希望于通過排查這些數量較少的升級,發現對內存的影響。經過2天的排查,沒有任何的發現。
結合該模塊內存的歷史表現和近期升級功能的排查結果,我們認為模塊的內存增長很可能不是泄露,而是某些數據在不斷的調用過程中不斷的增大,從而導致內存不斷的上漲。理論上,經過足夠長的時間后程序的內存使用是可以穩定的。但是受限于程序的物理內存,我們無法觀察到內存穩定的那一刻。
排除數據熱加載導致的內存泄露。在線下環境中,所有的數據文件都沒有更新,因此排除了數據熱加載導致的內存泄露。
各模塊逐步排查。小版本間的升級點排查無果后,我們將排查的方法調整為對程序內的各個子模塊(簡稱module)逐個排除的方法。模塊的module共有13個,如果逐個查,那么消耗的時間會特別多。在實施的過程中采用了二分法進行分析。具體的是某個module為中間點,將該module及以后的模塊去掉,來觀察模塊的內存變化情況。在去掉中間module(含)之后的模塊后,發現內存的上漲速度下降了30%,說明該module之前的模塊存在70%的泄露。通過分析這些模塊,發現某個module (簡稱module A) 的嫌疑最大。
通過UT驗證內存上漲情況。在之前確定主要泄露module的過程中,我們采用在真實環境中進行驗證的方法。這個方法的缺點是時間消耗巨大。啟動程序,觀察都需要消耗很長的時間,一天只能驗證一個版本。為了加快問題的驗證速度,并結合模塊的特點,我們采用了寫UT調用module的方法進行驗證。每次驗證的時間只需要30分鐘,使得問題驗證速度大大加快。
部署監控,定位問題。通過寫UT,我們排除了module A中的兩個子module。并且,我們發現module A單線程的內存上漲速度占線上單線程上漲量的30%,這個地方很可能存在著嚴重的問題。在UT中,我們對這個module中最主要的數據結構merged_data(存儲其包含的子module的特征數據)進行了監控。我們發現,merged_data這個數據結構的內存一直上漲,上漲量與module A整體的量一致。到此,我們確認了merged_data這種類型的結構存在內存上漲。而這種類型的數據結構在模塊中還有很多,我們合理的懷疑整個模塊的內存上漲都是這種情況導致的。
我們先看下module A中merged_data字段的用法。其主要的使用過程如下:
通過上面的代碼,我們可以看到_merged_data字段,在run函數中會向里面插入數據,在reset函數中會調用Clear方法對數據進行清理。結果監控中發現的_merged_data占用的內存空間不斷的變大。通過查閱protobuf clear函數的介紹,我們發現:protobuf的message在執行clear操作時,是不會對其用到的空間進行回收的,只會對數據進行清理。這就導致線程占用的數據越來越大,直到出現理論上的最大數據后,其內存使用量才會保持穩定。
我們可以得到這樣一個結論:protobuf的clear操作適合于清理那些數據量變化不大的數據,對于大小變化較大的數據是不適合的,需要定期(或每次)進行delete操作。
圖1反映出模塊中一些主要protobuf message的變化情況。baseline-old是程序啟動后的內存情況。baseline-new是程序啟動6小時后的內存情況,可以看到所有的數據結構內存占用量都有增加。并且大部分的數據都有大幅的增加。
在了解了問題的原因后,解決方案就比較簡單了。代碼如下:
優化的代碼中,在每次reset的時候,都會調用scoped_ptr的reset操作,reset會delete指針指向的對象,然后用新的地址進行賦值。優化后的效果如圖2所示。newversion-old是優化版本啟動1小時候的數據,newversion-latest是優化版本啟動6小時后的數據。可以看到從絕對值和上漲量上,優化效果都非常明顯。
這個優化方法可能存在一個問題:那就是每次進行reset時,都會對數據進行析構,并重新申請內存,這個操作理論上是非常耗時的。內存優化后,可能會導致程序的CPU消耗增加。具體CPU的變化情況還需要在測試環境中驗證。
優化版本的表現情況如圖3。
圖4顯示的是優化版本與基線版本的CPU IDLE對比情況。可以看到優化版本的CPU IDLE反而更高,CPU占用變少了。一個合理的解釋是:當protobuf的messge數據量非常大時,其clear操作消耗的CPU比小message的析構和構造消耗的總的CPU還要多。
下面是Clear操作的代碼。
通過上面的代碼及圖5可以看出,Clear操作采用了遞歸的方式對Message中的逐個字段都進行了處理。對于基礎類型字段,代碼會對每個字段都設置默認值。對于一個非常長大的Message來說,消耗的CPU會非常多。相對于這種情況,釋放Message的內存并重新申請小的空間,所占用CPU資源反而更少一些。在這個Case中,經常出現Clear操作清理6、7M內存的情況。這樣數據量的Clear操作與釋放Message,再申請200K Message空間比起來,顯然更消耗CPU資源。
protobuf的cache機制
protobuf message的clear()操作是存在cache機制的,它并不會釋放申請的空間,這導致占用的空間越來越大。如果程序中protobuf message占用的空間變化很大,那么最好每次或定期進行清理。這樣可以避免內存不斷的上漲。這也是模塊內存一直上漲的核心問題。
內存監控機制
需要對程序的各個模塊添加合適的監控機制,這樣當某個module的內存占用增加時,我們可以及時發現細節的問題,而不用從頭排查。根據這次的排查經驗,后面會主導在產品代碼中添加線程/module級內存和cpu處理時間的監控,將監控再往”下”做一層。
UT在內存問題定位中的作用
在逐個對module進行排查時,UT驗證比在測試環境中更高效,當然前提是這些module的UT能夠比較容易的寫出來。這也是使用先進框架的一個原因。對于驗證環境代價高昂的模塊,UT驗證的效果更加明顯。
百度MTC是業界領先的移動應用測試服務平臺,為廣大開發者在移動應用測試中面臨的成本、技術和效率問題提供解決方案。同時分享行業領先的百度技術,作者來自百度員工和業界領袖等。
>>如有問題,歡迎與我溝通
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。