題記:
這是工作以來困擾我最久的問題。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, 返回
參考
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://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 -ljemallocd、運行時可能會報錯,找不到庫:
此時需要把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