Análisis de rendimiento de Python asíncrono vs síncrono

A

Introducción

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.

Lado 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:

[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.

Servidores 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

Servidor de desarrollo (solicitudes / seg)2023.08721,92180,24
gunicorn (Solicitudes / seg)2396.241041.30130.12
% de aumento sobre el servidor de desarrollo18.4544,24
gunicorn -w 4 (Solicitudes / seg)2541,971729.1747.01
% de aumento sobre el servidor de desarrollo25,65139,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.

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad