一文帶你理解為什么Linux下多線程程序如此消耗虛擬內(nèi)存!(從這四點入手)
最近在進行服務(wù)器內(nèi)存優(yōu)化的時候,發(fā)現(xiàn)一個非常奇妙的問題,我們的認證服務(wù)器(AuthServer)負責跟第三方渠道SDK打交道,由于采用了curl阻塞的方式,所以這里開了128個線程,奇怪的是每次剛啟動的時候占用的虛擬內(nèi)存在2.3G,然后每次處理消息就增加64M,增加到4.4G就不再增加了,由于我們采用預(yù)分配的方式,在線程內(nèi)部根本沒有大塊分內(nèi)存,那么這些內(nèi)存到底是從哪來的呢?讓人百思不得其解。
探索
一開始首先排除掉內(nèi)存泄露,不可能每次都泄露64M內(nèi)存這么巧合,為了證明我的觀點,首先,我使用了valgrind。
然后啟動測試,跑至內(nèi)存不再增加,果然valgrind顯示沒有任何內(nèi)存泄露。反復試驗了很多次,結(jié)果都是這樣。
在多次使用valgrind無果以后,我開始懷疑程序內(nèi)部是不是用到mmap之類的調(diào)用,于是使用strace對mmap,brk等系統(tǒng)函數(shù)的檢測:
其結(jié)果如下:
我檢查了一下trace文件也沒有發(fā)現(xiàn)大量內(nèi)存mmap動作,即便是brk動作引起的內(nèi)存增長也不大。于是感覺人生都沒有方向了,然后懷疑是不是文件緩存把虛擬內(nèi)存占掉了,注釋掉了代碼中所有讀寫日志的代碼,虛擬內(nèi)存依然增加,排除了這個可能。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個人覺得比較好的學習書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。∏?00名進群領(lǐng)取,額外贈送一份價值699的內(nèi)核資料包(含視頻教程、電子書、實戰(zhàn)項目及代碼)? ? ?


靈光一現(xiàn)
后來,我開始減少thread的數(shù)量開始測試,在測試的時候偶然發(fā)現(xiàn)一個很奇怪的現(xiàn)象。那就是如果進程創(chuàng)建了一個線程并且在該線程內(nèi)分配一個很小的內(nèi)存1k,整個進程虛擬內(nèi)存立馬增加64M,然后再分配,內(nèi)存就不增加了。測試代碼如下:
其運行結(jié)果如下圖,剛開始時,進程占用虛擬內(nèi)存14M,輸入0,創(chuàng)建子線程,進程內(nèi)存達到23M,這增加的10M是線程堆棧的大小(查看和設(shè)置線程堆棧大小可用ulimit –s),第一次輸入1,程序分配1k內(nèi)存,整個進程增加64M虛擬內(nèi)存,之后再輸入2,3,各再次分配1k,內(nèi)存均不再變化。

這個結(jié)果讓我欣喜若狂,由于以前學習過谷歌的Tcmalloc,其中每個線程都有自己的緩沖區(qū)來解決多線程內(nèi)存分配的競爭,估計新版的glibc同樣學習了這個技巧,于是查看pmap $(pidof main) 查看內(nèi)存情況,如下:

請注意65404這一行,種種跡象表明,這個再加上它上面那一行(在這里是132)就是增加的那個64M)。后來增加thread的數(shù)量,就會有新增thread數(shù)量相應(yīng)的65404的內(nèi)存塊。
刨根問底
經(jīng)過一番搜索和代碼查看。終于知道了原來是glibc的malloc在這里搗鬼。glibc 版本大于2.11的都會有這個問題:在redhat 的官方文檔上:
總結(jié)一下,glibc為了分配內(nèi)存的性能的問題,使用了很多叫做arena的memory pool,缺省配置在64bit下面是每一個arena為64M,一個進程可以最多有 cores * 8個arena。假設(shè)你的機器是4核的,那么最多可以有4 * 8 = 32個arena,也就是使用32 * 64 = 2048M內(nèi)存。 當然你也可以通過設(shè)置環(huán)境變量來改變arena的數(shù)量.例如export MALLOC_ARENA_MAX=1
hadoop推薦把這個值設(shè)置為4。當然了,既然是多核的機器,而arena的引進是為了解決多線程內(nèi)存分配競爭的問題,那么設(shè)置為cpu核的數(shù)量估計也是一個不錯的選擇。設(shè)置這個值以后最好能對你的程序做一下壓力測試,用以看看改變arena的數(shù)量是否會對程序的性能有影響。
mallopt(M_ARENA_MAX, xxx)如果你打算在程序代碼中來設(shè)置這個東西,那么可以調(diào)用
mallopt(M_ARENA_MAX, xxx)來實現(xiàn),由于我們AuthServer采用了預(yù)分配的方式,在各個線程內(nèi)并沒有分配內(nèi)存,所以不需要這種優(yōu)化,在初始化的時候采用mallopt(M_ARENA_MAX, 1)將其關(guān)掉,設(shè)置為0,表示系統(tǒng)按CPU進行自動設(shè)置。
意外發(fā)現(xiàn)
想到tcmalloc小對象才從線程自己的內(nèi)存池分配,大內(nèi)存仍然從中央分配區(qū)分配,不知道glibc是如何設(shè)計的,于是將上面程序中線程每次分配的內(nèi)存從1k調(diào)整為1M,果然不出所料,再分配完64M后,仍然每次都會增加1M,由此可見,新版 glibc完全借鑒了tcmalloc的思想。

忙了幾天的問題終于解決了,心情大好,通過今天的問題讓我知道,作為一個服務(wù)器程序員,如果不懂編譯器和操作系統(tǒng)內(nèi)核,是完全不合格的,以后要加強這方面的學習。
