环境
python 版本3.6.4
gevent 1.5.0
gunicorn 20.1.0
错误
RecursionError: maximum recursion depth exceeded while calling a Python object
错误原因
根据错误栈,出问题的代码在python官方ssl包ssl.py第465行,具体代码
class SSLContext(_SSLContext):
@property
def options(self):
return Options(super().options)
@options.setter
def options(self, value):
# 这就是抛错的代码
super(SSLContext, SSLContext).options.__set__(self, value)
在对SSLContext
实例设置option属性的时候,会调用到super(SSLContext, SSLContext).options.__set__(self, value)
问题的原因在于先导入了ssl
包,然后才进行了gevent patch,这样上面这一行代码中的SSLContext
实际上已经被patch成了gevent._ssl3.SSLContext
gevent._ssl3.SSLContext
相关的代码如下
class SSLContext(orig_SSLContext):
@orig_SSLContext.options.setter
def options(self, value):
super(orig_SSLContext, orig_SSLContext).options.__set__(self, value)
gevent._ssl3.SSLContext
中继承的orig_SSLContext
就是python官方的ssl.SSLContext
所以整体的逻辑就变成了
1.super(SSLContext, SSLContext).options.__set__(self, value)
2.由于已经经过了patch,所以SSLContext
实际上是gevent._ssl3.SSLContext
,那么super(SSLContext, SSLContext).options.__set__(self, value)
实际上是super(gevent._ssl3.SSLContext, gevent._ssl3.SSLContext).options.__set__(self, value)
3.由于gevent继承了ssl.SSLContext
所以会调用到SSLContext的options.setter
方法,这样就回到了1,在这里开始了无限递归
所以patch时机不对,导致调用SSLContext
实际是调用了gevent._ssl.SSLContext
如果先patch再导入,则自始至终都是gevent._ssl3.SSLContext
,调用的代码变成super(orig_SSLContext, orig_SSLContext).options.__set__(self, value)
orig_SSLContext
即ssl.SSLContext
patch时机正确,则直接从gevent._ssl.SSLContext
调用
根本原因
抛出异常的原因清楚了,我们再来找找为什么会抛出这个异常
先看gunicorn的启动顺序,为了清晰,我省略了无关的代码,只列出了和启动相关的代码
gunicorn启动的入口是WSGIApplication().run()
WSGIApplication
继承了Application
,Application
继承BaseApplication
,BaseApplication
的__init__
方法中调用了self.do_load_config()
进行配置加载
首先,进行初始化,在__init__中调用了这个方法
def do_load_config(self):
\"\"\"
Loads the configuration
\"\"\"
try:
# 对cfg进行初始化,读取配置
self.load_default_config()
# 加载配置文件
self.load_config()
except Exception as e:
print(\"\\nError: %s\" % str(e), file=sys.stderr)
sys.stderr.flush()
sys.exit(1)
self.do_load_config()
调用self.load_default_config()
和self.load_config()
对cfg进行初始化
接着,调用run
方法,WSGIApplication
没有实现run
方法,则调用Application
的run
方法
def run(self):
if self.cfg.print_config or self.cfg.check_config:
try:
# 在这里加载app
self.load()
except Exception:
sys.exit(1)
sys.exit(0)
# 这里会调用Arbiter的run方法
super().run()
可以看到调用了self.load()
接着看load
方法
def load(self):
if self.cfg.paste is not None:
return self.load_pasteapp()
else:
# 我们目前走这里
return self.load_wsgiapp()
所以load
这里加载了我们的app
接着,Application
的run
方法最后会调用Arbiter
的run
方法
def run(self):
\"Main master loop.\"
self.start()
util._setproctitle(\"master [%s]\" % self.proc_name)
try:
# 这里处理worker
self.manage_workers()
# 省略部分代码
except Exception:
sys.exit(-1)
启动worker最终会调用spawn_worker
def spawn_worker(self):
self.worker_age += 1
# 在配置中设置的worker class
worker = self.worker_class(self.worker_age, self.pid, self.LISTENERS,
self.app, self.timeout / 2.0,
self.cfg, self.log)
# 省略部分代码
try:
# 这里初始化,对gevent而言,初始化的时候,才会进行patch
worker.init_process()
sys.exit(0)
except SystemExit:
raise
worker的init_process
方法如下
def init_process(self):
# 在这里调用patch
self.patch()
hub.reinit()
super().init_process()
看self.patch()
的实现
def patch(self):
# 在这里进行patch
monkey.patch_all()
综上,gunicorn启动的时候,加载顺序为:
配置文件加载 -> app加载 -> worker初始化
此外我们还发现,在gunicorn处理config的时候,在gunicorn.config
中导入了ssl
包,所以在worker初始化之前ssl
包已经被导入了,后面的patch又把ssl
包patch成了gevent._ssl3
,最终导致了上面的问题
复现
问题找到,我们先构造一个可以复现的例子
app.py
from flask import Flask
import requests
app = Flask(__name__)
from requests.packages.urllib3.util.ssl_ import create_urllib3_context
ctx = create_urllib3_context()
@app.route(\"/test\")
def test():
requests.get(\"https://www.baidu.com\")
return \"test\"
if __name__ == \"__main__\":
app.run(debug=True)
启动命令
gunicorn -w 2 --worker-class gevent --preload -b 0.0.0.0:5000 app:app
现在当我们启动后,调用http://127.0.0.1:5000/test 就会触发RecursionError
解决
既然问题在于ssl包导入之后才进行patch,那么我们前置patch即可,考虑到配置文件加载在加载app之前,如果我们在配置文件加载时patch,则是目前能够找到的最早的patch时机。
配置文件gunicorn_config.py
import gevent.monkey
gevent.monkey.patch_all()
workers = 8
启动命令
gunicorn --config config.py --worker-class gevent --preload -b 0.0.0.0:5000 app:app
问题解决
来源:https://www.cnblogs.com/buxizhizhoum/p/16264833.html
本站部分图文来源于网络,如有侵权请联系删除。