Python e clientes HTTP rápidos

Atualmente, se você estiver escrevendo algum tipo de aplicativo Python, provavelmente precisará equipá-lo com a funcionalidade de um cliente HTTP capaz de se comunicar com servidores HTTP. A onipresença da API REST tornou as ferramentas HTTP um recurso respeitado em inúmeros projetos de software. É por isso que qualquer programador precisa possuir padrões destinados a organizar o trabalho ideal com conexões HTTP.



Existem muitos clientes HTTP para Python. O mais comum entre eles e, além disso, o mais fácil de trabalhar, pode ser chamado de solicitação . Hoje, esse cliente é o padrão de fato.

Conexões permanentes


A primeira otimização a considerar ao trabalhar com HTTP é usar conexões persistentes com servidores da web. As conexões persistentes tornaram-se padrão desde o HTTP 1.1, mas muitos aplicativos ainda não as utilizam. Essa falha é fácil de explicar, sabendo que ao usar a biblioteca de requests no modo simples (por exemplo, usando seu método get ), a conexão com o servidor é fechada após receber uma resposta dele. Para evitar isso, o aplicativo precisa usar o objeto Session , que permite reutilizar conexões abertas:

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

As conexões são armazenadas no conjunto de conexões (o padrão é 10 conexões por padrão). O tamanho da piscina pode ser personalizado:

 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") 

Reutilizar uma conexão TCP para enviar várias solicitações HTTP oferece ao aplicativo muitos benefícios de desempenho:

  • Reduzindo a carga no processador e reduzindo a necessidade de RAM (devido ao fato de menos conexões serem abertas ao mesmo tempo).
  • Redução de atrasos na execução de solicitações, uma após a outra (não há procedimento de handshake TCP).
  • Exceções podem ser lançadas sem tempo adicional para fechar a conexão TCP.

O HTTP 1.1 também suporta o pipelining de solicitações. Isso permite enviar várias solicitações dentro da mesma conexão sem aguardar respostas para solicitações enviadas anteriormente (ou seja, enviar solicitações em "pacotes"). Infelizmente, a biblioteca de requests não suporta esse recurso. No entanto, as solicitações de pipeline podem não ser tão rápidas quanto processá-las em paralelo. Além disso, é apropriado prestar atenção a isso: as respostas às solicitações de "pacotes" devem ser enviadas pelo servidor na mesma sequência em que foram recebidas. O resultado não é o esquema de processamento de solicitação mais eficiente baseado no princípio FIFO ("primeiro a entrar, primeiro a sair" - "primeiro a chegar, primeiro a sair").

Processamento de consulta paralela


requests também têm outra desvantagem séria. Esta é uma biblioteca síncrona. Uma chamada de método como requests.get("http://example.org") bloqueia o programa até que uma resposta completa do servidor HTTP seja recebida. O fato de o aplicativo ter que esperar e não fazer nada pode ser considerado menos esse esquema de organização da interação com o servidor. É possível fazer o programa fazer algo útil em vez de apenas esperar?

Um aplicativo com design inteligente pode atenuar esse problema usando um conjunto de encadeamentos, semelhante aos fornecidos pelo concurrent.futures . Isso permite que você paralelize rapidamente solicitações 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) 

Esse padrão muito útil é implementado na biblioteca de pedidos futuros . Ao mesmo tempo, o uso de objetos Session é transparente para o desenvolvedor:

 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) 

Por padrão, um trabalhador com dois threads é criado, mas o programa pode facilmente definir esse valor passando o argumento FuturSession ou mesmo seu próprio executor para o objeto FuturSession . Por exemplo, pode ser assim:

 FuturesSession(executor=ThreadPoolExecutor(max_workers=10)) 

Trabalho assíncrono com solicitações


Como já mencionado, a biblioteca de requests é completamente síncrona. Isso leva ao bloqueio de aplicativos enquanto aguarda uma resposta do servidor, o que afeta mal o desempenho. Uma solução para esse problema é executar solicitações HTTP em threads separados. Mas o uso de threads é uma carga adicional no sistema. Além disso, isso significa a introdução de um esquema de processamento de dados paralelo no programa, que não é adequado a todos.

