Рубрики
Без рубрики

Асинхронный и Синхронный Анализ производительности Python

Автор оригинала: Dean Shaff.

Вступление

Эта статья является второй частью серии статей об использовании Python для разработки асинхронных веб-приложений. Первая часть обеспечивает более глубокое освещение параллелизма в Python и asyncio , а также aiohttp .

Если вы хотите узнать больше об асинхронном Python для веб-разработки , мы его рассмотрим.

Из-за неблокирующего характера асинхронных библиотек, таких как aiohttp , мы надеемся, что сможем делать и обрабатывать больше запросов за заданное время по сравнению с аналогичным синхронным кодом. Это связано с тем, что асинхронный код может быстро переключаться между контекстами, чтобы минимизировать время ожидания ввода-вывода.

Производительность на стороне клиента и Сервера

Тестирование клиентской производительности асинхронной библиотеки типа aiohttp относительно просто. Мы выбираем какой-то сайт в качестве ссылки, а затем делаем определенное количество запросов, рассчитывая, сколько времени потребуется нашему коду для их выполнения. Мы будем смотреть на относительную производительность aiohttp и запросов при выполнении запросов к https://example.com .

Тестирование производительности на стороне сервера немного сложнее. Библиотеки, такие как aiohttp , поставляются со встроенными серверами разработки, которые отлично подходят для тестирования маршрутов в локальной сети. Однако эти серверы разработки не подходят для развертывания приложений в общедоступной сети, поскольку они не могут справиться с нагрузкой, ожидаемой от общедоступного веб-сайта, и они не очень хорошо обслуживают статические ресурсы, такие как Javascript, CSS и файлы изображений.

Чтобы получить лучшее представление об относительной производительности aiohttp и аналогичного синхронного веб-фреймворка, мы собираемся повторно реализовать наше веб-приложение с помощью Flask , а затем сравним серверы разработки и производства для обеих реализаций.

Для производственного сервера мы будем использовать 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 :

[email protected]:~$ time python multiple_sync_requests.py
real    0m13.112s
user    0m1.212s
sys     0m0.053s
[email protected]:~$ 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()

Запуск этого довольно многословного фрагмента кода приведет к:

[email protected]:~$ time python multiple_sync_request_threaded.py -nt 10
real    0m2.170s
user    0m0.942s
sys     0m0.104s

И мы можем увеличить производительность, используя больше потоков, но отдача быстро уменьшается:

[email protected]:~$ 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 мы можем указать общее количество запросов, которые нужно сделать, в дополнение к количеству параллельных запросов, которые нужно сделать.

