python 進程內存增長問題, 解決方法和工具


轉載:http://drmingdrmer.github.io/tech/programming/2017/05/06/python-mem.html#pyrasite-%E8%BF%9E%E6%8E%A5%E8%BF%9B%E5%85%A5python%E7%A8%8B%E5%BA%8F

 

表現

運行環境:

# uname -a Linux ** 3.10.0-327.el7.x86_64 #1 SMP Thu Nov 19 22:10:57 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux # python2 --version Python 2.7.5 # cat /etc/*-release CentOS Linux release 7.2.1511 (Core) 

python程序在長時間(較大負載)運行一段時間后, python 進程的系統占用內存持續升高:

# ps aux | grep python2 USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 124910 10.2 0.8 5232084 290952 ? Sl Mar17 220:37 python2 offline.py restart # ~~~~~~ # 290M 內存占用 

這里的python進程在經歷大量請求處理過程中, 內存持續升高, 但最終負載壓力下降之后, 內存個並沒有下降.

解決方法

為了節省讀者時間, 這里先給出結論, 后面再記錄詳細的排查步驟.

我們分幾個步驟逐步定位到問題所在:

  • 首先確定當時程序在做什么, 是否有異常行為.
  • 排除行為異常之后, 查看python的內存使用情況, 是否所有該回收的對象都回收了.
  • 排除垃圾回收等python內部的內存泄漏問題后, 定位到時libc的malloc實現的問題.

而最后的解決方法也很簡單, 直接替換malloc模塊為tcmalloc:

LD_PRELOAD="/usr/lib64/libtcmalloc.so" python x.py 

定位問題過程

gdb-python: 搞清楚python程序在做什么

首先要確定python在做什么, 是不是有正常的大內存消耗任務在運行, 死鎖等異常行為.

這方面可以用gdb來幫忙, 從gdb-7開始, gdb支持用python來實現gdb的擴展. 我們可以像調試c程序那樣, 用gdb對python程序檢查線程, 調用棧等.

而且可以將python代碼和內部的c代碼的調用棧同時打印出來.

這樣對不確定是python代碼問題還是其底層c代碼的問題的時候, 很有幫助.

以下步驟的詳細信息可以參考 debug-with-gdb.


准備gdb

首先安裝python的debuginfo:

# debuginfo-install python-2.7.5-39.el7_2.x86_64 

如果缺少debuginfo, 運行后面的步驟gdb會提示blabla, 按照提示安裝完繼續就好:

Missing separate debuginfos, use: debuginfo-install python-2.7.5-39.el7_2.x86_64

接入gdb

然后我們可以直接用gdb attach到1個python進程, 來查看它的運行狀態:

# gdb python 11122 

attach 之后進入了gdb, 能做的事情就多了. 幾個基本的檢查步驟:


查看線程

(gdb) info threads Id Target Id Frame 206 Thread 0x7febdbfe3700 (LWP 124916) "python2" 0x00007febe9b75413 in select () at ../sysdeps/unix/syscall-template.S:81 205 Thread 0x7febdb7e2700 (LWP 124917) "python2" 0x00007febe9b75413 in select () at ../sysdeps/unix/syscall-template.S:81 204 Thread 0x7febdafe1700 (LWP 124918) "python2" 0x00007febe9b75413 in select () at ../sysdeps/unix/syscall-template.S:81 203 Thread 0x7febda7e0700 (LWP 124919) "python2" 0x00007febe9b7369d in poll () at ../sysdeps/unix/syscall-template.S:81 

一般加鎖死鎖差不多可以在這里看到, 會有線程卡在xx_wait之類的函數上.

之前用這個方法定位了1個python-logging模塊引起的, 在多線程的進程中運行fork, 導致logging的鎖被鎖住后fork到新的進程, 但解鎖線程沒有fork到新進程而造成的死鎖問題.


查看調用棧

如果發現某個線程有問題, 切換到那個線程上, 查看調用棧確定具體的執行步驟, 使用bt 命令:

(gdb) bt #16 0x00007febea8500bd in PyEval_EvalCodeEx (co=<optimized out>, globals=<optimized out>, locals=locals@entry=0x0, args=<optimized out>, argcount=argcount@entry=1, kws=0x38aa668, kwcount=2, defs=0x3282a88, defcount=2, closure=closure@entry=0x0) at /usr/src/debug/Python-2.7.5/Python/ceval.c:3330 ... #19 PyEval_EvalFrameEx ( f=f@entry=Frame 0x38aa4d0, for file t.py, line 647, in run (part_num=2, consumer=<... 

