Вступление
Эта статья является второй частью серии об использовании Python для
разработки асинхронных веб-приложений. В первой части более подробно
asyncio
параллелизм в Python и asyncio, а также aiohttp
.
Если вы хотите узнать больше об асинхронном Python для веб-разработки , у нас это есть.
Из-за неблокирующей природы асинхронных библиотек, таких как aiohttp
мы надеемся, что сможем делать и обрабатывать больше запросов за
заданный промежуток времени по сравнению с аналогичным синхронным кодом.
Это связано с тем, что асинхронный код может быстро переключаться между
контекстами, чтобы минимизировать время ожидания ввода-вывода.
Клиентская и серверная производительность
Тестирование производительности асинхронной библиотеки, такой как
aiohttp
стороне клиента относительно несложно. Мы выбираем какой-либо
веб-сайт в качестве ссылки, а затем делаем определенное количество
запросов, определяя, сколько времени потребуется нашему коду для их
выполнения. Мы рассмотрим относительную производительность aiohttp
и
requests
при отправке запросов на https://example.com
.
Проверить производительность на стороне сервера немного сложнее. Такие
библиотеки, как aiohttp
поставляются со встроенными серверами
разработки, которые подходят для тестирования маршрутов в локальной
сети. Однако эти серверы разработки не подходят для развертывания
приложений в общедоступной сети, поскольку они не могут справиться с
нагрузкой, ожидаемой от общедоступного веб-сайта, и не подходят для
обслуживания статических ресурсов, таких как Javascript, CSS и файлы
изображений.
Чтобы получить лучшее представление об относительной производительности
aiohttp
и аналогичной синхронной веб-инфраструктуры, мы собираемся
повторно реализовать наше веб-приложение с помощью
Flask, а затем сравним серверы разработки и
производства для обеих реализаций.
Для производственного сервера мы будем использовать Gunicorn .
На стороне клиента: aiohttp vs запросы
Для традиционного синхронного подхода мы просто используем простой цикл
for
Однако перед запуском кода обязательно установите модуль
запросов :
$ 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 против Flask
Мы будем использовать инструмент Apache Benchmark (ab) для тестирования производительности различных серверов.
С помощью ab
мы можем указать общее количество запросов, которые нужно
сделать, в дополнение к количеству одновременных запросов, которые
нужно сделать.
Прежде чем мы сможем начать тестирование, мы должны заново реализовать
наше приложение для отслеживания планет (из предыдущей
статьи ), используя
синхронный фреймворк. Мы будем использовать Flask
, поскольку API
похож на aiohttp
(на самом 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
, который сам является подкаталогом
моей домашней папки.
[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 и Flask, обслуживаемые gunicorn
Прежде чем мы сможем протестировать наши приложения в производственном
режиме, мы должны сначала установить gunicorn
и выяснить, как
запускать наши приложения, используя соответствующий рабочий класс
gunicorn
Чтобы протестировать Flask
, мы можем использовать
стандартный gunicorn
, но для aiohttp
мы должны использовать
рабочий- gunicorn
aiohttp
. Мы можем установить gunicorn
с помощью
pipenv:
[email protected] ~/planettracker$ pipenv install gunicorn
Мы можем запустить aiohttp
с соответствующим gunicorn
:
# terminal window 1
[email protected] :~/planettracker$ pipenv run gunicorn aiohttp_app:app --worker-class aiohttp.GunicornWebWorker
Продвигаясь вперед, при отображении 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: 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
нашего
приложения для отслеживания планет в таблице:
aiohttp Колба \% разница
Сервер разработки (запросов / сек) 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
Заключение
В этой статье мы сравнили производительность асинхронного веб-приложения по сравнению с его синхронным аналогом и использовали для этого несколько инструментов.
Использование асинхронных библиотек Python и методов программирования
может ускорить работу приложения, независимо от того, отправляет ли оно
запросы на удаленный сервер или
обработка входящих запросов.