Python 進程內存增長解決方案
作者簡介:
張炎潑(XP)
白山雲科技合伙人兼研發副總裁,綽號XP。
張炎潑先生於2016年加入白山雲科技,主要負責對象存儲研發、數據跨機房分布和修復問題解決等工作。以實現100PB級數據存儲為目標,其帶領團隊完成全網分布存儲系統的設計、實現與部署工作,將數據「冷」「熱」分離,使冷數據成本壓縮至1.2倍冗餘度。
張炎潑先生2006年至2015年,曾就職於新浪,負責Cross-IDC PB級雲存儲服務的架構設計、協作流程制定、代碼規範和實施標準制定及大部分功能實現等工作,支持新浪微博、微盤、視頻、SAE、音樂、軟體下載等新浪內部存儲等業務;2015年至2016年,於美團擔任高級技術專家,設計了跨機房的百PB對象存儲解決方案:設計和實現高並發和高可靠的多副本複製策略,優化Erasure Code降低90%IO開銷。
一 . 表現
運行環境:
# 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進程在大量請求的處理過程中,內存持續升高。負載壓力下降之後,內存並未下降。
# 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內部內存泄漏問題後,則可以定位到libc的malloc實現問題;
確定後可直接替換malloc模塊為tcmalloc:
LD_PRELOAD="/usr/lib64/libtcmalloc.so" python x.py
三 . 問題定位過程解讀
gdb-python:搞清楚python程序在做什麼
首先確定python在做什麼,是否有大內存消耗任務正在運行,或出現死鎖等異常行為。
從gdb-7開始,gdb支持用python實現gdb擴展,可以像調試c程序一樣,用gdb對python程序檢查線程、調用棧等;且可同時列印python代碼和內部c代碼的調用棧。
這對於定位是python代碼問題還是其底層c代碼問題,有很大幫助。
· 準備gdb
首先安裝python的debuginfo:
# debuginfo-install python-2.7.5-39.el7_2.x86_64
如果缺少debuginfo,當運行後續步驟時,gdb會提示,按提示安裝完成即可:
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源代碼的所在行。
如果只查看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
如果要進行長時間跟蹤,最好 coredump下python程序的全部進程信息,之後再分析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可以直接連上一個正在運行的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
...
連接到有問題的python程序,開始收集信息:
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本身帶有垃圾回收,但同時滿足以下2個條件時,python程序中個別對象則無法被回收(uncollectable object) :
? 循環引用
? 循環引用鏈上某對象定義了__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個例子來演示如何生成不能回收的對象:
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.
http://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個不可回收,它們都定義了__del__方法,唯一區別在於是否引用了自己(從而構成了引用環)。
如果在這個步驟發現了循環引用,則需進一步查出具體哪些引用關係造成了循環,進而破壞循環引用,最終讓對象可回收。
objgraph 查找循環引用
# pip install objgraph
pyrasite-shell 11122
>>> import objgraph
>>> objgraph.show_refs([an_object], filename=sample-graph.png)
以上例子中,將在本地生成一個圖片,描述可以由an_object引用到的關係圖:
在這一步我們仍未找到不能回收的對象,排除一切原因後我們推測libc的malloc實現問題。使用tcmalloc替代libc默認的malloc後問題最終得到修復。
推薦閱讀:
※Python unittest單元測試框架 斷言assert
※python中多進程+協程的使用
※黃哥Python 幫網友debug裝飾器代碼
※用python去搞數學建模可行性大不大?
TAG:Python |