bt 命令不僅可以看到c的調用棧, 還會顯示出python源碼的調用棧, 想上面frame-16是c的, frame-19顯示出在python的源代碼對應哪1行.

如果只查看python的代碼的調用棧, 使用py-bt命令:

(gdb) py-bt #1 <built-in method poll of select.epoll object at remote 0x7febeacc5930> #3 Frame 0x3952450, for file /usr/lib64/python2.7/site-packages/twisted/internet/epollreactor.py, line 379, in doPoll (self=<... l = self._poller.poll(timeout, len(self._selectables)) #7 Frame 0x39502a0, for file /usr/lib64/python2.7/site-packages/twisted/internet/base.py, line 1204, in mainLoop (self=<... 

py-bt顯示出python源碼的調用棧, 調用參數, 以及所在行的代碼.


coredump

如果要進行比較長時間的跟蹤, 最好將python程序的進程信息全部coredump出來, 之后對core文件進行分析, 避免影響正在運行的程序.

(gdb) generate-core-file 

這條命令將當前gdb attach的程序dump到它的運行目錄, 名字為core.<pid>, 然后再用gdb 加載這個core文件, 進行打印堆棧, 查看變量等分析, 無需attach到正在運行的程序:

# gdb python core.<pid> 

其他命令

其他命令可以在gdb輸入py<TAB><TAB> 看到, 和gdb的命令對應, 例如:

(gdb) py py-bt py-list py-print python py-down py-locals py-up python-interactive 
  • py-uppy-down 可以用來移動到python調用站的上一個或下一個frame.
  • py-locals 用來打印局部變量

等等等等. gdb里也可以用help命令查看幫助:

(gdb) help py-print Look up the given python variable name, and print it 

在這次追蹤過程中, 用gdb-python排除了程序邏輯問題. 然后繼續追蹤內存泄漏問題:

pyrasite: 連接進入python程序

pyrasite 是1個可以直接連上一個正在運行的python程序, 打開一個類似ipython的交互終端來運行命令來檢查程序狀態.

這給我們的調試提供了非常大的方便. 簡直神器.

安裝:

# pip install pyrasite ... # pip show pyrasite Name: pyrasite Version: 2.0 Summary: Inject code into a running Python process Home-page: http://pyrasite.com Author: Luke Macken ... 

連接到有問題的程序上, 開始收集信息:

pyrasite-shell <pid>
>>> 

接下來就可以在<pid>的進程里調用任意的python代碼, 來查看進程的狀態.

下面是幾個小公舉(特么的輸入法我是說工具..)可以用來在進程內查看內存狀態的:

psutil 查看python進程狀態

pip install psutil

首先看下python進程占用的系統內存RSS:

pyrasite-shell 11122
>>> import psutil, os >>> psutil.Process(os.getpid()).memory_info().rss 29095232 

基本和ps命令顯示的結果一致

rss the real memory (resident set) size of the process (in 1024 byte units).

guppy 取得內存使用的各種對象占用情況

guppy 可以用來打印出各種對象各占用多少空間, 如果python進程中有沒有釋放的對象, 造成內存占用升高, 通過guppy可以查看出來:

同樣, 以下步驟是在通過pyrasite-shell, attach到目標進程后操作的.

# pip install guppy from guppy import hpy h = hpy() h.heap() # Partition of a set of 48477 objects. Total size = 3265516 bytes. # Index Count % Size % Cumulative % Kind (class / dict of class) # 0 25773 53 1612820 49 1612820 49 str # 1 11699 24 483960 15 2096780 64 tuple # 2 174 0 241584 7 2338364 72 dict of module # 3 3478 7 222592 7 2560956 78 types.CodeType # 4 3296 7 184576 6 2745532 84 function # 5 401 1 175112 5 2920644 89 dict of class # 6 108 0 81888 3 3002532 92 dict (no owner) # 7 114 0 79632 2 3082164 94 dict of type # 8 117 0 51336 2 3133500 96 type # 9 667 1 24012 1 3157512 97 __builtin__.wrapper_descriptor # <76 more rows. Type e.g. '_.more' to view.> h.iso(1,[],{}) # Partition of a set of 3 objects. Total size = 176 bytes. # Index Count % Size % Cumulative % Kind (class / dict of class) # 0 1 33 136 77 136 77 dict (no owner) # 1 1 33 28 16 164 93 list # 2 1 33 12 7 176 100 int 