Прежде чем мы сможем начать тестирование, мы должны переопределить наше приложение planet tracker (из предыдущей статьи ) с помощью синхронного фреймворка. Мы будем использовать Flask , так как API похож на aiohttp (на самом деле aiohttp routing 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/", 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 в каталог planet tracker , который сам является подкаталогом моей домашней папки.

[email protected]:~/planettracker$ ls
planet_tracker.py
flask_app.py
aiohttp_app.py

Я настоятельно рекомендую вам посетить предыдущую статью и ознакомиться с приложением, которое мы уже создали, прежде чем продолжить.

Серверы разработки aiohttp и Flask

Давайте посмотрим, сколько времени требуется нашим серверам, чтобы обработать 1000 запросов, сделанных по 20 за раз.

Сначала я открою два окна терминала. В первом случае я запускаю сервер:

# terminal window 1
[email protected]:~/planettracker$ pipenv run python aiohttp_app.py

Во втором случае давайте запустим ab :

# terminal window 2
[email protected]:~/planettracker$ ab -k -c 20 -n 1000 "localhost:8000/planets/mars?lon=145.051&lat=-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 выводит много информации, и я показал только самый релевантный бит. Из этого числа наибольшее внимание следует уделить полю “Запросы в секунду”.

Теперь, выйдя из сервера в первом окне, давайте запустим наше приложение Flask :

# terminal window 1
[email protected]:~/planettracker$ pipenv run python flask_app.py

Повторный запуск тестового сценария:

# terminal window 2
[email protected]:~/planettracker$ ab -k -c 20 -n 1000 "localhost:8000/planets/mars?lon=145.051&lat=-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 работает в 2,5-3 раза быстрее, чем Flask при использовании соответствующего сервера разработки каждой библиотеки.

Что произойдет, если мы используем gunicorn для обслуживания наших приложений?

aiohttp и колба, обслуживаемая gunicorn

Прежде чем мы сможем протестировать наши приложения в производственном режиме, мы должны сначала установить gunicorn и выяснить, как запускать ваши приложения с помощью соответствующего gunicorn рабочего класса. Для тестирования приложения Flask мы можем использовать стандартный gunicorn worker, но для aiohttp мы должны использовать gunicorn worker в комплекте с aiohttp . Мы можем установить gunicorn с pip env:

[email protected]~/planettracker$ pipenv install gunicorn

Мы можем запустить приложение aiohttp с соответствующим gunicorn работником:

# terminal window 1
[email protected]:~/planettracker$ pipenv run gunicorn aiohttp_app:app --worker-class aiohttp.GunicornWebWorker

Двигаясь вперед, при отображении результатов теста a b я буду показывать только поле “Запросы в секунду” для краткости:

# terminal window 2
[email protected]:~/planettracker$ ab -k -c 20 -n 1000 "localhost:8000/planets/mars?lon=145.051&lat=-39.754&elevation=0"
...
Requests per second:    2396.24 [\#/sec] (mean)
...

Теперь давайте посмотрим, как работает приложение Flask :

# terminal window 1
[email protected]:~/planettracker$ pipenv run gunicorn flask_app:app

Тестирование с помощью ab :

# terminal window 2
[email protected]:~/planettracker$ ab -k -c 20 -n 1000 "localhost:8000/planets/mars?lon=145.051&lat=-39.754&elevation=0"
...
Requests per second:    1041.30 [\#/sec] (mean)
...

Использование gunicorn определенно приводит к повышению производительности как для приложений aiohttp , так и для приложений Flask . Приложение aiohttp по-прежнему работает лучше, хотя и не так сильно, как с сервером разработки.

gunicorn позволяет нам использовать несколько рабочих для обслуживания наших приложений. Мы можем использовать аргумент командной строки -w , чтобы сказать gunicorn , чтобы породить больше рабочих процессов. Использование 4 рабочих приводит к значительному повышению производительности наших приложений:

# terminal window 1
[email protected]:~/planettracker$ pipenv run gunicorn aiohttp_app:app -w 4

Тестирование с помощью ab :

# terminal window 2
[email protected]:~/planettracker$ ab -k -c 20 -n 1000 "localhost:8000/planets/mars?lon=145.051&lat=-39.754&elevation=0"
...
Requests per second:    2541.97 [\#/sec] (mean)
...

Переходим к версии Flask :

# terminal window 1
[email protected]:~/planettracker$ pipenv run gunicorn flask_app:app -w 4

Тестирование с помощью ab :

# terminal window 2
[email protected]:~/planettracker$ ab -k -c 20 -n 1000 "localhost:8000/planets/mars?lon=145.051&lat=-39.754&elevation=0"
...
Requests per second:    1729.17 [\#/sec] (mean)
...

Приложение Flask показало более значительный рост производительности при использовании нескольких рабочих!

Подведение Итогов

Давайте сделаем шаг назад и посмотрим на результаты тестирования серверов разработки и производства как для aiohttp , так и для Flask реализаций приложения our planet tracker в таблице:

721.92 2023.08 Сервер разработки (Запросы/сек) 180.24
1041.30 2396.24 gunicorn (Запросы/сек) 130.12
44.24 18.45 % увеличение по сравнению с сервером разработки
1729.17 2541.97 gunicorn -w 4 (Запросы/сек) 47.01
139.52 25.65 % увеличение по сравнению с сервером разработки

Вывод

В этой статье мы сравнили производительность асинхронного веб-приложения по сравнению с его синхронным аналогом и использовали для этого несколько инструментов.

Использование асинхронных библиотек Python и методов программирования потенциально может ускорить работу приложения, будь то выполнение запросов к удаленному серверу или обработка входящих запросов.