Gunicorn开启preload导致requests出现递归调用异常的BUG

问题

最近几天使用Flask开发应用,开发环境启动正常,但是部署到线上环境就有问题,有一个请求第三方HTTP的接口报异常,具体异常如下:

RecursionError: maximum recursion depth exceeded while calling a Python object

从字面意思理解:应该是某个地方递归调用导致超出了最大递归深度,当然最大递归深度肯定不至于正常的业务受影响,检查了所有代码,根本没有出现递归调用。

网上搜索出来的内容,大概有这几种情况,但以下这些情况都解决不了我的问题:

一 python递归调用问题

Python的递归深度是有限制的,默认是1000,当递归超过1000此时就报错。

那对应的解决办法就是检查代码,如果确实循环调用需要递归超过1000次,就增大递归深度的参数,如下:

import sys
sys.setrecursionlimit(5000)

二 gunicorn使用gevent导致的bug

gunicorn以gevent方式启动,在gunicorn使用gevent时,系统会使用monkey patch。系统的部分函数会被修改,这个原因就是在python官方包ssl导入之后才进行patch,修改了ssl.py的SSLContext,导致ssl模块出现bug。原因详细分析可见这篇文章:https://www.cnblogs.com/buxizhizhoum/p/16264833.html

因此,在使用gevent,有些库要选择兼容gevent的版本。例如,任务调度的库apscheduler,web socket需要socketio的库等,需要专门选择gevent的函数。而有些库则直接无法使用,例如多进程multiprocess。(参见:https://blog.csdn.net/yyw794/article/details/104741340

解决方案:在Python官方包ssl导入之前就执行gevent.monkey.patch_all()

但是对于这个时间点,网上大多数文章没有给出明确的说法,前面那篇文章的作者分析的比较透彻,给出的方案也比较靠谱,可以看看:

既然问题在于ssl包导入之后才进行patch,那么我们前置patch即可,考虑到配置文件加载在加载app之前,如果我们在配置文件加载时patch,则是目前能够找到的最早的patch时机。

配置文件gunicorn_config.py

import gevent.monkey
gevent.monkey.patch_all()

workers = 8

启动命令

gunicorn --config gunicorn_config.py --worker-class gevent --preload -b 0.0.0.0:5000 app:app

但其实,我的应用在app.py的create_app方法中首先加载了自定义的config.py,在我自定义的config.py里应用gevent.monkey.patch_all()也可以达到同样的效果,故我的启动脚本不用–config参数,实际上我是在docker-compose方式部署,完整启动脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
gunicorn \
--bind "${APP_BIND_ADDRESS:-0.0.0.0}:${PORT:-5001}" \
--workers ${SERVER_WORKER_AMOUNT:-1} \
--worker-class ${SERVER_WORKER_CLASS:-gevent} \
--timeout ${GUNICORN_TIMEOUT:-200} \
--log-file /logs/gunicorn.log \
--log-level debug \
--access-logfile /logs/access.log \
--error-logfile /logs/error.log \
--capture-output \
--preload \
app:app

其实我在启动脚本中取消–preload也可以达到同样的效果,细究了一下preload的含义,

preload

--preload 选项会在所有 Worker分叉(forked)之前加载应用,这有助于减少内存使用,加快服务启动时间,但有时会导致不兼容问题。

官方的解释(原文链接:https://docs.gunicorn.org/en/latest/settings.html#preload-app):

1
2
3
4
5
Command line: --preload
Default: False
Load application code before the worker processes are forked.
By preloading an application you can save some RAM resources as well as speed up server boot times. Although, if you defer application loading to each worker process, you can reload your application code easily by restarting workers.

也就是启用preload之后,gunicorn会预先(分叉)一些worker进程,提高服务器处理性能。

默认情况下,gunicorn的每个进程,会将代码重新加载一次,以保障进程之间是互相隔离的。这样可以做到更好的兼容性。

但是,有些情况,需要多个进程共享同一个资源时,或多个进程只能开启1个任务时,则需要使用–preload

使用preload后,API函数之外的初始化代码,只会出现在gunicorn的管理进程中,以共享的方式让worker进程访问

原文链接:https://blog.csdn.net/yyw794/article/details/104741340

在检索这些的时候,发现很多有用的关于gunicorn知识,详细的可以看引用来源。

参考资料

gunicorn的实践经验:https://blog.csdn.net/yyw794/article/details/104741340

Gunicorn的预分叉架构:快速启动与高效资源利用:https://blog.csdn.net/2401_85639015/article/details/140335553

Gunicorn官网关于配置项:https://docs.gunicorn.org/en/latest/settings.html#preload-app

RecursionError: maximum recursion depth exceeded]:https://www.cnblogs.com/buxizhizhoum/p/16264833.html