一文搞定 Perf 和 Gpertools
本文轉(zhuǎn)載自微信公眾號「小姐姐味道」,作者小姐姐養(yǎng)的狗。轉(zhuǎn)載本文請聯(lián)系小姐姐味道公眾號。
在Linux下開發(fā)是幸福的,尤其是在發(fā)生問題的時候。永遠(yuǎn)忘不了在Windows下應(yīng)用發(fā)生問題時那種無助的感覺。
Java提供了非常多的工具來應(yīng)對故障排查、性能分析,比如jstat、jmap、jmc等。但大多數(shù)情況下,資源的瓶頸在操作系統(tǒng)上。如果宿主機并不能正常工作,那么在之上進行各種Java的應(yīng)用分析就會變得沒有意義。皮之不存毛將焉附,就是這個道理。
要做性能分析,Linux下有一個非常好用的工具,叫做perf。幾乎每個發(fā)行版都有它的安裝包。perf誕生于2009年,是一個內(nèi)核級的工具;另外,還有一個工具叫做gperftools,是google的產(chǎn)品,是一個應(yīng)用級別的產(chǎn)品。
雖然它們都有perf字樣,但使用場景和處理的問題也是不一樣的。
1. perf:CPU暴漲問題排查
顧名思義,perf是做性能分析用的。perf支持兩種模式,計算模式和采樣模式。比如,perf stat使用的是計算模式,而perf record采用的是采樣模式。拿采樣來說,它的原理是這樣的:每隔一個固定的時間,產(chǎn)生一個中斷,然后統(tǒng)計對應(yīng)的pid和函數(shù)。采樣就預(yù)示著與實際運行情況并不能保持一致,但如果一個函數(shù)運行的時間越長,被時鐘中斷的機會就越大。鑒于perf最終顯示的是統(tǒng)計值,所以它的測量結(jié)果是高度可信的。
通過包管理工具可以很容易的獲取perf。比如在centos下,直接通過yum install perf進行安裝。perf提供了非常多的命令,我們可以直接輸入perf輸出這些選項。
Perf的功能非常多,常用的有perf list、perf stat、perf top、perf record、perf report等。下面以幾個常見的例子,來說明它的應(yīng)用場景。
使用下面的腳本,使得某一核CPU使用飆升到100%。
- cat /dev/zero > /dev/null
使用下面腳本,耗光CPU資源。(先取得cpu的核數(shù),然后循環(huán)生成任務(wù))。這段腳本將數(shù)據(jù)輸出到/dev/null,所以只占用CPU資源,沒有占用任何I/O資源。
- for i in `seq 1 $(cat /proc/cpuinfo |grep "physical id" |wc -l)`;
- do dd if=/dev/zero of=/dev/null &
- done
我們就拿下面的這個腳本來說明情況。從top的截圖中,可以看到sy和ni的占用,達到了100%,我的腳本起作用了!(溫馨提示:執(zhí)行完腳本后,如果你想要殺死這些進程,除了重啟之外,還可以直接通過ps找到相應(yīng)的進程,然后使用kill命令終止)。
接下來使用record指令來錄制CPU的使用情況。從上面的描述可以得知,統(tǒng)計結(jié)果是采樣結(jié)果。
- <># perf record -a -e cycles -o perf.perf -g sleep 10
- [ perf record: Woken up 55 times to write data ]
- [ perf record: Captured and wrote 14.282 MB perf.perf (160302 samples) ]
程序?qū)\行10秒鐘,然后將采樣結(jié)果輸出到perf.perf文件中。通過report命令可以展示統(tǒng)計結(jié)果。
- perf report -i perf.perf
可以看到大多數(shù)cpu的損耗都是在dd命令上,甚至里面的調(diào)用樹,也能夠清晰的展示。這在調(diào)試一些c++語言寫的程序,或者調(diào)試jvm的一些內(nèi)部行為時,非常的有用,因為它可以直接跟蹤到系統(tǒng)調(diào)用層面。但有些細(xì)節(jié),如果對Linux內(nèi)核不是非常了解的話,下手就比較困難。所以通常情況下,我們只能通常粗略的定位到有問題的模塊,然后再深入進行調(diào)試。
perf還可以通過指定進程號進行性能追蹤,來獲取性能數(shù)據(jù)。
- perf top -g -p 2343
2. 示例代碼
了解到perf的基本用法,我們拿一個經(jīng)常實際遇到的例子來說明一下perf的使用。堆外內(nèi)存是通過JNI等類庫進行調(diào)用所產(chǎn)生的內(nèi)存,在實際排查中定位非常困難。傳統(tǒng)的工具包括JMC,都不能快速有效的找到問題的元兇。黔驢技窮的時候,一般就到了perf上場的時候了。
為了演示這個過程,我特意做了一段非常精致的JAVA代碼。代碼片段較長,可以訪問下面的gist鏈接,下面只說明關(guān)鍵代碼。這段代碼是典型的堆外內(nèi)存泄露問題。
- https://gist.github.com/lycying/70ff3897d8516011c7ffc702aa0d03c2
使用com.sun.net.httpserver.HttpServer自帶的簡易server,可以非常容的構(gòu)造一個服務(wù)器,我們可以通過請求去改變一些應(yīng)用行為。
使用下面的JVM參數(shù)啟動這段代碼。
- java -Xmx1G -Xmn1G \
- -XX:+AlwaysPreTouch \
- -XX:MaxMetaspaceSize=10M \
- -XX:MaxDirectMemorySize=10M \
- -XX:NativeMemoryTracking=detail LeakExample
程序?qū)⑷藶閯?chuàng)建一個停頓狀態(tài),具體測試步驟如下。
- 程序運行一小段時間,內(nèi)存使用率迅速達到60%時,這時候程序?qū)⒆詣訏炱?/li>
- 啟動perf進行采樣,相當(dāng)于問題發(fā)生中進行切入 (perf record -g -p $pid)
- 訪問http://localhost:8888 端口,將會將內(nèi)存閾值提高到85%,內(nèi)存會迅速達到這個狀態(tài)
- 停止采樣,將生成perf數(shù)據(jù)
- 使用perf report進行分析 (perf report -i perf.data )
這將會得到下面的一張圖。
但我們不能從中得到有用的信息。是方法錯了么?是的。采樣內(nèi)存,要使用perf mem record指令,但是這個指令在大多數(shù)機器上都不能工作,得到的信息也有限。
perf記錄的是CPU的性能數(shù)據(jù),這里要特別說明一下。只要是使用率上5%的,我一般都會關(guān)注。一般情況下,占用的cpu時間片多,證明使用內(nèi)存也比較多。但事情總有例外的時候,比如頻繁申請1byte的方法塊,和一次性申請1MB的方法塊,并不能同日而語。
所以perf能不能發(fā)現(xiàn)內(nèi)存問題,要看運氣。
3. gperftools:找到堆外內(nèi)存的元兇
要找到內(nèi)存問題,要使用google的gperftools,我們主要用到它的 Heap Profiler,功能很強大。https://github.com/gperftools/gperftools
它的啟動方式有點特別,安裝成功之后,你只需要輸出兩個環(huán)境變量即可。
- mkdir -p /opt/test
- export LD_PRELOAD=/usr/lib64/libtcmalloc.so
- export HEAPPROFILE=/opt/test/heap
在同一個終端,再次啟動我們的應(yīng)用程序,可以看到內(nèi)存申請動作都被記錄到了 opt 目錄下的 test 目錄。
接下來,我們就可以使用 pprof 命令分析這些文件。
- cd /opt/test
- pprof -text *heap | head -n 200
使用這個工具,能夠一眼追蹤到申請內(nèi)存最多的函數(shù)。Java_java_util_zip_Inflater_init 這個函數(shù)立馬就被發(fā)現(xiàn)了。
這就是我們模擬內(nèi)存泄漏的整個過程,到此問題就解決了。
GZIPInputStream 使用 Inflater 申請堆外內(nèi)存、Deflater 釋放內(nèi)存,調(diào)用 close() 方法來主動釋放。如果忘記關(guān)閉,Inflater 對象的生命會延續(xù)到下一次 GC,有一點類似堆內(nèi)的弱引用。在此過程中,堆外內(nèi)存會一直增長。
問題發(fā)生在我們的decompress函數(shù)上。它在使用的時候,忘記關(guān)閉流了。我們可以看一下異常和正常情況的區(qū)別。
這段是忘了關(guān)閉流的函數(shù)。這種情況在編碼中經(jīng)常會發(fā)生。
- public static String decompress(byte[] input) throws Exception {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- copy(new GZIPInputStream(new ByteArrayInputStream(input)), out);
- return new String(out.toByteArray());
- }
下面是修改后正常的函數(shù)。
- public static String decompress(byte[] input) throws Exception {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(input));
- try {
- copy(gzip, out);
- return new String(out.toByteArray());
- }finally {
- try{ gzip.close(); }catch (Exception ex){}
- try{ out.close(); }catch (Exception ex){}
- }
- }
4. 題外話
使用pprof,還可以輸出圖形化的分析報告,需要安裝圖形生成工具graphviz,可以說是非常nice了。
另外不得不提的一點是,perf和gperftools對性能的影響,雖然不是特別大,但也盡量不要在線上環(huán)境使用它們。據(jù)我實際使用的經(jīng)驗判斷,這個性能損耗率大概在30%左右。如果你的問題可以復(fù)現(xiàn),通過常規(guī)手法又無法解決的情況下,可以使用這些工具去分析。比如你的應(yīng)用實例有5個,完全可以分20%的流量到專用的機器上,把profile打開,相信你會很快定位到問題。
作者簡介:小姐姐味道 (xjjdog),一個不允許程序員走彎路的公眾號。聚焦基礎(chǔ)架構(gòu)和Linux。十年架構(gòu),日百億流量,與你探討高并發(fā)世界,給你不一樣的味道。我的個人微信xjjdog0,歡迎添加好友,進一步交流。