Introducción
Contenido
Este artículo es la segunda parte de una serie sobre el uso de Python para desarrollar aplicaciones web asincrónicas. La primera parte proporciona una cobertura más profunda de la concurrencia en Python y asyncio
, tanto como aiohttp
.
Si desea leer más sobre Python asincrónico para desarrollo web, lo tenemos cubierto.
Debido a la naturaleza no bloqueante de las bibliotecas asincrónicas como aiohttp
Esperamos poder hacer y manejar más solicitudes en un período de tiempo determinado en comparación con el código síncrono análogo. Esto se debe al hecho de que el código asincrónico puede cambiar rápidamente entre contextos para minimizar el tiempo de espera de E / S.
Rendimiento del lado del cliente frente al del lado del servidor
Probando el rendimiento del lado del cliente de una biblioteca asincrónica como aiohttp
es relativamente sencillo. Elegimos algún sitio web como referencia y luego hacemos una cierta cantidad de solicitudes, cronometrando cuánto tiempo tarda nuestro código en completarlas. Observaremos el rendimiento relativo de aiohttp
y requests
al hacer solicitudes a https://example.com
.
Probar el rendimiento del lado del servidor es un poco más complicado. Bibliotecas como aiohttp
vienen con servidores de desarrollo integrados, que están bien para probar rutas en una red local. Sin embargo, estos servidores de desarrollo no son adecuados para implementar aplicaciones en la web pública, ya que no pueden manejar la carga esperada de un sitio web disponible públicamente y no son buenos para servir activos estáticos, como Javascript, CSS y archivos de imagen.
Para tener una mejor idea del desempeño relativo de aiohttp
y un marco web sincrónico análogo, vamos a volver a implementar nuestra aplicación web usando Matraz y luego compararemos los servidores de desarrollo y producción para ambas implementaciones.
Para el servidor de producción, usaremos gunicorn.
Te puede interesar:Cómo crear, mover y eliminar archivos en PythonLado del cliente: aiohttp vs solicitudes
Para un enfoque tradicional y sincrónico, usamos un simple for
lazo. Sin embargo, antes de ejecutar el código, asegúrese de instalar el módulo de solicitudes:
$ pip install --user requests
Con eso fuera del camino, sigamos adelante e implementémoslo de una manera más tradicional:
# 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()
Sin embargo, el código asincrónico análogo es un poco más complicado. Realizar múltiples solicitudes con aiohttp
aprovecha el asyncio.gather
método para realizar solicitudes al mismo tiempo:
# 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())
Ejecutar código síncrono y asincrónico con bash hora utilidad:
[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
El código concurrente / asincrónico es mucho más rápido. Pero, ¿qué sucede si utilizamos varios subprocesos del código síncrono? ¿Podría igualar la velocidad del código concurrente?
# 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()
Ejecutar este código bastante detallado producirá:
[email protected]:~$ time python multiple_sync_request_threaded.py -nt 10
real 0m2.170s
user 0m0.942s
sys 0m0.104s
Y podemos aumentar el rendimiento utilizando más subprocesos, pero los retornos disminuyen rápidamente:
Te puede interesar:Aplicación de métodos de filtrado en Python para la selección de características[email protected]:~$ time python multiple_sync_request_threaded.py -nt 20
real 0m1.714s
user 0m1.126s
sys 0m0.119s
Al introducir el subproceso, podemos acercarnos a igualar el rendimiento del código asincrónico, a costa de una mayor complejidad del código.
Si bien ofrece un tiempo de respuesta similar, no vale la pena por el precio de complicar el código que podría ser simple: la calidad del código no aumenta por la complejidad o la cantidad de líneas que usamos.
Lado del servidor: aiohttp vs Flask
Usaremos el Benchmark de Apache (ab) herramienta para probar el rendimiento de diferentes servidores.
Con ab
podemos especificar el número total de solicitudes a realizar, además del número de solicitudes simultáneas a realizar.
Antes de que podamos comenzar a probar, tenemos que volver a implementar nuestra aplicación de seguimiento de planetas (del artículo anterior) utilizando un marco sincrónico. Usaremos Flask
, ya que la API es similar a aiohttp
(en realidad el aiohttp
La API de enrutamiento se basa en 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("https://Pharos.sh.com/")
def hello():
return render_template("index.html")
if __name__ == "__main__":
app.run(
host="localhost",
port=8000,
threaded=True
)
Si está saltando sin leer el artículo anterior, tenemos que configurar nuestro proyecto un poco antes de probarlo. Puse todo el código del servidor Python en un directorio planettracker
, en sí mismo un subdirectorio de mi carpeta de inicio.
[email protected]:~/planettracker$ ls
planet_tracker.py
flask_app.py
aiohttp_app.py
Le sugiero que visite el artículo anterior y se familiarice con la aplicación que ya hemos creado antes de continuar.
Te puede interesar:Manejo de señales Unix en PythonServidores de desarrollo aiohttp y Flask
Veamos cuánto tardan nuestros servidores en manejar 1000 solicitudes, 20 a la vez.
Primero, abriré dos Windows de terminal. En el primero, ejecuto el servidor:
# terminal window 1
[email protected]:~/planettracker$ pipenv run python aiohttp_app.py
En el segundo, corramos 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
genera mucha información, y solo he mostrado el bit más relevante. De este número, el número al que debemos prestar más atención es el campo «Solicitudes por segundo».
Ahora, saliendo del servidor en la primera ventana, encienda nuestro Flask
aplicación:
# terminal window 1
[email protected]:~/planettracker$ pipenv run python flask_app.py
Ejecutando el script de prueba nuevamente:
# 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
...
Parece el aiohttp
La aplicación es de 2,5 a 3 veces más rápida que la Flask
al usar el servidor de desarrollo respectivo de cada biblioteca.
¿Qué pasa si usamos gunicorn
para servir nuestras aplicaciones?
aiohttp y Flask según lo servido por gunicorn
Antes de que podamos probar nuestras aplicaciones en modo de producción, primero tenemos que instalar gunicorn
y descubra cómo ejecutar nuestras aplicaciones con un gunicorn
clase trabajadora. Para probar el Flask
aplicación podemos usar el estándar gunicorn
trabajador, pero para aiohttp
tenemos que usar el gunicorn
trabajador incluido con aiohttp
. Podemos instalar gunicorn
con pipenv:
[email protected]~/planettracker$ pipenv install gunicorn
Podemos ejecutar el aiohttp
aplicación con el apropiado gunicorn
trabajador:
# terminal window 1
[email protected]:~/planettracker$ pipenv run gunicorn aiohttp_app:app --worker-class aiohttp.GunicornWebWorker
Avanzando, al mostrar ab
Resultados de la prueba Solo voy a mostrar el campo «Solicitudes por segundo» en aras de la brevedad:
# 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)
...
Ahora veamos cómo Flask
tarifas de la aplicación:
# terminal window 1
[email protected]:~/planettracker$ pipenv run gunicorn flask_app:app
Probando con 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)
...
Utilizando gunicorn
definitivamente resulta en un mayor rendimiento tanto para el aiohttp
y Flask
aplicaciones. los aiohttp
La aplicación aún funciona mejor, aunque no por un margen tanto como con el servidor de desarrollo.
gunicorn
nos permite utilizar varios trabajadores para ofrecer nuestras aplicaciones. Podemos usar el -w
argumento de línea de comando para contar gunicorn
para generar más procesos de trabajo. El uso de 4 trabajadores da como resultado un aumento significativo en el rendimiento de nuestras aplicaciones:
# terminal window 1
[email protected]:~/planettracker$ pipenv run gunicorn aiohttp_app:app -w 4
Probando con 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)
...
Moviéndose en el Flask
versión:
# terminal window 1
[email protected]:~/planettracker$ pipenv run gunicorn flask_app:app -w 4
Probando con 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)
...
los Flask
¡La aplicación vio un aumento más significativo en el rendimiento cuando se utilizan varios trabajadores!
Resumen de resultados
Demos un paso atrás y veamos los resultados de probar los servidores de desarrollo y producción para ambos aiohttp
y Flask
implementaciones de nuestra aplicación de seguimiento de planetas en una tabla:
aiohttp Flask% de diferencia
Te puede interesar:Biblioteca Seaborn para la visualización de datos en Python: Parte 2Servidor de desarrollo (solicitudes / seg) | 2023.08 | 721,92 | 180,24 |
gunicorn (Solicitudes / seg) | 2396.24 | 1041.30 | 130.12 |
% de aumento sobre el servidor de desarrollo | 18.45 | 44,24 | |
gunicorn -w 4 (Solicitudes / seg) | 2541,97 | 1729.17 | 47.01 |
% de aumento sobre el servidor de desarrollo | 25,65 | 139,52 |
Conclusión
En este artículo, comparamos el rendimiento de una aplicación web asíncrona con su contraparte síncrona y usamos varias herramientas para hacerlo.
El uso de bibliotecas Python asíncronas y técnicas de programación tiene el potencial de acelerar una aplicación, ya sea que realice solicitudes a un servidor remoto o
Manejo de solicitudes entrantes.