環境
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
問題解決
浙公網安備 33010602011771號