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
				0
				x7febeacc5930>
				#3
				Frame
				0
				x3952450,
				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
				0
				x39502a0,
				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个例子来演示如何生成不能回收的对象:

不可回收对象的例子 :chestnut:

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 引用到的关系图:

python 进程内存增长问题,解决方法和工具

via:drdrxp

End.

转载请注明来自36大数据(36dsj.com): 36大数据 » python 进程内存增长问题,解决方法和工具

随意打赏

提交建议
微信扫一扫,分享给好友吧。