題記:
這是工做以來困擾我最久的問題。python 進程內存佔用問題。html
通過長時間斷斷續續的研究,終於有了一些結果。python
項目(IM服務器)中是以C作底層驅動python代碼,主要是用C完成 網絡交互部分。隨着用戶量和用戶數據的增長,服務器進程內存出現持續上升(基本不會降低),致使須要常常重啓服務器,這也是比較危險的信號。git
所以便開始了python內存研究之路。github
一、業務代碼問題
開始是懷疑業務代碼問題,可能出現了內存泄漏,有一些對象沒有釋放。python3.x
因而便檢查一些全局變量,和檢查有沒有循環引用致使對象沒有釋放。數組
a、全局變量問題:緩存
發現有一些全局變量(緩存數據)沒有作定時清理,因而便加上一些定時清理機制,和縮短一些定時清理的時間。服務器
結果:確實清理了很多對象,可是對總體內存佔用狀況並無改善多少。網絡
b、循環引用問題:函數
使用python的gc模塊可發現,並無循環引用的對象,具體可參考 gc.garbage,gc.collect,gc.disable 等方法。
參考:
http://www.cnblogs.com/Xjng/p/5128269.html
http://www.javashuo.com/article/p-qiqxenep-d.html
結論:內存上漲和業務代碼關係不大
二、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內存管理暫時沒發現問題,多是由其餘問題引發。
接下來很長一段時間都在糾結:進程剩下的內存哪去了?
三、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.一、理解進程的內存佔用狀況後,python緩存好像優勢佔用太高,能夠回頭再仔細分析;
4.二、聽說使用jemalloc或tcmalloc會有提高,準備試用;
更新至2018-4-16
五、jemalloc
今天測試了jemalloc,如今總結一下:
5.一、安裝使用:
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.二、使用效果:
一樣條件測試,內存佔用變化不大(這裏主要關注的是內存使用率不是cpu使用率);
參考:
結論:測試使用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