python 內存問題(glibc庫的malloc相關)



題記:

這是工作以來困擾我最久的問題。python 進程內存占用問題。

經過長時間斷斷續續的研究,終於有了一些結果。

項目(IM服務器)中是以C做底層驅動python代碼,主要是用C完成 網絡交互部分。隨着用戶量和用戶數據的增加,服務器進程內存出現持續上升(基本不會下降),導致需要經常重啟服務器,這也是比較危險的信號。

因此便開始了python內存研究之路。


1、業務代碼問題

開始是懷疑業務代碼問題,可能出現了內存泄漏,有一些對象沒有釋放。

於是便檢查一些全局變量,和檢查有沒有循環引用導致對象沒有釋放。

a、全局變量問題:

  發現有一些全局變量(緩存數據)沒有做定時清理,於是便加上一些定時清理機制,和縮短一些定時清理的時間。

  結果:確實清理了不少對象,但是對整體內存占用情況並沒有改善多少。

b、循環引用問題:

  使用python的gc模塊可發現,並沒有循環引用的對象,具體可參考 gc.garbage,gc.collect,gc.disable 等方法。

參考:
http://www.cnblogs.com/Xjng/p/5128269.html
https://www.cnblogs.com/xybaby/p/7491656.html#_label_9

 

結論:內存上漲和業務代碼關系不大


2、python內存管理問題

python 有自己一套緩存池機制,可能是緩存池持續占用沒有釋放導致,於是便開始研究python緩存池機制。

python中一些皆為對象,所有對象都繼承自 type(PyType_Type),但內建類型具體的內存管理不太一樣。

 

a、int對象:

int對象一旦申請了內存空間,就不會再釋放,銷毀的int對象的內存會由一個 free_list 數組管理,等待下次復用。

因此int 對象占用進程的內存空間,總為int對象最多的時候,等到進程結束后才會把內存返回給操作系統。(python3.x則會調用free)

python啟動時會先創建int小整數對象做緩存池用([-5, 256])。

 

b、string對象:

字符串對象釋放后則會調用free。

對於長度為0的字符串會直接返回 nullstring對象,長度唯一的對象則用 characters 數組管理,長度大於1的對象則使用interned機制(interned 字典維護)。

 

c、其他變長對象(list、dict、tuple等):

list有一個大小有80的free_list緩存池,用完后申請釋放直接用malloc和free;

dict和list一樣有一個大小為80的free_list緩存池,機制也一樣;

tuple有長度為[0, 19]的緩存池,長度為0的緩存池只有一個,其余19個緩沖池最多能有2000個,存放長度小於20的元組,長度超過20或對應長度緩沖池滿了則直接用malloc和free;

 

接下來分析Python的緩存池機制:

python內存池主要由 block、pool、arena組成,其中block在代碼中沒有實體代碼,不過block都是8字節對齊;

block由pool管理,所有512字節以下的內存申請會根據申請字節的大小分配不一樣的block,一個pool通常為4K(系統頁大小),一個pool管理着一堆固定大小的內存塊(block);

 * Request in bytes Size of allocated block Size class idx * ---------------------------------------------------------------- * 1-8 8 0 * 9-16 16 1 * 17-24 24 2 * 25-32 32 3 * 33-40 40 4 * 41-48 48 5 * 49-56 56 6 * 57-64 64 7 * 65-72 72 8 * ... ... ... * 497-504 504 62 * 505-512 512 63 * * 0, SMALL_REQUEST_THRESHOLD + 1 and up: routed to the underlying * allocator. */

pool有arena管理,一個arena為256KB,但pyhton申請小內存是不會直接使用arena的,會使用use_pools:

pool = usedpools[size + size]; if pool可用: pool 沒滿, 取一個block返回 pool 滿了, 從下一個pool取一個block返回 否則: 獲取arena, 從里面初始化一個pool, 拿到第一個block, 返回

參考

http://www.wklken.me/category/python.html

 

python 中大部分對象都小於512B,故python主要還是使用內存池;

 

再看下python的垃圾回收機制:

python使用的是gc垃圾回收機制,主要由引用計數(主要), 標記清除, 分代收集(輔助)。

引用計數:每次申請或釋放內存時都增減引用計數,一旦沒有對象指向該引用時就釋放掉;

標記清除:主要解決循環引用問題;

分代收集:划分三代,每代的回收檢測時間不一樣;

 

到這一步,卡了比交久,修改了python源碼,打印了pool和arena的情況(重編譯python后可提現),

arena的大小只占了服務器進程占用內存大小的一小部分,后面發現是python版本比較舊,使用pool的閾值是256B,但是在64位系統上python的dict(程序里比較多的對象)的大小為300+B,就不會使用內存池。

故把python升級到2.7.14(閾值已被修改為512),arena的相對大小比較合理,占了近半進程內存。

這里分析是否合理的方法,就是打印python進程中各對象的數量以及大小,一個方法是利用gc,因為大部分內存申請會經過gc,使用gc.get_objects可以獲取gc管理的所有對象,然后再按類型區分,可獲取不同類型的對象的數量以及大小;另一種方法是直接使用第三方工具guppy,也可打印這些信息。(不過這兩種方法實現不一樣,得到的結果會有一點區別,guppy的分類會更准確)

