IT & Engineering
Teste de carga HTTP com Twisted Python e Treq
Estar no mercado de APIs tem seus desafios, e manter a robustez do sistema nos horários de pico é um deles. É por isso que fazemos muitos testes de carga aqui na Mailgun.
Tentamos diversas abordagens ao longo do tempo, de um simples Apache Bench a suítes de teste personalizadas mais complexas. Mas esta publicação é sobre um teste de carga “rápido e improvisado”, porém muito flexível, usando Python.
Quando se trata de programar clientes HTTP em Python, somos fãs da biblioteca Requests. É isso que recomendamos aos usuários da nossa API. A Requests é ótima, mas tem um ponto fraco: o bloqueio de uma chamada por thread. É difícil ou impossível gerar dezenas de milhares de solicitações rapidamente com ela.
Apresentando o Treq no Twisted
Para resolver esse problema, avaliamos o Treq (repositório do Github). O Treq é uma biblioteca de clientes HTTP inspirada na Requests, mas executada no Twisted, e possui as vantagens típicas do Twisted: é assíncrona e altamente simultânea quando se trata de E/S de rede.
O Treq não é específico para testes de carga: é uma ótima ferramenta para criar clientes HTTP de alta simultaneidade no geral, como os rastreadores da web. O Treq é elegante, simples de usar e poderoso. Aqui está um exemplo:
>>> from treq import get
>>> def done(response):
... print response.code
... reactor.stop()
>>> get("http://www.github.com").addCallback(done)
>>> from twisted.internet import reactor
>>> reactor.run() 200
O script de teste simples
Abaixo, mostramos um script simples que usa o Treq para bombardear um único URL com o maior número possível de solicitações.
#!/usr/bin/env python
from twisted.internet import epollreactor
epollreactor.install()
from twisted.internet import reactor, task
from twisted.web.client import HTTPConnectionPool
import treq
import random
from datetime import datetime
req_generated = 0
req_made = 0
req_done = 0
cooperator = task.Cooperator()
pool = HTTPConnectionPool(reactor)
def counter():
'''This function gets called once a second and prints the progress at one
second intervals.
'''
print("Requests: {} generated; {} made; {} done".format(
req_generated, req_made, req_done))
# reset the counters and reschedule ourselves
req_generated = req_made = req_done = 0
reactor.callLater(1, counter)
def body_received(body):
global req_done
req_done += 1
def request_done(response):
global req_made
deferred = treq.json_content(response)
req_made += 1
deferred.addCallback(body_received)
deferred.addErrback(lambda x: None) # ignore errors
return deferred
def request():
deferred = treq.post('http://api.host/v2/loadtest/messages',
auth=('api', 'api-key'),
data={'from': 'Loadtest ',
'to': 'to@example.org',
'subject': "test"},
pool=pool)
deferred.addCallback(request_done)
return deferred
def requests_generator():
global req_generated
while True:
deferred = request()
req_generated += 1
# do not yield deferred here so cooperator won't pause until
# response is received
yield None
if __name__ == '__main__':
# make cooperator work on spawning requests
cooperator.cooperate(requests_generator())
# run the counter that will be reporting sending speed once a second
reactor.callLater(1, counter)
# run the reactor
reactor.run()
A saída:
2013-04-25 09:30 Requests: 327 generated; 153 sent; 153 received
2013-04-25 09:30 Requests: 306 generated; 156 sent; 156 received
2013-04-25 09:30 Requests: 318 generated; 184 sent; 154 received
As solicitações marcadas como “Generated” são aquelas que já foram preparadas, mas que o reator do Twisted ainda não enviou. Por simplicidade, este script ignora todos os erros. Incluir as estatísticas de tempo limite fica como um exercício para você.
O script pode ser usado como um ponto de partida, sendo aprimorado e ampliado com a sua própria lógica personalizada e específica do aplicativo. Uma sugestão de melhoria seria usar collections.Counter em vez das deselegantes variáveis globais. O script é executado em uma única thread. Para conseguir o número máximo de solicitações de uma máquina, tente usar algo como a multiprocessing pode ser usado.
Bons testes de carga!
Abraços,
Mailgunners