Django ASGI应用内存泄漏问题排查及临时解决方法
Django ASGI应用内存泄漏问题排查与临时解决方案
2024年4月25日更新
malloc_trim(0) 初步验证有效,仍需长期观察服务器内存消耗。以下是修改后的asgi.py,通过启动守护线程,在内存达到阈值时调用malloc_trim(0):
import ctypes import random import time from threading import Thread import psutil import os import django from django.core.handlers.asgi import ASGIHandler MEMORY_THRESHOLD = 1024 * 1024 * 256 # 256MB def trim_memory() -> int: libc = ctypes.CDLL("libc.so.6") return libc.malloc_trim(0) def should_trim_memory() -> bool: # 检查是否接近内存耗尽阈值 process = psutil.Process(os.getpid()) return process.memory_info().rss > MEMORY_THRESHOLD def trim_loop() -> None: while True: time.sleep(random.randint(30, 60)) # 随机延迟30-60秒,避免固定频率 if not should_trim_memory(): continue ret = trim_memory() print("trim memory result: ", ret) def get_asgi_application(): """ The public interface to Django's ASGI support. Return an ASGI 3 callable. Avoids making django.core.handlers.ASGIHandler a public API, in case the internal implementation changes or moves in the future. """ django.setup(set_prefix=False) thread = Thread(name="TrimThread", target=trim_loop) thread.daemon = True thread.start() return ASGIHandler() os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'v45.settings') application = get_asgi_application()
2024年4月24日更新
全新Django ASGI应用存在内存泄漏问题,关联CPython官方bug:TLS/SSL asyncio内存泄漏。报告称Python 3.9不使用uvloop时泄漏极小或无泄漏,但测试后该配置无效。由于Railway的nixpacks最高支持Python 3.11,无法验证Python 3.12是否修复该问题,欢迎分享有效配置方案。
复现步骤
- 克隆测试仓库fifdee/djangofresh,需根据Railway生成的域名修改settings.py中的ALLOWED_HOSTS和CSRF_TRUSTED_ORIGINS;
- 在Railway部署,自定义启动命令:
hypercorn --bind 0.0.0.0:$PORT --workers 1 --reload djangofresh.asgi:application; - 访问应用确认显示“Hello, world!”;
- 本地发起大量并发请求(代码如下),观察服务器内存消耗:每次请求循环结束后仅部分内存释放,内存会随时间持续增长。
from concurrent.futures import ThreadPoolExecutor import requests as requests def get_server_response(params): url = 'https://djangofresh-production.up.railway.app/' with requests.Session() as s: r = s.get(url, headers={ }, json=params) print(r.text[:100]) return r.text if __name__ == '__main__': with ThreadPoolExecutor(max_workers=100) as executor: for _ in range(100): params = { 'text': ' Very long text ' * 1000000, } executor.submit(get_server_response, params)
2024年4月18日更新
问题并非特定于django-storages和图片上传,而是所有请求都会引发内存泄漏:
- 通过预签名URL让客户端直接上传图片至S3,仅在数据库存储文件名(无图片数据经过Django服务器),内存仍缓慢增长;
- 移除AWS Lambda返回结果中的base64缩略图后,内存增长速度进一步变慢。
结论:Django服务器处理的每个请求都会引发内存泄漏,请求/响应体积越大,泄漏速度越快。
初始问题描述
花费数日排查,尝试多种方案未解决:通过django-storages将图片保存至绑定S3的Django模型ImageField时,托管Web应用内存持续增长。
测试与观察
- 无论使用哪种ASGI服务器(Daphne、Uvicorn、Hypercorn、Granian),结果一致;
- 用tracemalloc逐行检查内存分配,发现调用
uploaded_image.image.save或uploaded_image.thumbnail.save时内存占用上升,指向json/decoder.py文件; - 本地及服务器tracemalloc日志显示:单张图片保存后内存从约45MB升至55MB,短时间大量保存时内存飙升至约400MB,但之后会回落,垃圾回收看似正常;
- 托管容器内存统计完全不同:单worker启动内存约250MB,每次上传图片后内存增长50-100MB,仅约10%能释放,压力测试2分钟即可耗尽8GB内存;
- 测试railway.app和render.com均出现该问题:使用
hypercorn --workers 5 --max-requests 50 --max-requests-jitter 25时,render.com可通过重启worker释放内存,但railway.app无法释放,且重启worker会导致请求丢失,并非可行方案。
疑问
是否有人遇到过此类问题并已解决?能否强制分块保存图片至S3?或是托管容器的外部层在请求处理后无法释放内存?
内容的提问来源于stack exchange,提问作者fifdee
相关产品推荐
相关产品推荐

