Python y clientes HTTP rápidos

Hoy en día, si está escribiendo algún tipo de aplicación Python, lo más probable es que tenga que equiparla con la funcionalidad de un cliente HTTP que pueda comunicarse con los servidores HTTP. La ubicuidad de la API REST ha hecho de las herramientas HTTP una característica respetada en innumerables proyectos de software. Es por eso que cualquier programador necesita poseer patrones destinados a organizar un trabajo óptimo con conexiones HTTP.



Hay muchos clientes HTTP para Python. Los más comunes entre ellos y, además, con el que es fácil trabajar, se pueden llamar solicitudes . Hoy, este cliente es el estándar de facto.

Conexiones permanentes


La primera optimización a tener en cuenta al trabajar con HTTP es utilizar conexiones persistentes a los servidores web. Las conexiones persistentes se han convertido en estándar desde HTTP 1.1, pero muchas aplicaciones aún no las usan. Esta falla es fácil de explicar, sabiendo que cuando se usa la biblioteca de requests en modo simple (por ejemplo, usando su método get ), la conexión al servidor se cierra después de recibir una respuesta de este. Para evitar esto, la aplicación necesita usar el objeto Session , que permite reutilizar conexiones abiertas:

 import requests session = requests.Session() session.get("http://example.com") #    session.get("http://example.com") 

Las conexiones se almacenan en el grupo de conexiones (el valor predeterminado es 10 conexiones por defecto). El tamaño de la piscina se puede personalizar:

 import requests session = requests.Session() adapter = requests.adapters.HTTPAdapter(    pool_connections=100,    pool_maxsize=100) session.mount('http://', adapter) response = session.get("http://example.org") 

La reutilización de una conexión TCP para enviar múltiples solicitudes HTTP le brinda a la aplicación muchos beneficios de rendimiento:

  • Reduciendo la carga en el procesador y reduciendo la necesidad de RAM (debido al hecho de que se abren menos conexiones al mismo tiempo).
  • Reducción de demoras al ejecutar solicitudes una tras otra (no hay un procedimiento de protocolo de enlace TCP).
  • Se pueden generar excepciones sin tiempo adicional para cerrar la conexión TCP.

HTTP 1.1 también admite la canalización de solicitudes. Esto le permite enviar múltiples solicitudes dentro de la misma conexión sin esperar respuestas a solicitudes enviadas previamente (es decir, enviar solicitudes en "paquetes"). Lamentablemente, la biblioteca de requests no admite esta función. Sin embargo, las solicitudes de canalización pueden no ser tan rápidas como procesarlas en paralelo. Y, además, es apropiado prestar atención a esto: las respuestas a las solicitudes de "paquete" deben ser enviadas por el servidor en la misma secuencia en que recibió estas solicitudes. El resultado no es el esquema de procesamiento de solicitudes más eficiente basado en el principio FIFO (“primero en entrar, primero en salir” - “primero en llegar, primero en salir”).

Procesamiento de consultas paralelas


requests también tienen otro inconveniente grave. Esta es una biblioteca síncrona. Una llamada a un método como requests.get("http://example.org") bloquea el programa hasta que se recibe una respuesta completa del servidor HTTP. El hecho de que la aplicación tenga que esperar y no hacer nada puede considerarse un inconveniente de este esquema de organización de interacción con el servidor. ¿Es posible hacer que el programa haga algo útil en lugar de solo esperar?

Una aplicación de diseño inteligente puede mitigar este problema mediante el uso de un grupo de subprocesos, similar a los proporcionados por concurrent.futures . Esto le permite paralelizar rápidamente las solicitudes HTTP:

 from concurrent import futures import requests with futures.ThreadPoolExecutor(max_workers=4) as executor:    futures = [        executor.submit(            lambda: requests.get("http://example.org"))        for _ in range(8)    ] results = [    f.result().status_code    for f in futures ] print("Results: %s" % results) 

