IT & Engineering
Tests de charge HTTP avec Twisted Python et Treq
Le secteur des API a ses défis, et maintenir la robustesse du système aux heures de pointe en fait partie. C’est pourquoi nous effectuons de nombreux tests de charge chez Mailgun.
Au fil du temps, nous avons essayé de nombreuses approches, allant du simple test Apache Bench à des suites de tests personnalisées plus complexes. Mais cet article porte sur un test de charge « rapide et sans fioritures », mais très flexible, utilisant Python.
Pour l’écriture de clients HTTP en Python, nous apprécions particulièrement la bibliothèque Requests. C’est ce que nous recommandons aux utilisateurs de notre API. Requests est formidable, mais elle a une faiblesse : il s’agit d’un système bloquant à un seul appel par thread. Il est difficile voire impossible de générer des dizaines de milliers de requêtes rapidement avec elle.
Présentation de Treq sur Twisted
Pour résoudre ce problème, nous nous sommes penchés sur Treq (dépôt Github). Treq est une bibliothèque de client HTTP inspirée par Requests, mais elle fonctionne sur Twisted et en possède les capacités typiques : elle est asynchrone et hautement concurrente en matière d’entrées/sorties réseau.
Treq n’est pas du tout spécifique aux tests de charge : c’est un excellent outil pour écrire des clients HTTP hautement concurrents en général, comme les robots d’indexation. Treq est élégante, simple à utiliser et puissante. Voici un exemple :
>>> 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
Le script de test simple
Vous trouverez ci-dessous un script simple qui utilise Treq pour bombarder une URL unique avec le nombre maximum de requêtes possible.
#!/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()
Le résultat :
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
Les requêtes « Generated » sont celles qui ont été préparées, mais que le réacteur Twisted n’a pas encore envoyées. Ce script ignore toutes les erreurs par souci de simplicité. L’ajout des statistiques pour les dépassements de délai est laissé à titre d’exercice.
Ce script peut servir de point de départ, être amélioré et étendu avec votre propre logique personnalisée spécifique à l’application. Une amélioration suggérée serait d’utiliser collections.Counter au lieu des disgracieuses variables globales. Le script s’exécute sur un seul thread. Pour tirer le maximum de requêtes d’une machine, un outil tel que multiprocessing peut être utilisé.
Bons tests de charge !
Cordialement,
L’équipe Mailgun