記一次 gunicorn 啟動 flask 出問題的經歷


出錯現象:

gunicorn+nginx+flask 部署項目, 部署過程沒問題,項目也正常啟動了,但是一旦訪問接口,就會報錯:

Traceback (most recent call last):
  File "/usr/local/lib/python3.6/dist-packages/gunicorn/workers/sync.py", line 135, in handle
    self.handle_request(listener, req, client, addr)
  File "/usr/local/lib/python3.6/dist-packages/gunicorn/workers/sync.py", line 176, in handle_request
    respiter = self.wsgi(environ, resp.start_response)
TypeError: __call__() takes from 1 to 2 positional arguments but 3 were given

但是我通過 runserver運行的話,是沒有問題的,外網可以正常訪問.

所以問題就出在gunicorn 和 flask 的 wsgi 對接上.

gunicorn 啟動時的方式是 gunicorn [options] file:app

我其他方面想了很多,試了很多,都沒解決, 然后盯着這句話想了一二十分鍾........ 嗯.......可能是這個原因:

直接通過終端 runserver方式啟動的話,會直接運行啟動文件, 最終提供服務的是app (就是flask的實例)

我為了數據庫遷移, 啟動文件注冊了flask-script的對象,最后啟動的實際是flask-script對象, 這個對象提供的就是些命令行之類的東西, 簡單地說,就是一個命令行擴展, 在不使用命令行遷移數據庫的時候沒什么用(至少我小白看來是這樣).

這就出來個想法了.

gunicorn說白了就是服務代理, 用來處理高並發的, 通過多進程/協程/線程 的方式提供並發訪問, 所以gunicorn 代理的是flask對象, 也就是flask實例化的APP.

我的啟動文件代碼大概如下:

from projects import APP, db
from flask_migrate import Migrate, MigrateCommand
from flask_script import Manager

manage = Manager(APP)
migrate = Migrate(app=APP, db=db)
manage.add_command('db', MigrateCommand)

if __name__ == '__main__':
    manage.run()

我這次的錯誤就在於將 flask-script的實例提供給了gunicorn, 這就導致了gunicorn接收到的參數並不是標准wsgi參數,所以報錯.

解決辦法很簡單: 將APP(對於此處而言)交給gunicorn代理就好了.

所以gunicorn的啟動命令改成:

1 gunicorn -D -w 3 -t 300 -b 0.0.0.0:5000 manage:APP

有需要的話,加上日志的配置,個人建議最好加上日志,日志是處理問題的最直接資料

gunicorn -D --error-logfile=/logs/gunicorn.log --pid=/logs/gunicorn.pid --access-logfile=/logs/access.log -w 3 -t 300 -b 0.0.0.0:5000 manage:APP

 

所以本次問題的原因在於wsgi協議未被標准的執行,代理服務器代理的是服務器APP, 而我一直沒注意到這個.

下面是flask-script在終端直接runserver時的運行機制:

如我上方代碼所示,manage是flask-script的實例, 執行Python manage時, 會執行該實例的run()方法

    def run(self, commands=None, default_command=None):
        """
        Prepares manager to receive command line input. Usually run
        inside "if __name__ == "__main__" block in a Python script.

        :param commands: optional dict of commands. Appended to any commands
                         added using add_command().

        :param default_command: name of default command to run if no
                                arguments passed.
        """

        if commands:
            self._commands.update(commands)

        # Make sure all of this is Unicode
        argv = list(text_type(arg) for arg in sys.argv)
        if default_command is not None and len(argv) == 1:
            argv.append(default_command)

        try:
            result = self.handle(argv[0], argv[1:])
        except SystemExit as e:
            result = e.code

        sys.exit(result or 0)

如上方法會執行 handle() 然后通過一系列的方法,路過執行如下代碼:

    def add_default_commands(self):
        """
        Adds the shell and runserver default commands. To override these,
        simply add your own equivalents using add_command or decorators.
        """

        if "shell" not in self._commands:
            self.add_command("shell", Shell())
        if "runserver" not in self._commands:
            self.add_command("runserver", Server())

上面的代碼會給flask-script對象添加兩個命令,其中就有我們很熟悉的 runserver, 該命令會執行Server()對象的call方法,call方法如下:

    def __call__(self, app, host, port, use_debugger, use_reloader,
                 threaded, processes, passthrough_errors, ssl_crt, ssl_key):
        # we don't need to run the server in request context
        # so just run it directly

        if use_debugger is None:
            use_debugger = app.debug
            if use_debugger is None:
                use_debugger = True
                if sys.stderr.isatty():
                    print("Debugging is on. DANGER: Do not allow random users to connect to this server.", file=sys.stderr)
        if use_reloader is None:
            use_reloader = use_debugger

        if None in [ssl_crt, ssl_key]:
            ssl_context = None
        else:
            ssl_context = (ssl_crt, ssl_key)

        app.run(host=host,
                port=port,
                debug=use_debugger,
                use_debugger=use_debugger,
                use_reloader=use_reloader,
                threaded=threaded,
                processes=processes,
                passthrough_errors=passthrough_errors,
                ssl_context=ssl_context,
                **self.server_options)

到這里就很明了了,在執行 python manage.py runserver 的時候,如果命令不是flask-script的提供的其他命令的話,就會執行flask實例的run方法, 實質上,就是 Flask(__name__).run()

而flask-script就是監測有沒有收到自己的命令.

雖然flask-script也會代理flask的APP, 但是flask-script的對象並不等同與flask的實例,所以提供給gunicorn的還必須得是flask的app

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM