Вступление
Эта статья является второй частью серии статей об использовании 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 и методов программирования потенциально может ускорить работу приложения, будь то выполнение запросов к удаленному серверу или обработка входящих запросов.