每个API调用Flask App内存泄漏

Bej*_*ian 8 memory-leaks flask

我的Flask API有一个很小的内存泄漏,在许多API调用中导致我的应用程序达到内存限制并崩溃.我一直试图找出为什么一些记忆没有被释放到目前为止没有成功,我相信我确实知道消息来源.我很感激任何帮助!

不幸的是我无法分享代码但是用英语描述它,我的烧瓶应用程序为用户提供了一个API端点,可以执行以下操作(一次性调用):

  1. 根据提供的ID从MongoDB中提取一些数据.
  2. 根据返回的内容,使用python-docx库构建文档对象并将其保存到磁盘
  3. 最后,我将保存到磁盘的内容上传到S3存储桶,然后删除磁盘上的内容

据我所知,使用memory_profiler库,我看到内存使用最多的两个区域是Document对象的初始化和连接/保存到S3(分别为7MB和4.8MB).

我正在做的是监视我的Python进程的内存使用情况,我正在使用psutils打印出在某些关键点使用的rss内存(下面的示例代码).

process = psutil.Process(os.getpid())
mem0 = process.memory_info().rss
print('Memory Usage After Action',mem0/(1024**2),'MB')

## Perform some action

mem1 = process.memory_info().rss
print('Memory Usage After Action',mem1/(1024**2),'MB')
print('Memory Increase After Action',(mem1-mem0)/(1024**2),'MB')
Run Code Online (Sandbox Code Playgroud)

提供控制台图像是在我在本地托管应用程序三次之后调用该应用程序.令人担忧的是,每个顺序API调用似乎都是在最后一次调用离开内存使用量的位置或之上开始并继续添加到它上面应用程序从93MB开始(参见黄色高亮显示)但在第一次调用之后它以103.79结束MB,第二个起始于103.87MB,结束时为105.39MB,第三个起始于105.46Mb,最终为106MB.使用量减少但在100次调用后我仍然看到增量内存使用量.红色和蓝色线显示API调用期间各个点的内存更改.红线在文档构建之后,蓝线在S3上载之后.

请注意,我的测试程序每次都使用相同的参数调用API.

除其他外,我测试了以下内容:

  1. 所以GC.Collect()
  2. 使用'del'显式删除变量/对象引用
  3. 确保mongo连接关闭(因为我使用IBM_Botos3库进行S3连接,我不知道是否有办法明确关闭此连接)
  4. 我没有为每个API调用保存的全局变量(app是唯一的全局变量)

我知道,因为我无法提供代码,所以可能没有什么可以离开这里但是如果没有想法我想知道是否有一种最佳实践方法来处理烧瓶内存使用或在烧瓶功能返回之后清除内存的方法.现在我的烧瓶功能是相对标准的Python函数(因此我希望此函数中的局部变量在之后被垃圾收集).

我正在使用Python 3.6,Flask 0.11.1和pymongo 3.6.1,我的测试现在是在Windows 7机器上,但我的IBM云服务器看到了同样的问题.

abo*_*vel 9

重要的提示

自从提出这个问题后,Sanked Patel 在 PyCon India 2019 上发表了关于如何修复 Flask 内存泄漏的演讲。这是他的策略的总结。

最小示例

假设您有一个简单的无状态 Flask 应用程序,其中只有一个名为“foo”的端点。请注意,其他端点“内存”和“快照”不是原始应用程序的一部分。我们稍后需要它们来查找内存泄漏。

import gc
import os
import tracemalloc

import psutil
from flask import Flask

app = Flask(__name__)
global_var = []
process = psutil.Process(os.getpid())
tracemalloc.start()
s = None


def _get_foo():
    global global_var
    global_var.append([1, "a", 3, True] * 10000)  # This is our (amplified) memory leak
    return {'foo': True}


@app.route('/foo')
def get_foo():
    gc.collect()  # does not help
    return _get_foo()


@app.route('/memory')
def print_memory():
    return {'memory': process.memory_info().rss}


