Google performance Tools (gperftools) 使用心得
gperftools是google開發的一款非常實用的工具集,主要包括:性能優異的malloc free內存分配器tcmalloc;基於tcmalloc的堆內存檢測和內存泄漏分析工具heap-profiler,heap-checker;基於tcmalloc實現的程序CPU性能監測工具cpu-profiler.
上述所說的三種工具在我們服務器進程的性能分析監控,定位內存泄漏,尋找性能熱點,提高malloc free內存分配性能的各個方面上都有非常成功的使用經驗。
1.tcmalloc
之前我們一直使用glibc的malloc free,也是相安無事,后來就出現了內存泄漏的問題,服務器在出現一段高負載的情況后,內存使用率很高,當負載降下來很長一段時間后,進程所占用的內存仍然保持高占用率下不來。一開始就確定是內存泄漏,判斷是libevent收到的網絡包沒處理過來,在接收緩沖區越積越多,使得緩沖區占用的內存也越來越大(因為之前就有這樣的經驗)。我們開始來監控libevent的內存請況,因為libevent可以替換malloc free(google libevent的event_set_mem_functions),我們將默認的malloc free替換為有日志信息的自己定制的版本,然后再啟動服務器,分析結果。日志顯示:在比較大壓力下,libevent使用的內存確實很多,基本符合top顯示出的內存使用情況,但當壓力過去了,日志明顯顯示libevent已經釋放掉了內存,可是top顯示的內存使用還是很高。free掉的內存沒有還回給系統,而且過了一兩天也還是現狀,我們都知道malloc free是用了內存池的,但是把長時間沒使用的大塊內存還給系統,不是應該是內存池該做的事嗎? 既然這只是glibc的malloc free實現,也許他的內存分配算法就是這樣,可能有些其他原因也不一定,不了解其實現和原理,不好妄作評論。因為其實以前就有同事加上了tcmalloc,所以馬上想換換tcmalloc試試,這也是聽說過tcmalloc是最快的malloc free實現,google內部許多工程都是使用它做底層雲雲。
換上tcmalloc很容易,根據需要以動態庫或靜態庫的形式鏈接進你的可執行程序中即可(gcc ... -o myprogram -ltcmalloc)。使用tcmalloc后按照上述的情形再跑了一次,發現在同樣的壓力下,cpu占用率下降了10%~20%左右,甚是驚喜。但是在壓力過去很長一段時間后,同樣的問題出現了,內存占用率還是居高不下。這個非常困惑我,於是去網上去看官方文檔,看其實現原理和特性。然后就在在文檔發現了--把沒用的內存還回給系統原來是可以控制的行為。
以下完全參考官方文檔http://google-perftools.googlecode.com/svn/trunk/doc/tcmalloc.html:
在tcmalloc中控制把未使用的內存還回給系統,主要是兩個方法,一種是修改環境變量TCMALLOC_RELEASE_RATE,這個值代表把未使用的內存還回給系統的速度吧,取值在0~10,值越高返還的速率越高,值為0表示從不返還,默認是1.0,其實是一個比較低的速率,所以在之前的表現中,就是很長一段時間內存降不下來。這個值的改變可以在程序運行期間就起作用。這個環境變量也有對應的函數調用SetMemoryReleaseRate,可以在程序代碼中調用。還有一種是一個函數調用:
MallocExtension::instance()->ReleaseFreeMemory();
這個函數如其名,把未使用的內存全部返還給系統。我按照第二種方法,讓運行着的服務器程序動態執行我的命令,這個命令就是運行上面的函數調用(我們服務器有套機制,能夠在運行時執行一些我們自定義的命令,實現方式有很多,如信號),結果top上顯示的內存占用率立馬降下來很多,頓時讓我覺得世界都清凈了。
后記:我估計glibc的malloc free也有類似控制,只不過現階段我並沒有時間和很大興趣去關注了。
2.heap-profiler, heap-checker
這兩個工具其實挺像的,heap-checker專門檢測內存泄漏,heap-profiler則是內存監控器,可以隨時知道當前內存使用情況(程序中內存使用熱點),當然也能檢測內存泄漏。我們工作中一直是使用heap-profiler,實時監控程序的內存使用情況。文檔在此:http://google-perftools.googlecode.com/svn/trunk/doc/heapprofile.html。
heap-profiler是基於tcmalloc的,正規開啟它的方法是在代碼中調用 HeapProfilerStart方法,參數是profile文件前綴名,相應的關閉則需調用 HeapProfilerStop。前面有介紹過我們的服務器有運行時執行自定義命令的機制,我們把兩個命令MemProfilerStart,MemProfilerStop實現成調用上述相應的開啟/關閉heap-profiler的API,這樣我們可以在服務器運行時,隨時開啟和關閉heap-profiler,非常方便的查看當前程序內存使用情況。
heap-profiler會在每當一定量的內存被新申請分配出來時或者當一段固定時間過去后,輸出含有當前內存使用情況的profile文件,文件名類似這種:
<prefix>.0000.heap
<prefix>.0001.heap
<prefix>.0002.heap
...
prefix是前文所說的profile數據文件的前綴,如果並沒有指定成絕對路徑則會在你的程序的工作目錄中生成,這些文件可以被一個腳本工具pprof解析輸出成各種可視的數據格式文件,pprof使用了dot語言繪圖,需要安裝graphviz。pprof也是下文解析CPU profile文件的工具,pprof工具可以把profile信息輸出成多種格式,如pdf,png,txt等,如果是以圖的形式顯示,則是根據調用堆棧的有向圖,像下圖這樣:
這個圖里面每個節點代表一個函數調用,比如GFS_MasterChunkTableUpdateState節點,176.2(17%) of 729.9(70%) 大致表示這個函數本身自己直接消耗了17%的內存,加上子調用共消耗了70%的內存,然后每條邊則顯示每個子調用花費了多少內存。因為我們的linux系統並未安裝圖形界面,通常都是直接生成了txt文件:
% pprof --text gfs_master /tmp/profile.0100.heap
255.6 24.7% 24.7% 255.6 24.7% GFS_MasterChunk::AddServer
184.6 17.8% 42.5% 298.8 28.8% GFS_MasterChunkTable::Create
176.2 17.0% 59.5% 729.9 70.5% GFS_MasterChunkTable::UpdateState
169.8 16.4% 75.9% 169.8 16.4% PendingClone::PendingClone
76.3 7.4% 83.3% 76.3 7.4% __default_alloc_template::_S_chunk_alloc
49.5 4.8% 88.0% 49.5 4.8% hashtable::resize
第一列代表這個函數調用本身直接使用了多少內存,第二列表示第一列的百分比,第三列是從第一行到當前行的所有第二列之和,第四列表示這個函數調用自己直接使用加上所有子調用使用的內存總和,第五列是第四列的百分比。基本上只要知道這些,就能很好的掌握每一時刻程序運行內存使用情況了,並且對比不同時段的不同profile數據,可以分析出內存走向,進而定位熱點和泄漏。
在我們的實踐中,也經常發現一些環境變量非常有用:
HEAP_PROFILE_ALLOCATION_INTERVAL:上文說每當一定量的內存被新申請分配出來時,就會輸出profile文件,這個變量值就是控制多少字節,默認是(1024*1024*1024)1GB,粒度相對比較大,常常會被我們調整為幾百MB甚至更小。
HEAP_PROFILE_MMAP:有時候程序申請內存的方式是通過mmap,sbrk系統調用而不是malloc free,如果想profile這些內存,可以開啟這個變量,默認是false。我們工程代碼中就有些調用了mmap申請大塊內存的地方,開啟這個環境變量,能更准確跟蹤內存走向。
HEAP_PROFILE_MMAP_ONLY:如其名,只profile mmap,sbrk申請的內存。
更改這些環境變量是可以在啟動命令中完成的:
% env HEAPPROFILE=/tmp/mybin.hprof /usr/local/bin/my_binary_compiled_with_tcmalloc
3.CPU profiler
CPU profiler的使用方式類似heap-profiler,區別就是要在構建你的程序時不僅要鏈接-ltcmalloc還要鏈接-lprofiler。它也是需要一個函數調用(ProfilerStart)來開啟,和一個函數調用(ProfilerStop)來關閉,調用這些函數需要include <google/profiler.h>。當然我們還是通過內部的自定義指令機制來運行時控制profiler的開啟和關閉。ProfilerStart接受輸出文件名作參數,ProfilerStop關閉profiler時同時輸出含有profile信息的文件,這些信息也是要pprof解析后可以生成各種可讀格式:
% pprof /bin/ls ls.prof
Enters "interactive" mode
% pprof --text /bin/ls ls.prof
Outputs one line per procedure
% pprof --gv /bin/ls ls.prof
Displays annotated call-graph via 'gv'
% pprof --gv --focus=Mutex /bin/ls ls.prof
Restricts to code paths including a .*Mutex.* entry
% pprof --gv --focus=Mutex --ignore=string /bin/ls ls.prof
Code paths including Mutex but not string
% pprof --list=getdir /bin/ls ls.prof
(Per-line) annotated source listing for getdir()
% pprof --disasm=getdir /bin/ls ls.prof
(Per-PC) annotated disassembly for getdir()
% pprof --text localhost:1234
Outputs one line per procedure for localhost:1234
% pprof --callgrind /bin/ls ls.prof
Outputs the call information in callgrind format
我們實踐中用的最多的是導出成pdf格式,非常直觀,描述文檔在此http://google-perftools.googlecode.com/svn/trunk/doc/heapprofile.html,因為這個有向圖代表的意義類似之前在heap-profiler中所描述的圖,所以不再多着筆墨。實踐證實CPU profiler效果很好,為我們一次次定位了性能熱點,為此讓人好奇其實現原理。CPU profiler的原理類似許多其他proflier,都是對運行着的程序定時采樣,最后根據每次記錄的堆棧頻度導出采樣信息。網上有人這樣解釋到:相當於用gdb attach一個正在運行的進程,然后每隔一段時間中斷程序打印堆棧,當然耗時最多的調用最頻繁的堆棧是最常被打印出來的。有稍微看過gperftools這方面的實現,大致就是注冊一個定時觸發的信號(SIGPROF)處理函數,在此函數中獲取當前堆棧信息,通過hash算法以此做hash表的key,放入樣本統計hash table中,如果hash table中已經有同樣的堆棧key了,value就加1,這樣當采樣結束,就把hash table的統計信息導出到文件供pprof程序解析,從而得到真正直觀的profile信息。
最后,如果根本不想使用任何profiler功能,只想使用其快速的tcmalloc,可以直接鏈接-ltcmalloc_minimal。