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

Введение Эта статья является второй частью серии статей об использовании Python для разработки асинхронных веб-приложений. В первой части более подробно рассматривается параллелизм в Python и asyncio, а также aiohttp. Если вы хотите узнать больше об асинхронном Python для веб-разработки [/ asynchronous-python-for-web-development /], у нас это есть. Из-за неблокирующей природы асинхронных библиотек, таких как aiohttp, мы надеемся, что сможем делать и обрабатывать больше запросов в

Вступление

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

comments powered by Disqus