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
- 表現
- 解決方法
- 定位問題過程
- gdb-python: 搞清楚python程序在做什麽
- 準備gdb
- 接入gdb
- 查看線程
- 查看調用棧
- coredump
- 其他命令
- pyrasite: 連接進入python程序
- psutil 查看python進程狀態
- guppy 取得內存使用的各種對象占用情況
- 無法回收的對象
- 不可回收對象的例子 ??
- objgraph 查找循環引用
- gdb-python: 搞清楚python程序在做什麽
表現
運行環境:
# 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-up
,py-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
- 20 Nov 2017 python 並發subprocess.Popen的坑
- 05 Aug 2017 程序員必讀: 摸清hash表的脾性
- 06 May 2017 python 進程內存增長問題, 解決方法和工具
- 01 Feb 2017 xp的分布式系統系列教程之: Erasure-Code: 工作原理, 數學解釋, 實踐和分析.
- 01 Feb 2017 xp的分布式系統系列教程之: Erasure-Code: 工作原理, 數學解釋, 實踐和分析.
- 11 Nov 2015 可靠分布式系統基礎 Paxos 的直觀解釋
- 28 Jul 2015 socket關閉: close()和shutdown()的差異
- 17 May 2015 隨手改變世界之 git-auto-squash
- 17 Feb 2015 Numbers Programmers Should Know About Hash
- 11 Feb 2015 Vim-tabbar: Simple, stupid and fast tab-bar for VIM
- 24 Jul 2014 1% 慢請求優化
- 31 Jan 2014 Some useful resources
- 31 Jan 2014 jobq.py -- Queue processing engine
python 進程內存增長問題, 解決方法和工具