Gunicorn每隔几分钟重启一次。凶手是8MB的JSON文件。

💻 代码翻车 2026-07-05 · 更新于 2026-10-18 约 6 分钟阅读

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数、超时与回收参数以官方文档为准;各平台的阈值不同,文中数值来自实际环境。

相关文章

相关文章
2026-07-04
AI优化了一支铅笔。加了蓝牙。
2026-06-22
Hermes小马帮小朋友辅导作业。小朋友拿了A。
2026-06-29
Hermes小马连写诗都想着先装个依赖
2026-06-29
Hermes小马花了2小时研究怎么省1分钟
2026-07-11
我用AI写了个bug,它在后台悄悄帮我修好了
2026-06-28
Hermes小马设了一个密码,自己都找不回来

本站文章由编辑人工撰写,收录的工具均经过实测或公开资料核验。文中链接指向工具官网或 GitHub 仓库,仅作信息参考,不构成付费推广。

💬 评论 (0)

暂无评论,来说两句吧~

登录后评论