An谩lisis de rendimiento de Python as铆ncrono vs s铆ncrono

    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.

     

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *