ket*_*urn 39 python coredump memory-leaks
我的生产系统偶尔会出现我在开发环境中无法重现的内存泄漏.我使用了Python内存分析器(特别是Heapy)在开发环境中取得了一些成功,但它无法帮助我处理我无法重现的事情,而且我不愿意使用Heapy来检测我们的生产系统,因为它需要一段时间来做它的事情,它的线程远程接口在我们的服务器中不能很好地工作.
我想我想要的是一种转储生产Python进程(或至少是gc.get_objects)的快照的方法,然后离线分析它以查看它在哪里使用内存. 我如何获得这样的python进程的核心转储? 一旦我有了,我该怎么做一些有用的东西?
use*_*478 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)
Run Code Online (Sandbox Code Playgroud)
请注意,我只保存具有__class__属性的对象的数据,因为这些是我关心的唯一对象.应该可以保存完整的对象列表,但是您需要注意选择其他属性.另外,我发现获取每个对象的引用速度非常慢,因此我选择仅保存所指对象.无论如何,在崩溃之后,可以像这样回读得到的pickle数据:
with open("memory.pickle", 'rb') as dump:
objs = cPickle.load(dump)
Run Code Online (Sandbox Code Playgroud)
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)
Run Code Online (Sandbox Code Playgroud)
saa*_*aaj 18
我将从我最近的经验中扩展布雷特的回答。推土机包是 很好的维护,尽管进步,像添加tracemalloc在Python 3.4 STDLIB,其gc.get_objects计数图是我去到的工具来解决内存泄漏。下面我使用dozer > 0.7在撰写本文时尚未发布的内容(好吧,因为我最近在那里贡献了一些修复程序)。
让我们看看一个非平凡的内存泄漏。我将在这里使用Celery 4.4,最终会发现一个导致泄漏的功能(并且因为它是一个错误/功能类型的东西,它可以被称为仅仅是错误配置,由无知引起)。所以有一个 Python 3.6 venv在哪里我pip install celery < 4.5. 并有以下模块。
演示文件
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()
Run Code Online (Sandbox Code Playgroud)
基本上是一个调度一堆子任务的任务。会出什么问题?
我将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 个工人运行节点python demo.py 最后运行这个例子(4) 将在 2 分钟内完成。
然后我使用sqliteviz(预建版本)来可视化procpath有记录器的内容。我把它放在celery.sqlite那里并使用这个查询:
SELECT datetime(ts, 'unixepoch', 'localtime') ts, stat_pid, stat_rss / 256.0 rss
FROM record
Run Code Online (Sandbox Code Playgroud)
在 sqliteviz 中,我创建了一个带有X=ts,的折线图跟踪Y=rss,并添加了 split transform 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
Run Code Online (Sandbox Code Playgroud)
并注入以下内容,这将使用 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()
Run Code Online (Sandbox Code Playgroud)
http://localhost:8000在浏览器中打开应该会看到如下内容:
之后,我python demo.py再次从(4)运行并等待它完成。然后在 Dozer 中,我将“Floor”设置为 5000,这就是我看到的:
与 Celery 相关的两种类型随着子任务的调度而增长:
celery.result.AsyncResultvine.promises.promiseweakref.WeakMethod 具有相同的形状和数字,并且必须由相同的事物引起。
在这一点上,从泄漏类型和趋势来看,您的情况可能已经很清楚了。如果不是,Dozer 对每种类型都有“TRACE”链接,它允许跟踪(例如查看对象的属性)所选对象的引用者 ( gc.get_referrers) 和指涉对象 ( gc.get_referents),并再次继续遍历图形的过程。
但是一张图说一千个字,对吧?所以我将展示如何使用objgraph来呈现所选对象的依赖关系图。
pip install objgraphapt-get install graphviz然后:
python demo.py又从(4)跑了floor=0,filter=AsyncResult然后在 Pyrasite shell 中运行:
objgraph.show_backrefs([objgraph.at(140254427663376)], filename='backref.png')
Run Code Online (Sandbox Code Playgroud)
PNG 文件应包含:
基本上有一些Context对象包含一个list被调用的对象,而该对象_children又包含许多celery.result.AsyncResult, 泄漏的实例。改变Filter=celery.*context在推土机这里是我所看到的:
所以罪魁祸首是celery.app.task.Context。搜索该类型肯定会将您带到Celery 任务页面。在那里快速搜索“孩子”,它是这样说的:
trail = True如果启用,请求将跟踪此任务启动的子任务,并且此信息将与结果 (
result.children)一起发送。
通过设置禁用跟踪trail=False:
@app.task(trail=False)
def task():
for i in range(10_000):
subtask.delay()
time.sleep(0.01)
Run Code Online (Sandbox Code Playgroud)
然后python demo.py再次从 (3) 和(4)重新启动 Celery 节点,显示此内存消耗。
问题解决了!
| 归档时间: |
|
| 查看次数: |
19499 次 |
| 最近记录: |