30
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Python + Flask + uWSGI + Nginxで処理完了を待たずに非同期処理(background処理)してレスポンスを先に返す方法

Last updated at Posted at 2018-12-04

この記事は全部俺 Advent Calendar 2018の4日目の記事です。

この記事 is 何?

Python + Flaskでバックグラウンド処理をしてレスポンスをすぐ返す方法について記載します。
※のっぴきならない理由がない限り、今から開発を始める場合はFlaskではなくresponderを使いましょう
この記事にresponderで同じことをする方法を記載しているのでよければご一読くださいm(_ _)m

tl;dr

  • Flaskで非同期処理を実現する前に、Flask.run()は開発環境前提なので本番環境用にuWSGIを通す必要があります
  • 処理を発火させた後、処理完了を待たずに終了することをfire and forgetと呼びます
  • 新たにサーバを立てる場合で非同期処理する可能性がある場合はresponderを使いましょう(2度目)

筆者の環境

software version
Ubuntu 18.04.1 LTS
Python 3.6.5
Flask 1.0.2

Flaskでawait/asyncを使えるようにするまで

まずはHello, World

まず、FlaskでHello, Worldするコードを書いてみます。

~/app.py
import flask

app = flask.Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, World\n'

if __name__ == '__main__':
    app.run(host='0.0.0.0')

そしてpython3 app.pyでサーバ起動した後、curl http://0.0.0.0:5000/すると、Hello, Worldが表示されました。同様に、ブラウザからhttp://[サーバIP]:5000にアクセスしてもHello, Worldが表示されるはずです。

Flaskのprocessesを増やしてみる

app.run(host="0.0.0.0")の部分を、app.run(host="0.0.0.0", processes=10)にして再度python3 app.pyしてみると、下記のエラーが出ます。
これは、Flask1.0.2のバグのようですが、そもそも3行目にWARNING: Do not use the development server in a production environment.と出ているように、Flask.run()は開発用なのでこれに対応して本番用の環境の作成から行っていきます。

 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
Traceback (most recent call last):
  File "app.py", line 14, in <module>
    app.run(host="0.0.0.0", threaded=True, processes=10)
  File "/home/ubuntu/.local/lib/python3.6/site-packages/flask/app.py", line 943, in run
    run_simple(host, port, self, **options)
  File "/home/ubuntu/.local/lib/python3.6/site-packages/werkzeug/serving.py", line 814, in run_simple
    inner()
  File "/home/ubuntu/.local/lib/python3.6/site-packages/werkzeug/serving.py", line 774, in inner
    fd=fd)
  File "/home/ubuntu/.local/lib/python3.6/site-packages/werkzeug/serving.py", line 656, in make_server
    raise ValueError("cannot have a multithreaded and "
ValueError: cannot have a multithreaded and multi process server.

本番環境の作成

uWSGIの準備

sudo pip3 install uwsgiでuwsgiをインストールした後、vim ~/uwsgi.iniで設定ファイルを作成し、以下のように記述します。

~/uwsgi.ini
[uwsgi]
socket = /tmp/uwsgi.sock
chmod-socket = 666
module = app
master = true
processes = 2

この設定では、HTTPではなくUnixドメインソケットを作るようにしているので、無駄なポートを消費しません。
processes = 2が今回の肝で、これによってマルチプロセス処理を可能にしているので、後のasync/await処理にて非同期処理を実装することができます。

Nginxの準備

sudo apt install nginxしてnginxをインストールします。
sudo vim /etc/nginx/conf.d/fireforget.confで新しく設定ファイルを作成し、以下のように記述します。

/etc/nginx/conf.d/fireforget.conf
server {
  listen 80 default_server;
  server_name _;

  location / {
    include uwsgi_params;
    uwsgi_pass unix:///tmp/uwsgi.sock;
  }
}

ここでは、uWSGIの準備で設定したUnixドメインソケットをuwsgi_passで指定しているので、/へのアクセスをuWSGIに流すことが可能になっています。

uWSGIとNginxの実行

sudo systemctl start nginx
sudo uwsgi --ini ~/uwsgi.ini

ここまで来たら、サーバ上でcurl http://127.0.0.1/した場合にHello, Worldが表示されるはずです。

並列処理化

vim ~/app.pyを実行し、以下のようにファイルを書き換えます。

~/app.py
import asyncio
import time
from datetime import datetime

import flask

app = flask.Flask(__name__)

@app.route('/')
def hello():
    # asyncioを使用し、処理を発火させた後完了を待たずにreturnする
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.ensure_future(sleep(10)))
    loop.close()

    print(f'[hello      ]{datetime.now().strftime("%Y/%m/%d %H:%M:%S")}')
    return "Hello, World\n"

# 長い時間がかかる処理
async def sleep(seconds):
    with open
    print(f'[sleep start]{datetime.now().strftime("%Y/%m/%d %H:%M:%S")}')
    time.sleep(seconds)
    print('slept!!')
    print(f'[sleep   end]{datetime.now().strftime("%Y/%m/%d %H:%M:%S")}')

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

これを実行すると、uWSGIから以下のように結果が出力されるはずです。

[hello      ]2018/12/04 14:45:43

ファイルの中身は以下のようになります。

[sleep start]2018/12/04 14:45:43
slept!!
[sleep   end]2018/12/04 14:45:53

以上でFlaskを用いた非同期実行が確認できました!

補足

ちなみに、上記のuWSGIとNginxを通さず、無理やりpython3 app.pyを実行してアクセスすると、以下のようなエラーログが出力されます。
これは、Flaskが非同期処理に対応したサーバではないのに無理やりasyncio.get_event_loop()を実行したために、新たなThreadを開始できずに発生するエラーです。

Traceback (most recent call last):
  File "/home/ubuntu/.local/lib/python3.6/site-packages/flask/app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/ubuntu/.local/lib/python3.6/site-packages/flask/app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/ubuntu/.local/lib/python3.6/site-packages/flask/app.py", line 1718, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/home/ubuntu/.local/lib/python3.6/site-packages/flask/_compat.py", line 35, in reraise
    raise value
  File "/home/ubuntu/.local/lib/python3.6/site-packages/flask/app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/ubuntu/.local/lib/python3.6/site-packages/flask/app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "app.py", line 11, in hello
    loop = asyncio.get_event_loop()
  File "/usr/lib/python3.6/asyncio/events.py", line 694, in get_event_loop
    return get_event_loop_policy().get_event_loop()
  File "/usr/lib/python3.6/asyncio/events.py", line 602, in get_event_loop
    % threading.current_thread().name)
RuntimeError: There is no current event loop in thread 'Thread-1'.
127.0.0.1 - - [04/Dec/2018 13:47:07] "GET / HTTP/1.1" 500 -

まとめ

どうでしたでしょうか?
Flask上で非同期処理をしたいだけなのに、かなり面倒な設定が多かったですね。。
繰り返しになりますが、今から開発を始める場合はresponderをおすすめします。
responderで非同期処理を行うノウハウについても近い内にまとめたいと思います。

30
31
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?