环境

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_SSLContextssl.SSLContext

patch时机正确,则直接从gevent._ssl.SSLContext调用

根本原因

抛出异常的原因清楚了,我们再来找找为什么会抛出这个异常

先看gunicorn的启动顺序,为了清晰,我省略了无关的代码,只列出了和启动相关的代码

gunicorn启动的入口是WSGIApplication().run()

WSGIApplication继承了ApplicationApplication继承BaseApplicationBaseApplication__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方法,则调用Applicationrun方法

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

接着,Applicationrun方法最后会调用Arbiterrun方法

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

问题解决

posted on 2022-05-12 22:56  Go_Forward  阅读(1488)  评论(0编辑  收藏  举报