Django + Gunicorn的 API间歇性挂起(504错误)——工作进程看起来处于空闲状态,但请求从未完成
我在Docker容器中运行一个Django应用,使用Gunicorn作为服务器,并配置多工作进程和多线程。系统还包含PostgreSQL、Redis,以及多个Celery任务进程。
问题在服务持续运行大约4 天后出现。
问题
在大约4 天的正常运行后:
- 所有前端API请求开始失败,错误为
504 Gateway Timeout - Django管理后台仍然正常工作
- Gunicorn的工作进程仍在运行(没有崩溃)
- 未发生容器重启
- CPU使用率较低,工作进程看起来处于空闲状态(
ep_poll、do_select) - 请求从未完成(没有返回响应)
观察
-
数据库连接保持健康:
-
空闲:5–7
-
活跃:1
-
Gunicorn进程仍在运行:
-
未观察到工作进程崩溃
-
未出现重启循环
-
内部健康检查日志显示:
-
响应码:000(未接收到HTTP响应)
Gunicorn配置
gunicorn alloy_admin.wsgi:application
--workers 3
--threads 2
--timeout 120
--max-requests 1000
--max-requests-jitter 100
--keep-alive 5
提问
是什么原因会让一个Django + Gunicorn的组合在正常工作好几天后,逐渐进入所有API请求卡住、最终返回504的状态,同时工作进程仍然存活且处于空闲?
这通常是由以下原因导致的吗:
- 线程耗尽 / 工作进程饱和?
- 视图内阻塞的同步外部API调用?
- 连接池耗尽(数据库 / HTTP)?
- Nginx / 代理超时问题?
- 还是Gunicorn工作进程中长期存在的内存/状态问题?
对于这种“延迟性全局挂起”且系统随时间下降且不崩溃的情况,推荐的排查方法是什么?
解决方案
在代理层积压请求、工作进程仍然存活且CPU低时,通常意味着以下两种情况之一:
- 请求不再可靠地到达Gunicorn的工作进程(在代理和Gunicorn之间的某个地方出现backlog/文件描述符/套接字耗尽)
- 工作进程被阻塞在外部I/O上(HTTP调用、Redis、DNS、上游服务等)
健康的数据库连接池,以及处于 epoll、select 的工作进程,使数据库瓶颈或CPU饱和的可能性降低。
管理后台仍在工作并不一定排除问题。后台管理的流量通常要小得多,可能绕开了你的API端点所涉及的代码路径或依赖。
再次出现问题时,我会检查工作进程到底在等待什么:
strace -fp <worker-pid>
如果工作进程一直处于 epoll_wait,没有任何活动,说明请求可能根本没有到达它们。
如果它们被阻塞在 recv、send、connect、DNS等系统调用中,那么工作进程很可能在等待某个外部依赖。
同时检查代理与Gunicorn之间的套接字状态:
ss -tanp | grep gunicorn
lsof -p <gunicorn-pid>
注意大量的:
SYN_RECVCLOSE_WAIT- 长时建立的连接
- 异常高的文件描述符数量
另一个有用的信号是:系统卡住时,请求是否仍然出现在Gunicorn的访问日志中?如果nginx/代理记录了请求但Gunicorn从未看到它们,这更指向工作进程前的连接排队/资源耗尽。
如果视图/任务中有同步的外部调用,也应检查。少量被卡住的外部请求最终可能耗尽所有可用的工作进程/线程,使整个API看起来不可用,而进程本身仍然健康。
在卡住时,我会从 strace 开始排查。这通常可以很快把问题定位到位。