この記事は、非同期Webアプリケーションを開発するためのPythonの使用に関するシリーズの第2部です。
最初のパートでは、Pythonの並行処理と asyncio
、そして aiohttp
について、より深く掘り下げて説明します。
Web開発のための非同期Pythonについてもっと読みたい方は、こちらもどうぞ。
aiohttp` のような非同期ライブラリのノンブロッキングの性質により、同期コードと比較して、一定時間内に多くのリクエストを作成し、処理できることが期待されます。
これは、非同期コードは I/O の待ち時間を最小にするために、 コンテキストを迅速に切り替えることができるという事実によるものです。
クライアントサイドとサーバーサイドのパフォーマンス比較
aiohttpのような非同期ライブラリのクライアントサイドのパフォーマンスをテストするのは、比較的簡単です。
ある Web サイトをリファレンスとして選び、ある数のリクエストを行い、コードがそれらを完了するまでにかかる時間を計測します。
ここでは、https://example.comにリクエストしたときの
aiohttpと
requests` の相対的なパフォーマンスについて見ていくことにする。
サーバサイドのパフォーマンスをテストするのは、少しトリッキーです。
aiohttp` のようなライブラリには開発用のサーバーが組み込まれており、ローカルネットワーク上でルートをテストするには適しています。
しかし、これらの開発用サーバーは、一般公開されているWebサイトに期待されるような負荷を扱うことができず、Javascript、CSS、画像ファイルのような静的な資産を提供することが苦手なので、一般公開されているWeb上にアプリケーションを展開するのには向いていません。
aiohttp` と類似の同期型 Web フレームワークの相対的なパフォーマンスを知るために、Flask を使用して Web アプリを再実装し、両方の実装の開発サーバーと本番サーバーを比較します。
本番サーバーには、gunicornを使用する予定です。
クライアントサイド: aiohttp vs リクエスト
伝統的な同期的アプローチでは、単純な for
ループを使用します。
ただし、このコードを実行する前に、requests モジュールをインストールしておいてください。
$ pip install --user requests
それでは、より伝統的な方法で実装してみましょう。
# multiple_sync_requests.py
import requests
def main():
n_requests = 100
url = "https://example.com"
session = requests.Session()
for i in range(n_requests):
print(f"making request {i} to {url}")
resp = session.get(url)
if resp.status_code == 200:
pass
main()
非同期なコードは少し複雑です。
aiohttpで複数のリクエストを行うには、
asyncio.gather` メソッドを利用して、同時にリクエストを行います。
# multiple_async_requests.py
import asyncio
import aiohttp
async def make_request(session, req_n):
url = "https://example.com"
print(f"making request {req_n} to {url}")
async with session.get(url) as resp:
if resp.status == 200:
await resp.text()
async def main():
n_requests = 100
async with aiohttp.ClientSession() as session:
await asyncio.gather(
*[make_request(session, i) for i in range(n_requests)]
)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
bashのtimeユーティリティを使って、同期と非同期の両方のコードを実行する。
me@local:~$ time python multiple_sync_requests.py
real 0m13.112s
user 0m1.212s
sys 0m0.053s
me@local:~$ time python multiple_async_requests.py
real 0m1.277s
user 0m0.695s
sys 0m0.054s
並列/非同期コードははるかに高速です。
# multiple_sync_request_threaded.py
import threading
import argparse
import requests
def create_parser():
parser = argparse.ArgumentParser(
description="Specify the number of threads to use"
)
parser.add_argument("-nt", "--n_threads", default=1, type=int)
return parser
def make_requests(session, n, url, name=""):
for i in range(n):
print(f"{name}: making request {i} to {url}")
resp = session.get(url)
if resp.status_code == 200:
pass
def main():
parsed = create_parser().parse_args()
n_requests = 100
n_requests_per_thread = n_requests // parsed.n_threads
url = "https://example.com"
session = requests.Session()
threads = [
threading.Thread(
target=make_requests,
args=(session, n_requests_per_thread, url, f"thread_{i}")
) for i in range(parsed.n_threads)
]
for t in threads:
t.start()
for t in threads:
t.join()
main()
このかなり冗長なコードを実行すると、次のような結果が得られます。
me@local:~$ time python multiple_sync_request_threaded.py -nt 10
real 0m2.170s
user 0m0.942s
sys 0m0.104s
スレッド数を増やすことでパフォーマンスを向上させることができますが、その効果は急速に減退します。
me@local:~$ time python multiple_sync_request_threaded.py -nt 20
real 0m1.714s
user 0m1.126s
sys 0m0.119s
スレッドを導入することで、非同期コードの性能に近づけることができますが、その代償としてコードがより複雑になります。
レスポンスタイムは同等になりますが、シンプルにできるコードを複雑にする代償として、その価値はありません – コードの品質は、複雑さや使用する行数によって向上するものではありません。
サーバーサイド:aiohttp vs Flask
異なるサーバーのパフォーマンスをテストするために、Apache Benchmark (ab) ツールを使用します。
ab` では、同時に行うリクエストの数に加えて、行うリクエストの総数も指定できます。
テストを始める前に、同期フレームワークを使用して惑星トラッカーアプリ(前回の記事)を再実装する必要があります。
ここでは、APIが aiohttp
に似ている Flask
を使用します(実際には aiohttp
ルーティング API は Flask
がベースになっています)。
# flask_app.py
from flask import Flask, jsonify, render_template, request
from planet_tracker import PlanetTracker
__all__ = ["app"]
app = Flask(__name__, static_url_path="",
static_folder="./client",
template_folder="./client")
@app.route("/planets/<planet_name", methods=["GET"])
def get_planet_ephmeris(planet_name):
data = request.args
try:
geo_location_data = {
"lon": str(data["lon"]),
"lat": str(data["lat"]),
"elevation": float(data["elevation"])
}
except KeyError as err:
# default to Greenwich observatory
geo_location_data = {
"lon": "-0.0005",
"lat": "51.4769",
"elevation": 0.0,
}
print(f"get_planet_ephmeris: {planet_name}, {geo_location_data}")
tracker = PlanetTracker()
tracker.lon = geo_location_data["lon"]
tracker.lat = geo_location_data["lat"]
tracker.elevation = geo_location_data["elevation"]
planet_data = tracker.calc_planet(planet_name)
return jsonify(planet_data)
@app.route('/')
def hello():
return render_template("index.html")
if __name__ == "__main__":
app.run(
host="localhost",
port=8000,
threaded=True
)
もし、あなたが前回の記事を読まずに飛び込んできたのなら、テストの前にプロジェクトを少しセットアップする必要があります。
私はすべてのPythonサーバーのコードを planettracker
というディレクトリに置きましたが、これは私のホームフォルダのサブディレクトリです。
me@local:~/planettracker$ ls
planet_tracker.py
flask_app.py
aiohttp_app.py
先に進む前に、前回の記事を見て、すでに構築されたアプリケーションに慣れることを強くお勧めします。
aiohttpとFlaskの開発サーバ
一度に20リクエスト、1000リクエストを処理するのにかかる時間を見てみましょう。
まず、ターミナルウィンドウを2つ開いてみます。
1つ目のウィンドウで、サーバを起動します。
# terminal window 1
me@local:~/planettracker$ pipenv run python aiohttp_app.py
2 番目のウィンドウで ab
を実行します。
# terminal window 2
me@local:~/planettracker$ ab -k -c 20 -n 1000 "localhost:8000/planets/mars?lon=145.051⪫=-39.754&elevation=0"
...
Concurrency Level: 20
Time taken for tests: 0.494 seconds
Complete requests: 1000
Failed requests: 0
Keep-Alive requests: 1000
Total transferred: 322000 bytes
HTML transferred: 140000 bytes
Requests per second: 2023.08 [#/sec] (mean)
Time per request: 9.886 [ms] (mean)
Time per request: 0.494 [ms] (mean, across all concurrent requests)
Transfer rate: 636.16 [Kbytes/sec] received
...
ab` はたくさんの情報を出力してくれますが、ここでは最も関連性の高いものだけを表示しました。
この中で最も注意を払うべきは “Requests per second” フィールドの数値である。
さて、最初のウィンドウでサーバーを終了して、Flask
アプリを起動してみましょう。
# terminal window 1
me@local:~/planettracker$ pipenv run python flask_app.py
テストスクリプトを再度実行します。
# terminal window 2
me@local:~/planettracker$ ab -k -c 20 -n 1000 "localhost:8000/planets/mars?lon=145.051⪫=-39.754&elevation=0"
...
Concurrency Level: 20
Time taken for tests: 1.385 seconds
Complete requests: 1000
Failed requests: 0
Keep-Alive requests: 0
Total transferred: 210000 bytes
HTML transferred: 64000 bytes
Requests per second: 721.92 [#/sec] (mean)
Time per request: 27.704 [ms] (mean)
Time per request: 1.385 [ms] (mean, across all concurrent requests)
Transfer rate: 148.05 [Kbytes/sec] received
...
各ライブラリの開発用サーバーを使用した場合、aiohttp
アプリは Flask
アプリの 2.5 倍から 3 倍の速度で動作しているように見えます。
もし、gunicorn
を使ってアプリを提供するとどうなるでしょうか?
aiohttp と Flask は gunicorn によって提供されたものです。
アプリを本番モードでテストする前に、まず gunicorn
をインストールし、適切な gunicorn
ワーカークラスを使ってアプリを実行する方法を考えなければなりません。
Flaskアプリのテストには標準の
gunicornワーカーを使用できますが、
aiohttpのテストには
aiohttpにバンドルされている
gunicornワーカーを使用しなければなりません。
gunicorn は pipenv でインストールすることができます。
me@local~/planettracker$ pipenv install gunicorn
適切な gunicorn
ワーカーを使用して、aiohttp
アプリを実行します。
# terminal window 1
me@local:~/planettracker$ pipenv run gunicorn aiohttp_app:app --worker-class aiohttp.GunicornWebWorker
今後、ab
のテスト結果を表示する際には、簡潔にするために “Requests per second” フィールドのみを表示することにします。
# terminal window 2
me@local:~/planettracker$ ab -k -c 20 -n 1000 "localhost:8000/planets/mars?lon=145.051⪫=-39.754&elevation=0"
...
Requests per second: 2396.24 [#/sec] (mean)
...
では、Flask
アプリの結果を見てみましょう。
# terminal window 1
me@local:~/planettracker$ pipenv run gunicorn flask_app:app
ab`を使ったテスト
# terminal window 2
me@local:~/planettracker$ ab -k -c 20 -n 1000 "localhost:8000/planets/mars?lon=145.051⪫=-39.754&elevation=0"
...
Requests per second: 1041.30 [#/sec] (mean)
...
gunicornを使用すると、
aiohttpと
Flaskの両方のアプリで確実にパフォーマンスが向上します。
aiohttp アプリの方が、開発サーバーほどではありませんが、まだパフォーマンスが高いです。
gunicornでは、アプリを提供するために複数のワーカーを使用することができます。
w コマンドライン引数を使って、 gunicorn
に多くのワーカープロセスを生成するように指示することができます。
4 つのワーカーを使用すると、アプリのパフォーマンスが大幅に向上します。
# terminal window 1
me@local:~/planettracker$ pipenv run gunicorn aiohttp_app:app -w 4
ab` を使ってテストしています。
# terminal window 2
me@local:~/planettracker$ ab -k -c 20 -n 1000 "localhost:8000/planets/mars?lon=145.051⪫=-39.754&elevation=0"
...
Requests per second: 2541.97 [#/sec] (mean)
...
次に Flask
バージョンです。
# terminal window 1
me@local:~/planettracker$ pipenv run gunicorn flask_app:app -w 4
ab` を使ってテストする
# terminal window 2
me@local:~/planettracker$ ab -k -c 20 -n 1000 "localhost:8000/planets/mars?lon=145.051⪫=-39.754&elevation=0"
...
Requests per second: 1729.17 [#/sec] (mean)
...
Flask` アプリでは、複数のワーカーを使用することで、より大幅なパフォーマンスの向上が見られました!
結果のまとめ
一歩下がって、惑星追跡アプリの aiohttp
と Flask
の両方の実装で、開発サーバーと本番サーバーをテストした結果を表にして見てみましょう。
aiohttp | Flask | %の違い||。 | |
---|---|---|---|
開発サーバー (リクエスト/秒) | 2023.08 | 721.92 | 180.24 |
gunicorn(リクエスト/秒)|2396.24|1041.30|130.12|。 | |||
開発サーバーに対する増加率|18.45|44.24|||||||||||。 | |||
gunicorn -w 4 (リクエスト/秒) | 2541.97 | 1729.17 | 47.01 |
開発サーバーからの増加率|25.65|139.52|||||||||||。 |
結論
この記事では、非同期のWebアプリケーションのパフォーマンスを同期のものと比較し、そのためにいくつかのツールを使用しました。
非同期のPythonライブラリやプログラミング技術を使うことで、リモートサーバへのリクエストや受信リクエストの処理など、アプリケーションを高速化する可能性があります。
アプリケーションを高速化できる可能性があります。