非同期と同期のPythonのパフォーマンス分析

この記事は、非同期Webアプリケーションを開発するためのPythonの使用に関するシリーズの第2部です。

最初のパートでは、Pythonの並行処理と asyncio 、そして aiohttp について、より深く掘り下げて説明します。

Web開発のための非同期Pythonについてもっと読みたい方は、こちらもどうぞ。

aiohttp` のような非同期ライブラリのノンブロッキングの性質により、同期コードと比較して、一定時間内に多くのリクエストを作成し、処理できることが期待されます。

これは、非同期コードは I/O の待ち時間を最小にするために、 コンテキストを迅速に切り替えることができるという事実によるものです。

クライアントサイドとサーバーサイドのパフォーマンス比較

aiohttpのような非同期ライブラリのクライアントサイドのパフォーマンスをテストするのは、比較的簡単です。

ある Web サイトをリファレンスとして選び、ある数のリクエストを行い、コードがそれらを完了するまでにかかる時間を計測します。

ここでは、https://example.comにリクエストしたときのaiohttprequests` の相対的なパフォーマンスについて見ていくことにする。

サーバサイドのパフォーマンスをテストするのは、少しトリッキーです。

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&amp;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&amp;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&amp;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&amp;elevation=0"
...
Requests per second:    1041.30 [#/sec] (mean)
...


gunicornを使用すると、aiohttpFlaskの両方のアプリで確実にパフォーマンスが向上します。

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&amp;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&amp;elevation=0"
...
Requests per second:    1729.17 [#/sec] (mean)
...


Flask` アプリでは、複数のワーカーを使用することで、より大幅なパフォーマンスの向上が見られました!

結果のまとめ

一歩下がって、惑星追跡アプリの aiohttpFlask の両方の実装で、開発サーバーと本番サーバーをテストした結果を表にして見てみましょう。

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ライブラリやプログラミング技術を使うことで、リモートサーバへのリクエストや受信リクエストの処理など、アプリケーションを高速化する可能性があります。

アプリケーションを高速化できる可能性があります。

タイトルとURLをコピーしました