A partir do Python 3.5, os recursos de linguagem padrão incluem programação assíncrona usando asyncio . A biblioteca aiohttp fornece ao desenvolvedor um cliente HTTP assíncrono com base no asyncio . Essa biblioteca permite que o aplicativo envie uma série de solicitações e continue trabalhando. Ao mesmo tempo, para enviar outra solicitação, você não precisa aguardar uma resposta para uma solicitação enviada anteriormente. Diferentemente das solicitações HTTP de pipelining, o aiohttp envia solicitações em paralelo usando várias conexões. Isso evita o "problema FIFO" descrito acima. Aqui está a aparência do uso do 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) 

Todas as abordagens descritas acima (usando Session , streams, concurrent.futures ou asyncio ) oferecem maneiras diferentes de acelerar os clientes HTTP.

Desempenho


O código a seguir é um exemplo no qual o cliente HTTP envia solicitações ao servidor httpbin.org . O servidor suporta uma API que pode, entre outras coisas, simular um sistema que demora muito para responder a uma solicitação (nesse caso, é de 1 segundo). Aqui, todas as técnicas discutidas acima são implementadas e seu desempenho é medido:

 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)])) 

Aqui estão os resultados obtidos após o início deste 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 

Aqui está um gráfico dos resultados.

Os resultados de um estudo do desempenho de diferentes métodos para fazer solicitações HTTP

Não é de surpreender que o esquema de execução de consulta síncrona mais simples tenha sido o mais lento. O ponto aqui é que aqui as consultas são executadas uma a uma, sem reutilizar a conexão. Como resultado, são necessários 12 segundos para concluir 10 consultas.

Usar o objeto Session e, como resultado, reutilizar conexões, economiza 8% do tempo. Isso já é muito bom, e conseguir isso é muito simples. Quem se preocupa com o desempenho deve usar pelo menos o objeto Session .

Se seu sistema e seu programa permitem trabalhar com threads, esse é um bom motivo para pensar em usar threads para paralelizar solicitações. Os fluxos, no entanto, criam uma carga adicional no sistema; eles são, por assim dizer, não "livres". Eles precisam ser criados, executados, é preciso aguardar a conclusão do trabalho deles.

Se você deseja usar o cliente HTTP assíncrono rápido, se não estiver escrevendo em versões mais antigas do Python, preste muita atenção ao aiohttp . Esta é a solução mais rápida e melhor escalável. É capaz de lidar com centenas de solicitações simultâneas.

Uma alternativa ao aiohttp , e não uma alternativa particularmente boa, é gerenciar centenas de threads em paralelo.

Processamento de dados de fluxo


Outra otimização do trabalho com recursos de rede, que pode ser útil em termos de melhoria do desempenho do aplicativo, é usar dados de streaming. O esquema de processamento de solicitações padrão se parece com o seguinte: o aplicativo envia uma solicitação, após a qual o corpo dessa solicitação é carregado de uma só vez. O parâmetro stream , que suporta a biblioteca de requests , bem como o atributo content da biblioteca aiohttp , permite que você se afaste desse esquema.

Veja como é a organização do processamento de dados de streaming usando requests :

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

Veja como transmitir dados usando o 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 a necessidade de carregamento instantâneo do conteúdo completo da resposta é importante nos casos em que é necessário evitar a possibilidade potencial de alocação inútil de centenas de megabytes de memória. Se o programa não precisar acessar a resposta como um todo, se puder trabalhar com fragmentos individuais da resposta, provavelmente é melhor recorrer a métodos de fluxo de trabalho com solicitações. Por exemplo, se você deseja salvar dados da resposta do servidor a um arquivo, a leitura e a gravação em partes serão muito mais eficientes em termos de uso de memória do que a leitura de todo o corpo da resposta, a alocação de uma quantidade enorme de memória e a gravação em disco.

Sumário


Espero que minha palestra sobre diferentes maneiras de otimizar a operação de clientes HTTP o ajude a escolher o que melhor se adequa ao seu aplicativo Python.

Caros leitores! Se você ainda conhece outras maneiras de otimizar o trabalho com solicitações HTTP em aplicativos Python, compartilhe-as.


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


All Articles