通過以上步驟, 可以看出並沒有很多python對象占用更大內存.

無法回收的對象

python本身是有垃圾回收的, 但python程序中有種情況是對象無法被垃圾回收掉(uncollectable object), 滿足2個條件:

  • 循環引用
  • 循環引用的鏈上某個對象定義了__del__方法.

官方的說法是, 循環引用的一組對象被gc模塊識別為可回收的, 但需要先調用每個對象上的__del__方法, 才能回收. 但用戶自定義了__del__的對象, gc系統不知道應該先調用環上的哪個__del__. 因此無法回收這類對象.

不能回收的python對象會持續占據內存, 當問題查到這里時我們懷疑有不能被回收的對象導致內存持續升高.

於是我們嘗試列出所有不能回收的對象.

后來確定不是這種問題引起的內存不釋放. 不能回收任然可以通過gc.get_objects()列出來, 並會在gc.collect()調用后被加入到gc.garbage的list里. 但我們沒有發現這類對象的存在.

查找uncollectable的對象:

pyrasite-shell 11122
>>> import gc >>> gc.collect() # first run gc, find out uncollectable object and put them in gc.garbage # output number of object collected >>> gc.garbage # print all uncollectable objects [] # empty 

如果在上面最后一步打印出了任何不能回收的對象, 則需要進一步查找循環引用鏈上在哪個對象上包含__del__方法.

下面是1個例子來演示如何生成不能回收的對象:

不可回收對象的例子 🌰

uncollectible.py

from __future__ import print_function import gc ''' This snippet shows how to create a uncollectible object: It is an object in a cycle reference chain, in which there is an object with __del__ defined. The simpliest is an object that refers to itself and with a __del__ defined. > python uncollectible.py ======= collectible object ======= *** init, nr of referrers: 4 garbage: [] created: collectible: <__main__.One object at 0x102c01090> nr of referrers: 5 delete: *** __del__ called *** after gc, nr of referrers: 4 garbage: [] ======= uncollectible object ======= *** init, nr of referrers: 4 garbage: [] created: uncollectible: <__main__.One object at 0x102c01110> nr of referrers: 5 delete: *** after gc, nr of referrers: 5 garbage: [<__main__.One object at 0x102c01110>] ''' def dd(*msg): for m in msg: print(m, end='') print() class One(object): def __init__(self, collectible): if collectible: self.typ = 'collectible' else: self.typ = 'uncollectible' # Make a reference to it self, to form a reference cycle. # A reference cycle with __del__, makes it uncollectible. self.me = self def __del__(self): dd('*** __del__ called') def test_it(collectible): dd() dd('======= ', ('collectible' if collectible else 'uncollectible'), ' object =======') dd() gc.collect() dd('*** init, nr of referrers: ', len(gc.get_referrers(One))) dd(' garbage: ', gc.garbage) one = One(collectible) dd(' created: ', one.typ, ': ', one) dd(' nr of referrers: ', len(gc.get_referrers(One))) dd(' delete:') del one gc.collect() dd('*** after gc, nr of referrers: ', len(gc.get_referrers(One))) dd(' garbage: ', gc.garbage) if __name__ == "__main__": test_it(collectible=True) test_it(collectible=False) 

上面這段代碼創建了2個對象, 1個可以回收, 1個不能回收, 他們2個都定義了__del__方法, 唯一區別就是是否引用了自己(從而構成了引用環).

如果在這個步驟發現了循環引用, 就要進一步查處哪些引用關系造成了循環引用, 進而破壞掉循環引用, 讓對象變成可以回收的.

objgraph 查找循環引用

# pip install objgraph pyrasite-shell 11122 >>> import objgraph >>> objgraph.show_refs([an_object], filename='sample-graph.png') 

上面的例子中, 將在本地生成一個圖片, 描述由可以由 an_object 引用到的關系圖:

具體參考: objgraph

在這一步我們也沒有找到不能回收的對象, 最后我們懷疑到時glibc的malloc的問題, 用tcmalloc替代glibc默認的malloc后問題得到修復.


Archive


免責聲明!

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



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