Protobuf使用不當導致的程序內存上漲問題

protocol buffers[1]google提供的一種將結構化數據進行序列化和反序列化的方法,其優點是語言中立,平臺中立,可擴展性好,目前在google內部大量用於數據存儲,通訊協議等方面。PB在功能上類似XML,但是序列化後的數據更小,解析更快,使用上更簡單。用戶只要按照proto語法在.proto文件中定義好數據的結構,就可以使用PB提供的工具(protoc)自動生成處理數據的代碼,使用這些代碼就能在程序中方便的通過各種數據流讀寫數據。PB目前支持Java, C++Python3種語言。另外,PB還提供了很好的向後兼容,即舊版本的程序可以正常處理新版本的數據,新版本的程序也能正常處理舊版本的數據。

筆者在項目的測試過程中,遇到了一個protocal buffer使用不當倒是的模塊內存不斷上漲的問題。這裏和大家分享一下問題的定位、分析以及解決過程。

 

1.   問題現象

5月,出現問題的模塊(以下成爲模塊)內存有泄露的嫌疑,表現爲程序在啓動後內存一直在緩慢的上漲。由於該模塊每天都存在重啓的操作,因此沒有帶來較大的影響。

8月,發現線上模塊的內存上漲速度加快。

9月,模塊線上出現內存報警。內存使用量從啓動時的40G,在70小時左右上漲到50G,由於會出現OOM的風險,模塊不得不頻繁重啓。

9月底,模塊的某個版本上線後,由於內存使用量稍有增加,導致程序在啓動後不到24小時內就出現內存報警,線上程序的穩定受到非常大的影響。線上程序回滾,並且停止該模塊的所有功能迭代,直到內存問題解決爲止

模塊是整個系統最核心的模塊,業務的停止迭代對產品的研發效率影響巨大。問題亟需解決!

 

2.   問題復現

出現這種問題後,首先要做的就是在線下復現問題,這樣才能更好的定位問題,並且能夠快速的驗證問題修復的效果。但是經過多天的嘗試,在QA的測試環境中,模塊的內存表現情況均與線上不一致。具體表現爲:

1)線上模塊的內存一直在上漲,直到機器內存耗盡,模塊重啓;線下模塊的內存在壓力持續若干小時後就趨於穩定,不再上漲。

2)線下環境中,模塊的內存上漲速度沒有線上快。

出現這兩種情況的原因後面再解釋。線上線下表現的不一致給問題的復現和效果驗證帶來了一定的困難。但好在在線下環境中內存使用量依然是上漲的,可以用來定位問題。

3.   模塊定位

小版本間升級點排查。對於這個內存上漲已存在數月的模塊來說,要直接定位問題的難度是非常大的,而且投入會十分巨大。爲了使模塊的功能迭代儘快開始,最初我們將定位的焦點聚焦於近期模塊上線的功能排查。寄希望於通過排查這些數量較少的升級,發現對內存的影響。經過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這種類型的結構存在內存上漲。而這種類型的數據結構在模塊中還有很多,我們合理的懷疑整個模塊的內存上漲都是這種情況導致的。

 

4.   問題分析

我們先看下module Amerged_data字段的用法。其主要的使用過程如下

 

通過上面的代碼,我們可以看到_merged_data字段,在run函數中會向裏面插入數據,在reset函數中會調用Clear方法對數據進行清理。結果監控中發現的_merged_data佔用的內存空間不斷的變大。通過查閱protobuf clear函數的介紹,我們發現:protobufmessage在執行clear操作時,是不會對其用到的空間進行回收的,只會對數據進行清理。這就導致線程佔用的數據越來越大,直到出現理論上的最大數據後,其內存使用量纔會保持穩定。

我們可以得到這樣一個結論:protobufclear操作適合於清理那些數據量變化不大的數據,對於大小變化較大的數據是不適合的,需要定期(或每次)進行delete操作。

1反映出模塊中一些主要protobuf message的變化情況。baseline-old是程序啓動後的內存情況。baseline-new是程序啓動6小時後的內存情況,可以看到所有的數據結構內存佔用量都有增加。並且大部分的數據都有大幅的增加。


5.   問題解決

在瞭解了問題的原因後,解決方案就比較簡單了。代碼如下:

優化的代碼中,在每次reset的時候,都會調用scoped_ptrreset操作,resetdelete指針指向的對象,然後用新的地址進行賦值。優化後的效果如圖2所示。newversion-old是優化版本啓動1小時候的數據,newversion-latest是優化版本啓動6小時後的數據。可以看到從絕對值和上漲量上,優化效果都非常明顯。

這個優化方法可能存在一個問題:那就是每次進行reset時,都會對數據進行析構,並重新申請內存,這個操作理論上是非常耗時的。內存優化後,可能會導致程序的CPU消耗增加。具體CPU的變化情況還需要在測試環境中驗證。

 

6.   問題驗證

優化版本的表現情況如圖3

4顯示的是優化版本與基線版本的CPU IDLE對比情況。可以看到優化版本的CPU IDLE反而更高,CPU佔用變少了。一個合理的解釋是:當protobufmessge數據量非常大時,其clear操作消耗的CPU比小message的析構和構造消耗的總的CPU還要多。

下面是Clear操作的代碼。

通過上面的代碼及圖5可以看出,Clear操作採用了遞歸的方式對Message中的逐個字段都進行了處理。對於基礎類型字段,代碼會對每個字段都設置默認值。對於一個非常長大的Message來說,消耗的CPU會非常多。相對於這種情況,釋放Message的內存並重新申請小的空間,所佔用CPU資源反而更少一些。在這個Case中,經常出現Clear操作清理67M內存的情況。這樣數據量的Clear操作與釋放Message,再申請200K Message空間比起來,顯然更消耗CPU資源。


7.   總結

protobufcache機制

protobuf messageclear()操作是存在cache機制的,它並不會釋放申請的空間,這導致佔用的空間越來越大。如果程序中protobuf message佔用的空間變化很大,那麼最好每次或定期進行清理。這樣可以避免內存不斷的上漲。這也是模塊內存一直上漲的核心問題。

內存監控機制

需要對程序的各個模塊添加合適的監控機制,這樣當某個module的內存佔用增加時,我們可以及時發現細節的問題,而不用從頭排查。根據這次的排查經驗,後面會主導在產品代碼中添加線程/module級內存和cpu處理時間的監控,將監控再往做一層。

UT在內存問題定位中的作用

在逐個對module進行排查時,UT驗證比在測試環境中更高效,當然前提是這些moduleUT能夠比較容易的寫出來。這也是使用先進框架的一個原因。對於驗證環境代價高昂的模塊,UT驗證的效果更加明顯。


百度MTC是業界領先的移動應用測試服務平臺,爲廣大開發者在移動應用測試中面臨的成本、技術和效率問題提供解決方案。同時分享行業領先的百度技術,作者來自百度員工和業界領袖等。

>>如有問題,歡迎與我溝通



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章