得到不同對象的數量以及大小后,可以對比arena的情況,看看是否合理了。

 

結論:python內存管理暫時沒發現問題,可能是由其他問題引起。

 

接下來很長一段時間都在糾結:進程剩下的內存哪去了?


3、malloc內存管理問題

回想一下進程內存分配,包括哪些部分:

 

查看 /proc/$PID/status (smaps、maps) 可以看到上圖中對應的 進程的信息,可發現堆分配(和映射區域)是占了絕大部分的內存的。

python的內存申請主要使用的malloc,malloc的實現主要是 brk和mmap,

brk實現是malloc方法的內存池實現,默認小於128KB的內存都經常brk,大於的則由mmap直接想系統申請和釋放。

使用brk的緩存池主要是考慮cpu性能,如果所有內存申請都由mmap管理(直接向系統申請),則會觸發大量的系統調用,導致cpu占用過高。

brk的緩存池就是為了解決這個問題,小內存(小於128KB)的申請和釋放在緩存池進行,減少系統調用減低cpu消耗。

 

使用C函數 mallinfo、malloc_stats、malloc_info等函數可以打印出brk、mmap內存分配、占比的情況。

 

本來閾值128KB是固定的,后來變成動態改變,變為隨峰值的增加而增加,所以大部分對象使用brk申請了。雖然brk方法申請的內存也可以復用和內存緊縮,但是內存緊縮要等到高地址的內存釋放后才能進行,這很容易導致內存不釋放。

於是便使用 mallopt 調整M_MMAP_THREASHOLD 和 M_MMAP_MAX,讓使用brk的閾值固定在128KB,調整后再本地進行測試。可以觀察到mmap內存占比增加了,系統調用次數增加,在申請和釋放大量Python對象后進程內存占用少了20%-30%。

系統調用次數查詢:

可通過以下命令查看缺頁中斷信息 
ps -o majflt,minflt -C <program_name> 
ps -o majflt,minflt -p <pid> 
其中:: majflt 代表 major fault ,指大錯誤;

           minflt 代表 minor fault ,指小錯誤。

這兩個數值表示一個進程自啟動以來所發生的缺頁中斷的次數。 
其中 majflt 與 minflt 的不同是::

        majflt 表示需要讀寫磁盤,可能是內存對應頁面在磁盤中需要load 到物理內存中,也可能是此時物理內存不足,需要淘汰部分物理頁面至磁盤中。

 

 

參考:

https://www.cnblogs.com/dongzhiquan/p/5621906.html

https://paper.seebug.org/255/

https://blog.csdn.net/rebirthme/article/details/50402082

 

 結論:malloc中的brk使用閾值動態調整,雖然降低了cpu負載,但是卻間接增加了內存碎片(brk使用緩存),在庫定后內存使用下降了20%-30%。


 

4、是否還存在其他問題

4.1、理解進程的內存占用情況后,python緩存好像優點占用過高,可以回頭再仔細分析;

4.2、據說使用jemalloc或tcmalloc會有提升,准備試用;

更新至2018-4-16


5、jemalloc

今天測試了jemalloc,現在總結一下:

5.1、安裝使用:

a、下載:https://github.com/jemalloc/jemalloc/releases  jemalloc-5.0.1.tar.bz2

b、安裝:

  ./configure –prefix=/usr/local/jemalloc

  make -j8

  make install

c、編譯時使用:
  gcc -g -c -o 1.o 1.c 
  gcc -g -o 1.out 1.o -L/usr/local/jemalloc/lib -ljemalloc

d、運行時可能會報錯,找不到庫:

此時需要把libjemalloc.so.2 放到可尋找到的路徑中就行

我的做法是:
先查看依賴庫是否找到位置:ldd xxx  (xxx是可運行文件)

把libjemalloc.so.2放到 /lib 下:ln -s /usr/local/jemalloc/lib/libjemalloc.so.2 /lib/libjemalloc.so.2  (我這里使用軟鏈接)

 

在用ldd xxx可以看到依賴庫可發現了

 5.2、使用效果:

同樣條件測試,內存占用變化不大(這里主要關注的是內存使用率不是cpu使用率);

參考:

https://blog.csdn.net/xiaofei_hah0000/article/details/52214592

 

結論:測試使用jemalloc,暫無明顯變化。

更新至2018-4-17


 

安裝tcmalloc,使用靜態庫編譯可執行文件,對比原來的方法、jemalloc方法(使用動態庫,5.0.1版本)和tcmalloc方法(使用靜態庫,2.7版本):

 

開了三個進程,里面定時加載、清除數據(3000個用戶私有數據,加載后內存增加300M),在運行4~5個小時后,tcmalloc 比原來 的方法內存占用少10%~15%,jemalloc方法比tcmalloc方法內存占用少5%~10%;使用jemalloc和tcmalloc都能優化內存碎片的問題,而jemalloc方法的效果會更好些。(tcmalloc、jemalloc源碼直接使用,未改源碼、未調參數情況)

 

更新至2018-5-29


 在正式服務器環境中連續運行一個月,tcmalloc占用內存比原來的ptmalloc 少了 25%,效果顯著!
更新至2018-6-11


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM