標籤:

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: 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.

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 |