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")
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 HTTPNã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
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.