@app.route("/snapshot")
def snap():
    global s
    if not s:
        s = tracemalloc.take_snapshot()
        return "taken snapshot\n"
    else:
        lines = []
        top_stats = tracemalloc.take_snapshot().compare_to(s, 'lineno')
        for stat in top_stats[:5]:
            lines.append(str(stat))
        return "\n".join(lines)


if __name__ == '__main__':
    app.run()
Run Code Online (Sandbox Code Playgroud)

内存泄漏在第 17 行并由注释指示。不幸的是,这种情况很少发生。;)

如您所见,我已尝试通过手动调用垃圾收集来修复内存泄漏,即gc.collect(),在端点“foo”处返回值之前。但这并不能解决问题。

查找内存泄漏

为了确定是否存在内存泄漏,我们多次调用端点“foo”并测量 API 调用前后的内存使用情况。此外,我们将拍摄两个tracemalloc快照。tracemalloc是一个调试工具,用于跟踪 Python 分配的内存块。如果您使用 Python 3.4+,它在标准库中。

以下脚本应阐明该策略:

    import requests

    # Warm up, so you don't measure flask internal memory usage
        for _ in range(10):
        requests.get('http://127.0.0.1:5000/foo')

    # Memory usage before API calls
    resp = requests.get('http://127.0.0.1:5000/memory')
    print(f'Memory before API call {int(resp.json().get("memory"))}')

    # Take first memory usage snapshot
    resp = requests.get('http://127.0.0.1:5000/snapshot')

    # Start some API Calls
    for _ in range(50):
        requests.get('http://127.0.0.1:5000/foo')

    # Memory usage after
    resp = requests.get('http://127.0.0.1:5000/memory')
    print(f'Memory after API call: {int(resp.json().get("memory"))}')

    # Take 2nd snapshot and print result
    resp = requests.get('http://127.0.0.1:5000/snapshot')
    pprint(resp.text)
Run Code Online (Sandbox Code Playgroud)

输出:

Memory before API call 35328000
Memory after API call: 52076544
('.../stackoverflow/flask_memory_leak.py:17: '
 'size=18.3 MiB (+15.3 MiB), count=124 (+100), average=151 KiB\n'
 '...\\lib\\tracemalloc.py:387: '
 'size=536 B (+536 B), count=3 (+3), average=179 B\n'
 '...\\lib\\site-packages\\werkzeug\\wrappers\\base_response.py:190: '
 'size=512 B (+512 B), count=1 (+1), average=512 B\n'
 '...\\lib\\tracemalloc.py:524: '
 'size=504 B (+504 B), count=2 (+2), average=252 B\n'
 '...\\lib\\site-packages\\werkzeug\\datastructures.py:1140: '
 'size=480 B (+480 B), count=1 (+1), average=480 B')
Run Code Online (Sandbox Code Playgroud)

API 调用之前和之后的内存使用量存在很大差异,即内存泄漏。快照端点的第二次调用返回五个最高的内存使用差异。第一个结果在第 17 行正确定位了内存泄漏。

如果内存泄漏隐藏在代码的更深处,您可能需要调整策略。我只了解了 tracemalloc 的功能。但是有了这个策略,你就有了一个很好的起点。


Bej*_*ian 5

几年后我应该更新一下。因为我在评论中发布了,所以除非有人找到更好的解决方案,否则我会将其作为“答案”。

不幸的是,我无法完全解决问题,不得不继续前进,但我能够将增量消耗减少到定期维护和监控将清除剩余内容/如果我们接近极限则通知的程度。

减少调用之间增量内存消耗的最重要的事情是启动另一个线程来处理 Flask 端点内的内存锁定任务,等待线程完成,一旦完成就杀死线程。就像我说的那样,它并没有完全解决问题,并且确实引入了开销,但它将内存泄漏问题减少到我们可以通过上述步骤接受的程度。这是一个创可贴修复。因此,如果存在的话,请随意提出替代/更好/真正的解决方案。

谢谢@above_c_level 为调试 Flask 中的内存泄漏提供的有用提示。