responderで理解するWebサーバー #6 非同期処理
こんにちは、Webエンジニアのjuri-tです。
本当は前回書きたいなと思っていた内容でしたが、勘違いしていて実際はバックグラウンドタスクになってしまったので、リベンジです。
非同期処理という言葉の意味が曖昧な問題
Pythonでは非同期処理は、Python3.4で実装されたasyncioに代表される非同期I/Oを指すことが一般的かと思います。マルチスレッド、マルチプロセスとは別概念として説明されていることも多い気がしますが、公式ドキュメントではasyncioについて並行処理のコードを書くためのライブラリと説明があります。
今回私の中では、バックグラウンドタスクと非同期処理は別物として扱おうと思いますが、上の説明とか読んでる印象だと、バックグラウンドタスクを非同期処理と説明してもあながち間違いではない気もしてます。(メインのリクエストと別の処理を非同期的に処理することが可能なので)
ただ今回言及したいのは、responderのrouteデコレーターでデコレートする関数がasyncと宣言している理由です。これはバックグラウンドタスクによって非同期的な処理が簡単に書けることとは別の話です。
FlaskはWSGIであり、responderはASGIです。nodejsやgolangと同程度のスピードがあるよ!とベンチマークを公開しているfastapiもASGIです。responderもfastapiも内部はstarletteを使っているので、responderもfastapiに匹敵するぐらい早いと思われます。(これは私の希望的観測です)
asyncはコルーチン関数を定義できる
asyncと宣言された関数は、コルーチンとして関数を定義することになります。コルーチンとして定義すると、関数内でawait構文を使って、別のコルーチン関数を呼ぶことができます。
説明だけだとよくわからないので、実際に動かして確認してみます。まずはasyncioを使わないtime.sleepを使った場合です。
time.sleepを使うとリクエストを順番にしか処理できない
time.sleepがI/Oが遅い処理を呼び出す代わりです。非同期対応していない関数の場合はtime.sleepした場合と同じような挙動になります。
import responder
from time import sleep
from datetime import datetime
api = responder.API()
@api.route("/")
async def index(req, resp):
await wait_for()
resp.media = {"message": 'ok'}
async def wait_for():
print(f"{datetime.now():%H:%M:%S} start.")
sleep(5)
print(f"{datetime.now():%H:%M:%S} end.")
サーバーを起動して、curlなどで同時にいくつかリクエストを送ってみましょう(Webサーバーでは、同時に複数のリクエストを捌く必要があります)。1つずつリクエストを処理する挙動をしたと思います。これはリクエストを順番に捌いている状況です。
さて、次はasyncioを使ってみます。
asyncio.sleepを使うとリクエストを同時に処理できる
import responder
from asyncio import sleep
from datetime import datetime
api = responder.API()
@api.route("/")
async def index(req, resp):
await wait_for()
resp.media = {"message": 'ok'}
async def wait_for():
print(f"{datetime.now():%H:%M:%S} start.")
await sleep(5)
print(f"{datetime.now():%H:%M:%S} end.")
同様にcurlなどで同時にいくつかリクエストを送ってみましょう。リクエストを同時に捌けましたね?これはresponderがリクエストを非同期的に捌いているからです。つまり、async対応しているDatabase Clientであれば、DBのレスポンスを待っている間も別のリクエストを処理することができることを意味します。
非同期ではないFlaskでは非同期関数をリクエスト処理内で呼び出す方法がたぶんないので、threaded=trueにしてマルチスレッド化するとか、上のレイヤーのWSGIサーバーのGunicornやuWSGIでマルチプロセス化するとか、複数台サーバーを立ち上げるとか、キャッシュさせてFlaskの前段で処理するとかで、リクエストを捌く感じになると思います。
このリクエストが大量に来たときのサーバーの限界値の比較とかは知らないので、実は大した差がなかったりするのかもしれません。ただ、C10K問題の解決策として非同期I/Oが登場しているので、より効率的にマシンリソースを使えると思われます。(関連する検証結果などを知っている方いらっしゃったらぜひ教えてください)
バックグラウンドタスクは実行完了を待つ必要がないとき
さて、最後にバックグラウンドタスクとの違いです。
バックグラウンドタスクは実行完了を待たないでレスポンスするので、レスポンスにタスクの結果を入れる必要がなく、処理に時間がかかるときに効力を発揮します。
一方、async/awaitは処理の結果を待つので、実行結果をレスポンスに反映させる必要があり、かつI/O待ちが多いときに使うことで効力を発揮します。
まとめ
今回は、非同期処理について書きました。書きやすさの差はあれど、これまでは「いや、それ別にFlaskでも出来るし・・」みたいなところはありましたが、非同期で処理できるのはresponderの大きなメリットの一つではないでしょうか。ではでは、今回はこのへんで