Este patrón muy útil se implementa en la biblioteca de solicitudes de futuros . Al mismo tiempo, el uso de objetos Session es transparente para el desarrollador:

 from requests_futures import sessions session = sessions.FuturesSession() futures = [    session.get("http://example.org")    for _ in range(8) ] results = [    f.result().status_code    for f in futures ] print("Results: %s" % results) 

De manera predeterminada, se crea un trabajador con dos subprocesos, pero el programa puede establecer fácilmente este valor al pasar el argumento FuturSession o incluso su propio ejecutor al objeto FuturSession . Por ejemplo, podría verse así:

 FuturesSession(executor=ThreadPoolExecutor(max_workers=10)) 

Trabajo asincrónico con solicitudes


Como ya se mencionó, la biblioteca de requests es completamente sincrónica. Esto conduce al bloqueo de la aplicación mientras se espera una respuesta del servidor, lo que afecta el rendimiento de manera deficiente. Una solución a este problema es ejecutar solicitudes HTTP en subprocesos separados. Pero el uso de hilos es una carga adicional en el sistema. Además, esto significa la introducción de un esquema paralelo de procesamiento de datos en el programa, que no se adapta a todos.

Comenzando con Python 3.5, las características del lenguaje estándar incluyen programación asincrónica usando asyncio . La biblioteca aiohttp proporciona al desarrollador un cliente HTTP asíncrono basado en asyncio . Esta biblioteca permite que la aplicación envíe una serie de solicitudes y continúe trabajando. Al mismo tiempo, para enviar otra solicitud, no necesita esperar una respuesta a una solicitud enviada anteriormente. A diferencia de canalizar solicitudes HTTP, aiohttp envía solicitudes en paralelo utilizando múltiples conexiones. Esto evita el "problema FIFO" descrito anteriormente. aiohttp es como se ve el uso de aiohttp :

 import aiohttp import asyncio async def get(url):    async with aiohttp.ClientSession() as session:        async with session.get(url) as response:            return response loop = asyncio.get_event_loop() coroutines = [get("http://example.com") for _ in range(8)] results = loop.run_until_complete(asyncio.gather(*coroutines)) print("Results: %s" % results) 

Todos los enfoques descritos anteriormente (usando Session , streams, concurrent.futures o asyncio ) ofrecen diferentes formas de acelerar los clientes HTTP.

Rendimiento


El siguiente código es un ejemplo en el que el cliente HTTP envía solicitudes al servidor httpbin.org . El servidor admite una API que puede, entre otras cosas, simular un sistema que tarda mucho en responder a una solicitud (en este caso, es de 1 segundo). Aquí, se implementan todas las técnicas discutidas anteriormente y se mide su rendimiento:

 import contextlib import time import aiohttp import asyncio import requests from requests_futures import sessions URL = "http://httpbin.org/delay/1" TRIES = 10 @contextlib.contextmanager def report_time(test):    t0 = time.time()    yield    print("Time needed for `%s' called: %.2fs"          % (test, time.time() - t0)) with report_time("serialized"):    for i in range(TRIES):        requests.get(URL) session = requests.Session() with report_time("Session"):    for i in range(TRIES):        session.get(URL) session = sessions.FuturesSession(max_workers=2) with report_time("FuturesSession w/ 2 workers"):    futures = [session.get(URL)               for i in range(TRIES)]    for f in futures:        f.result() session = sessions.FuturesSession(max_workers=TRIES) with report_time("FuturesSession w/ max workers"):    futures = [session.get(URL)               for i in range(TRIES)]    for f in futures:        f.result() async def get(url):    async with aiohttp.ClientSession() as session:        async with session.get(url) as response:            await response.read() loop = asyncio.get_event_loop() with report_time("aiohttp"):    loop.run_until_complete(        asyncio.gather(*[get(URL)                         for i in range(TRIES)])) 

