環境 python 版本3.6.4 gevent 1.5.0 gunicorn 20.1.0 錯誤 RecursionError: maximum recursion depth exceeded while calling a Python object 錯誤原因 根據錯誤棧,出問題的代碼在pyt ...
環境
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
問題解決