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.
Image pour 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