Gunicorn每隔几分钟重启一次。凶手是8MB的JSON文件。
Flask 跑在 Gunicorn 上,4个worker每5到10分钟静默重启,日志里没有崩溃堆栈。查了两小时才发现:8MB的配置JSON在 before_request 里每个请求重解析一遍。
💡 你将学到
Flask 跑在 Gunicorn 上,4个worker每5到10分钟静默重启,日志里没有崩溃堆栈。查了两小时才发现:8MB的配置JSON在 before_request 里每个请求重解析一遍。
Gunicorn每隔几分钟重启一次。凶手是8MB的JSON文件。
线上 Flask 服务每隔五到十分钟就静默重启一次:没有500错误,没有traceback,日志里只有几行worker退出记录。用户感觉"偶尔卡一下",监控上是内存曲线爬升后突然掉回去。查过异常、数据库超时、nginx代理超时都没问题,线索是内存曲线。
一、先确认是谁在杀 worker
worker 静默退出通常只有三种原因,必须分清,否则会改错地方:
| 现象 | 真实原因 | 怎么确认 |
|---|---|---|
日志出现 WORKER TIMEOUT |
请求处理超过worker超时,主进程杀掉worker | --log-level debug 看 worker 是否重新启动 |
Worker exiting (pid) 但无异常 |
触发 --max-requests 被回收 |
检查启动参数 |
系统日志出现 Killed process ... (python) |
被内核 OOM killer 杀掉 | dmesg -T \| grep -i oom |
我这次是第三种:平台发现RSS超阈值就杀进程重拉,退出不写应用日志,所以什么都看不到。
二、四条排查命令
# 每个 worker 的内存占用
ps -eo pid,rss,etime,cmd --sort=-rss | grep gunicorn | head
# 盯30秒看变化(RSS单位KB)
watch -n 5 'ps -eo pid,rss,cmd | grep "[g]unicorn"'
# 有没有被系统杀过
dmesg -T | grep -i -E "oom|killed process"
# worker 生命周期日志
gunicorn app:app --log-level debug --access-logfile - --error-logfile -
内存泄漏时四个worker的RSS同步上涨,单个请求打爆内存则集中在某个时间点。我的曲线是每个worker都稳定爬升,重启后从低位重来。
三、根因:每个请求都重解析8MB
@app.before_request
def load_config():
with open("/srv/app/config.json") as f: # 8MB
g.config = json.load(f)
g.index = build_index(g.config) # 建倒排索引,又占几十MB
两层问题:before_request 每个请求都执行,健康检查每10秒一次,一天就是8600次全量解析;8MB的JSON展开成Python对象常占原文件的5到20倍,中间还有一次索引拷贝,CPython的分配器又不把内存还给系统,RSS只涨不跌。
于是平台杀进程重拉。表面像泄漏,本质是重复计算加分配器不归还。
四、改法:加载一次,所有请求共享
import json
def _load_config():
with open("/srv/app/config.json", "rb") as f:
return json.load(f)
CONFIG = _load_config() # 导入时执行一次
INDEX = build_index(CONFIG) # 索引也只在启动时建一次
@app.route("/lookup/<key>")
def lookup(key):
return INDEX.get(key) or ("", 404)
三个细节:用 rb 打开省一次解码;查询走启动时建的dict,把O(n)变O(1),这步收益常比"只加载一次"更大;每个worker各有一份,默认worker数是 (2 × CPU核数) + 1。
多进程有两个选择:
# 方案A:主进程先加载,fork后共享只读页
gunicorn app:app --preload --workers 4
# 方案B:控制回收阈值(缓解,不是修复)
gunicorn app:app --workers 4 --max-requests 2000 --max-requests-jitter 200
--preload 依赖写时复制,数据库连接、线程这类初始化必须挪到 post_fork 钩子里,否则fork后会出问题。--max-requests 是止血手段,掩盖问题,流量翻倍时会回来;配 --max-requests-jitter 让worker错开重启。
改完用 wrk 打五分钟压力并 watch RSS,曲线走平、24小时零重启才算修好。
常见问题
Q:为什么日志里完全没有报错?
A:进程不是被应用杀的,是被平台监管或内核OOM杀的。应用日志只能记到"worker退出",看不到原因,要查 dmesg 和平台的事件日志。
Q:本地跑没问题,线上才重启? A:本地单worker、低并发、活不过几小时;线上多worker、持续流量,内存问题只在长时间稳态下暴露。
注:Gunicorn 的默认worker数、超时与回收参数以官方文档为准;各平台的阈值不同,文中数值来自实际环境。
相关文章
本站文章由编辑人工撰写,收录的工具均经过实测或公开资料核验。文中链接指向工具官网或 GitHub 仓库,仅作信息参考,不构成付费推广。
