我的生产系统偶尔会出现我在开发环境中无法重现的内存泄漏。我使用Python memory profiler(特别是Heapy)在开发环境中取得了一些成功,但它无法帮助我处理我无法重现的事情,而且我不愿意用Heapy来检测我们的生产系统因为它需要一段时间来做它的东西,它的线程远程接口在我们的服务器中不能很好地工作。
我认为我想要的是一种转储生产Python进程(或至少是gc.get_objects)的快照的方法,然后离线分析它以查看它在哪里使用内存。 How do I get a core dump of a python process like this?一旦我有了它,我该怎么做一些有用的东西?
答案 0 :(得分:33)
使用Python的gc
垃圾收集器接口和sys.getsizeof()
,可以转储所有python对象及其大小。这是我在生产中用于解决内存泄漏问题的代码:
rss = psutil.Process(os.getpid()).get_memory_info().rss
# Dump variables if using more than 100MB of memory
if rss > 100 * 1024 * 1024:
memory_dump()
os.abort()
def memory_dump():
dump = open("memory.pickle", 'wb')
xs = []
for obj in gc.get_objects():
i = id(obj)
size = sys.getsizeof(obj, 0)
# referrers = [id(o) for o in gc.get_referrers(obj) if hasattr(o, '__class__')]
referents = [id(o) for o in gc.get_referents(obj) if hasattr(o, '__class__')]
if hasattr(obj, '__class__'):
cls = str(obj.__class__)
xs.append({'id': i, 'class': cls, 'size': size, 'referents': referents})
cPickle.dump(xs, dump)
请注意,我只保存具有__class__
属性的对象的数据,因为这些是我唯一关心的对象。应该可以保存完整的对象列表,但是您需要注意选择其他属性。另外,我发现获取每个对象的引用速度非常慢,因此我选择仅保存所指对象。无论如何,在崩溃之后,可以像这样回读得到的腌制数据:
with open("memory.pickle", 'rb') as dump:
objs = cPickle.load(dump)
Python 3.6版本在这里:
import gc
import sys
import _pickle as cPickle
def memory_dump():
with open("memory.pickle", 'wb') as dump:
xs = []
for obj in gc.get_objects():
i = id(obj)
size = sys.getsizeof(obj, 0)
# referrers = [id(o) for o in gc.get_referrers(obj) if hasattr(o, '__class__')]
referents = [id(o) for o in gc.get_referents(obj) if hasattr(o, '__class__')]
if hasattr(obj, '__class__'):
cls = str(obj.__class__)
xs.append({'id': i, 'class': cls, 'size': size, 'referents': referents})
cPickle.dump(xs, dump)
答案 1 :(得分:14)
我将从最近的经历中进一步了解布雷特的回答。 Dozer package是well maintained,尽管有了进步,例如在Python 3.4中向stdlib添加了tracemalloc
,但它的gc.get_objects
计数表是我解决内存泄漏的首选工具。在下面,我使用dozer > 0.7
,在撰写本文时尚未发布(好吧,因为我最近在此做出了一些修复)。
让我们看看一个不重要的内存泄漏。我将在此处使用Celery 4.4,并将最终发现导致泄漏的功能(由于它是错误/功能,因此可以称为纯粹的配置错误,由无知引起)。因此,在pip install celery < 4.5
中有一个Python 3.6 venv 。并具有以下模块。
demo.py
import time
import celery
redis_dsn = 'redis://localhost'
app = celery.Celery('demo', broker=redis_dsn, backend=redis_dsn)
@app.task
def subtask():
pass
@app.task
def task():
for i in range(10_000):
subtask.delay()
time.sleep(0.01)
if __name__ == '__main__':
task.delay().get()
基本上是一个计划一堆子任务的任务。有什么问题吗?
我将使用procpath
分析Celery节点的内存消耗。 pip install procpath
。我有4个终端:
procpath record -d celery.sqlite -i1 "$..children[?('celery' in @.cmdline)]"
记录Celery节点的进程树统计信息docker run --rm -it -p 6379:6379 redis
运行Redis,它将充当Celery经纪人和结果后端celery -A demo worker --concurrency 2
使用2个worker运行节点python demo.py
最终运行示例(4)将在2分钟内完成。
然后,我使用sqliteviz(pre-built version)来可视化procpath
的记录器。我将celery.sqlite
放到那里并使用以下查询:
SELECT datetime(ts, 'unixepoch', 'localtime') ts, stat_pid, stat_rss / 256.0 rss
FROM record
然后在sqliteviz中,我使用X=ts
,Y=rss
创建折线图跟踪,并添加拆分变换By=stat_pid
。结果图表为:
与内存泄漏斗争的任何人都可能非常熟悉这种形状。
现在是时候dozer
了。我将展示非工具的情况(如果可以的话,您可以用类似的方式检测代码)。要将Dozer服务器注入目标进程,我将使用Pyrasite。有两件事要知道:
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
,这可能会带来安全风险请注意,我:
pip install https://github.com/mgedmin/dozer/archive/3ca74bd8.zip
(我上面提到的就是0.8)pip install pillow
(dozer
用于制图)pip install pyrasite
之后,我可以在目标进程中获取Python shell:
pyrasite-shell 26572
并注入以下内容,它们将使用stdlib的wsgiref
的服务器运行Dozer的WSGI应用程序。
import threading
import wsgiref.simple_server
import dozer
def run_dozer():
app = dozer.Dozer(app=None, path='/')
with wsgiref.simple_server.make_server('', 8000, app) as httpd:
print('Serving Dozer on port 8000...')
httpd.serve_forever()
threading.Thread(target=run_dozer, daemon=True).start()
在浏览器中打开http://localhost:8000
时,应该看到类似以下内容的
然后,我再次从(4)运行python demo.py
并等待其完成。然后在推土机中,将“ Floor”设置为5000,这就是我看到的内容:
与Celery相关的两种类型随着子任务的调度而增长:
celery.result.AsyncResult
vine.promises.promise
weakref.WeakMethod
具有相同的形状和数字,并且必须由同一件事引起。
在这一点上,从泄漏的类型和趋势来看,您的情况可能已经很清楚了。如果不是,则推土机每种类型都具有“ TRACE”链接,该链接允许跟踪(例如,查看对象的属性)所选对象的引荐来源网址(gc.get_referrers
)和引用对象(gc.get_referents
),并继续执行遍历图形的过程
但是一张图片说出一千个字吧?因此,我将展示如何使用objgraph
渲染所选对象的依赖关系图。
pip install objgraph
apt-get install graphviz
然后:
python demo.py
floor=0
,filter=AsyncResult
然后在Pyrasite Shell中运行:
objgraph.show_backrefs([objgraph.at(140254427663376)], filename='backref.png')
PNG文件应包含:
基本上,有一些Context
对象包含一个称为list
的{{1}},而该对象又包含许多_children
实例,这些实例会泄漏。这是我在推土机中更改celery.result.AsyncResult
的结果:
因此,罪魁祸首是Filter=celery.*context
。搜索该类型肯定会导致您进入Celery task page。在其中快速搜索“孩子”,其内容如下:
celery.app.task.Context
如果启用,则请求将跟踪由该任务启动的子任务,并且此信息将与结果(
trail = True
)一起发送。
通过设置result.children
来禁用跟踪,例如:
trail=False
然后从(3)重新启动Celery节点,从(4)重新启动@app.task(trail=False)
def task():
for i in range(10_000):
subtask.delay()
time.sleep(0.01)
,显示此内存消耗。
问题解决了!
答案 2 :(得分:5)
您是否可以在生产站点上记录流量(通过日志),然后在配备python内存调试器的开发服务器上重新播放? (我推荐推土机:http://pypi.python.org/pypi/Dozer)
答案 3 :(得分:2)
我不知道如何转储整个python解释器状态并恢复它。这将是有用的,我会留意这个答案,以防其他人有想法。
如果您知道内存泄漏的位置,可以添加检查对象的refcounts。例如:
x = SomeObject()
... later ...
oldRefCount = sys.getrefcount( x )
suspiciousFunction( x )
if (oldRefCount != sys.getrefcount(x)):
print "Possible memory leak..."
您还可以检查高于某个合适数量的引用计数。为了更进一步,您可以修改python解释器以通过用您自己的宏替换Py_INCREF
和Py_DECREF
宏来进行这些检查。但是,这在生产应用程序中可能有点危险。
这是一篇关于调试这些事情的更多信息的文章。它更适合插件作者,但大多数都适用。
答案 4 :(得分:2)
Make your program dump core,然后使用gdb在足够相似的框上克隆程序实例。有special macros来帮助调试gdb中的python程序,但是如果你可以让你的程序同时serve up a remote shell,你可以继续程序的执行,并用python查询它。
我从来没有这么做过,所以我不是百分百肯定它会起作用,但也许这些指针会有所帮助。
答案 5 :(得分:2)
Meliae看起来很有希望:
这个项目类似于heapy(在'guppy'项目中),试图了解内存的分配方式。
目前,它的主要区别在于它从实际的内存消耗扫描中分离了计算内存消耗的汇总统计等任务。它这样做,因为我经常想弄清楚我的进程中发生了什么,而我的进程消耗了大量的内存(1GB等)。它还允许大大简化扫描程序,因为我在尝试分析python对象内存消耗时没有分配python对象。
答案 6 :(得分:1)