Aquí están los resultados obtenidos después de comenzar este programa:

 Time needed for `serialized' called: 12.12s Time needed for `Session' called: 11.22s Time needed for `FuturesSession w/ 2 workers' called: 5.65s Time needed for `FuturesSession w/ max workers' called: 1.25s Time needed for `aiohttp' called: 1.19s 

Aquí hay una tabla de los resultados.

Los resultados de un estudio sobre el rendimiento de diferentes métodos para ejecutar solicitudes HTTP

No es sorprendente que el esquema de ejecución de consulta síncrona más simple resultó ser el más lento. El punto aquí es que aquí las consultas se ejecutan una por una, sin reutilizar la conexión. Como resultado, lleva 12 segundos completar 10 consultas.

El uso del objeto Session y, como resultado, la reutilización de las conexiones, ahorra el 8% del tiempo. Esto ya es muy bueno, y lograr esto es muy simple. Cualquiera que se preocupe por el rendimiento debe usar al menos el objeto Session .

Si su sistema y su programa le permiten trabajar con hilos, entonces esta es una buena razón para pensar en usar hilos para paralelizar las solicitudes. Sin embargo, las transmisiones crean una carga adicional en el sistema, por así decirlo, no son "gratuitas". Deben crearse, ejecutarse, debe esperar la finalización de su trabajo.

Si desea utilizar el cliente HTTP asíncrono rápido, entonces si no está escribiendo en versiones anteriores de Python, debe prestar la mayor atención a aiohttp . Esta es la solución más rápida, mejor escalable. Es capaz de manejar cientos de solicitudes concurrentes.

Una alternativa a aiohttp , no una alternativa particularmente buena, es administrar cientos de hilos en paralelo.

Procesamiento de datos de flujo


Otra optimización para trabajar con recursos de red, que puede ser útil en términos de mejorar el rendimiento de la aplicación, es utilizar la transmisión de datos. El esquema de procesamiento de solicitudes estándar tiene este aspecto: la aplicación envía una solicitud, después de lo cual el cuerpo de esta solicitud se carga de una vez. El parámetro de stream , que admite la biblioteca de requests , así como el atributo de content de la biblioteca aiohttp , le permite alejarse de este esquema.

Así es como se ve la organización del procesamiento de datos de transmisión mediante requests :

 import requests #  `with`          #     . with requests.get('http://example.org', stream=True) as r:    print(list(r.iter_content())) 

Aquí le mostramos cómo transmitir datos usando aiohttp :

 import aiohttp import asyncio async def get(url):    async with aiohttp.ClientSession() as session:        async with session.get(url) as response:            return await response.content.read() loop = asyncio.get_event_loop() tasks = [asyncio.ensure_future(get("http://example.com"))] loop.run_until_complete(asyncio.wait(tasks)) print("Results: %s" % [task.result() for task in tasks]) 

Eliminar la necesidad de cargar instantáneamente el contenido de la respuesta completa es importante en los casos en que necesite evitar la posibilidad potencial de asignación inútil de cientos de megabytes de memoria. Si el programa no necesita acceso a la respuesta en su conjunto, si puede trabajar con fragmentos individuales de la respuesta, entonces probablemente sea mejor recurrir a métodos de transmisión de trabajo con solicitudes. Por ejemplo, si va a guardar datos de la respuesta del servidor a un archivo, leerlo y escribirlo en partes será mucho más eficiente en términos de uso de memoria que leer todo el cuerpo de la respuesta, asignar una gran cantidad de memoria y luego escribirlo todo en el disco.

Resumen


Espero que mi charla sobre las diferentes formas de optimizar el funcionamiento de los clientes HTTP lo ayude a elegir lo que mejor se adapte a su aplicación Python.

Estimados lectores! Si aún conoce otras formas de optimizar el trabajo con solicitudes HTTP en aplicaciones Python, compártalas.


Source: https://habr.com/ru/post/472858/


All Articles