每次API调用时,Flask App内存泄漏

时间:2018-04-23 23:21:41

标签: flask memory-leaks

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

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

  1. 根据提供的ID从MongoDB中提取一些数据。
  2. 根据返回的内容,使用python-docx库构建文档对象并将其保存到磁盘
  3. 最后,我将保存到磁盘的内容上传到S3存储桶,然后删除磁盘上的内容
  4. 据我所知,使用memory_profiler library,我看到内存使用最多的两个区域是Document对象的初始化和连接/保存到S3(分别为7MB和4.8MB)。 / p>

    我正在做的是监视我的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')
    

    console image provided是在我在本地托管应用程序后三次调用应用程序之后。 有疑问的是,每个顺序API调用似乎都是在最后一次调用离开内存使用量并且继续添加到其上的位置或之上开始应用程序从93MB开始(参见黄色高光)但是之后第一个电话以103.79MB结束,第二个电话以103.87MB开始,以105.39MB结束,第三个以105.46Mb开始,以106MB结束。使用量减少但在100次调用后我仍然看到增量内存使用量。红色和蓝色线显示API调用期间各个点的内存更改。红线在文档构建之后,蓝线在S3上载之后。

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

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

    1. GC.Collect的()
    2. 使用'del'
    3. 显式删除变量/对象引用
    4. 确保mongo连接已关闭(因为我使用IBM_Botos3 library进行S3连接我不知道是否有办法明确关闭此连接)
    5. 没有全局变量可以保存到每个API调用(应用程序是唯一的全局变量)
    6. 我知道,因为我无法提供代码,所以可能没有什么可以离开这里但是如果没有想法我想知道是否有一种最佳实践方法来处理烧瓶内存使用或者在烧瓶后清除内存的方法函数返回一些东西现在我的烧瓶功能是相对标准的Python函数(因此我希望此函数中的局部变量在之后被垃圾收集)。

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

3 个答案:

答案 0 :(得分:5)

重要提示

由于提出了此问题,Sanked Patel gave a talk at 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()

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

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

发现内存泄漏

要确定是否存在内存泄漏,我们多次调用端点“ 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)

输出:

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')

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

如果内存泄漏在代码中更深处隐藏,则可能必须调整策略。我只涉及了tracemalloc的功能。但是,采用这种策略,您将有一个很好的起点。

答案 1 :(得分:1)

这种行为只发生在我在开发环境中的调试模式下,但是当我使用女服务员作为 Web 服务器时,我的 Flask 应用程序运行良好,没有内存泄漏。

这是我的应用程序。服务员要在虚拟环境中运行。

import sys
import os
import site
from waitress import serve
dir_path = os.path.dirname(__file__)
sys.path.append(os.path.abspath(dir_path))
venv_packages =  os.path.abspath(os.path.join(dir_path, 'venv', 'lib', 'site-packages'))
sys.path.append(venv_packages)
site.addsitedir(venv_packages)
from dotenv import load_dotenv
dotenv_path = os.path.join(os.path.dirname(__file__), '.env')
load_dotenv(dotenv_path)
from settings import API_HOST, API_PORT
from app import app as application
serve(application, host=API_HOST, port=API_PORT)

从终端(Mac 或 Linux)运行它:

. venv/bin/activate
pip install waitress
python app.waitress

从 Windows 运行它:

py -3 -m pip install waitress
py app.waitress

环境:

Python 3.7.9
waitress 1.4.1
Flask 1.1.2
Flask-Cors 3.0.10
Flask-JWT-Extended 3.25.0
python-dotenv 0.10.3

答案 2 :(得分:0)

几年后,我应该更新一下。由于我在评论中发表了文章,除非有人找到更好的解决方案,否则我将使其成为“答案”。

不幸的是,我无法完全解决问题,因此不得不继续前进,但是我能够将增量消耗减少到一定程度,以至于定期维护和监视可以清除剩余的电量/如果我们接近极限就发出通知。

减少调用之间增量内存消耗的最大方法是启动另一个线程来处理Flask端点内的内存锁定任务,等待该线程完成,然后杀死该线程。就像我说的那样,它不能完全解决问题,并且确实会带来开销,但是它可以将内存泄漏问题减少到可以接受上述步骤的程度。这是一个创可贴修复程序,因此,如果有的话,可以随时提出替代/更好/真正的解决方案。

感谢@above_c_level,帮助您调试Flask中的内存泄漏。