「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」。
问题
通过gunicorn部署flask服务,该服务是调用深度学习模型去做预测。然后通常到第3,第4次查询的时候就会出现服务莫名其妙的挂断。经过排查发现算法服务启动的时候内存占用只有1G不到,倒是到第3次时,内存占用就超过4G,这说明flask或者gunicorn出现的内存泄漏,后续就做实验验证内存泄漏这个问题。顺道把uwsgi也做了
实验
环境:
python 3.7 Flask==2.0.2 gevent==21.12.0 gunicorn==20.1.0
这里的实验并不是真正内存泄漏,而是通过一个全局的list模拟代码中存在内存泄漏,并通过对gunicorn和uwsgi配置修改对内存泄漏进行限制,防止爆内存。
gunicorn + flask
为了模拟泄漏的过程,定义L去接受l。
每一次请求的L都会增加78M。
# main.py
from flask import Flask, request, jsonify
import time
import sys
app = Flask(__name__)
L=[]
@app.route("/test", methods=["post","get"])
def test():
import sys
l=[10000 for _ in range(10000000)
L.append([l])
return jsonify({"code": 200,"msg": "success"})
if __name__ == '__main__':
app.run(debug=False)
这里使用的配置就是常规配置
# gunicorn.py 配置
# 绑定的ip与端口
bind = "0.0.0.0:8000"
# 进程数
workers = 4
# 指定每个进程开启的线程数
threads = 2
# 工作模式
worker_class = 'gevent'
# # 处理请求的工作线程数,使用指定数量的线程运行每个worker。为正整数,默认为1。
# worker_connections = 100
# 设置pid文件的文件名,如果不设置将不会创建pid文件
pidfile = './gunicorn.pid'
# 要写入错误日志的文件目录。
errorlog = './log/gunicorn.error.log'
# 要写入的访问日志目录
accesslog = './log/gunicorn.access.log'
在上述配置下,先请求10次,20次,50次,100次,然后看一下内存变化。
| 次数 | 占用内存 |
|---|---|
| 10 | 809MiB |
| 20 | 1.541GiB |
| 50 | 3.797GiB |
| 100 | 7.558GiB |
如果使用gunicorn配置中不对请求做限制,内存会存在一直占用
修改gunicorn 配置max_requests=20 每20次会重启一下服务。
# gunicorn.py 配置
#最大客户端并发数量,默认情况下这个值为1000。此设置将影响gevent和eventlet工作模式
max_requests=20
# max_requests_jitter
max_requests_jitter=2
| 次数 | 占用内存 |
|---|---|
| 10 | 872.6MiB |
| 20 | 1.605GiB |
| 50 | 2.282GiB |
| 100 | 1.231GiB |
内存
| 次数 | 限制占用内存 | 未限制占用内存 |
|---|---|---|
| 10 | 872.6MiB | 809MiB |
| 20 | 1.605GiB | 1.541GiB |
| 50 | 2.282GiB | 3.797GiB |
| 100 | 1.231GiB | 7.558GiB |
每20次请求就会随机重启(0,max_requests_jitter)worker,通过这种方式就可以限制内存
我使用uwsgi 做了相同的实验
uwsgi + flask
flask代码相同
[uwsgi]
# app是run.py里面的Flask对象
module = main:app
# 对外提供 http 服务的端口
http = :8000
#指定工作进程
processes = 4
#主进程
master = true
#每个工作进程有2个线程
threads = 2
#指的后台启动 日志输出的地方
daemonize = uwsgi.log
#保存主进程的进程号
pidfile = uwsgi.pid
| 次数 | 占用内存 |
|---|---|
| 10 | 811.7MiB |
| 20 | 1.544GiB |
| 50 | 3.802GiB |
| 100 | 7.564GiB |
修改uwsgi的配置
[uwsgi]
#设置工作进程使用虚拟内存超过N MB就回收重启
reload-on-as= 2048
内存
| 次数 | 限制占用内存 | 未限制占用内存 |
|---|---|---|
| 10 | 810.8MiB | 811.7MiB |
| 20 | 1.545GiB | 1.544GiB |
| 50 | 1.922GiB | 3.802GiB |
| 100 | 1.917GiB | 7.564GiB |
通过限制虚拟内存数就可以起限制内存作用
也可使用
limit-as
通过使用POSIX/UNIX的setrlimit()函数来限制每个uWSGI进程的虚拟内存使用数。
--limit-as 256
这个配置会限制uWSGI的进程占用虚拟内存不超过256M。如果虚拟内存已经达到256M,并继续申请虚拟内存则会使程序报内存错误,本次的http请求将返回500错误。
reload-on-rss
跟reload-on-as的效果类似,不过这个选项控制的是物理内存。你可以同时使用这2个选项:
uwsgi:
reload-on-as: 128
reload-on-rss: 96
结论
由于这里不是真正内存泄漏,因为正在的内存泄漏很难排查。
gunicorn的 max_requests
uwsgi的 reload-on-as,reload-on-rs,limit-as
这里通过web服务器的配置进行物理限制。
但是这里要注意,这里做物理限制,存在部分http请求,正好是重启的那个worker处理的,存在着请求丢失的可能性。但是正常来说丢失是可以接受的。
但是像我每一个请求都是一个起一个模型进行算法调用,如果出现worker重启,直接把我任务个挂了,我这边是不不能接受的。
整体来说 http 不太适合算法服务的搭建,不如grcp,websocket