IT & Engineering

HTTP-Stresstests mit Twisted Python und Treq

Das API-Geschäft bringt Herausforderungen mit sich. Die Stabilität des Systems zu Spitzenzeiten aufrechtzuerhalten, ist eine davon. Aus diesem Grund führen wir hier bei Mailgun viele Stresstests durch.
Bild für HTTP-Stresstests mit Twisted Python und Treq

Das API-Geschäft bringt Herausforderungen mit sich. Die Stabilität des Systems zu Spitzenzeiten aufrechtzuerhalten, ist eine davon. Aus diesem Grund führen wir hier bei Mailgun viele Stresstests durch.

Im Laufe der Zeit haben wir viele verschiedene Ansätze ausprobiert, von einfachen Apache-Benchmarks bis hin zu komplexeren, benutzerdefinierten Test-Suites. In diesem Beitrag geht es jedoch um „Quick-and-Dirty-Stresstests“, die dank Python dennoch sehr flexibel sind.

Wenn es darum geht, HTTP-Clients in Python zu schreiben, sind wir Fans der Requests-Bibliothek. Das empfehlen wir auch allen, die unsere API nutzen. Requests ist großartig, hat aber eine Schwäche: Es handelt sich um eine blockierende Angelegenheit mit einem Aufruf pro Thread. Es ist schwer oder gar unmöglich, damit schnell Zehntausende von Anfragen zu generieren.

Wir stellen vor: Treq auf Twisted

Um dieses Problem zu lösen, haben wir uns Treq angesehen (Github-Repository). Treq ist eine HTTP-Client-Bibliothek, die von Requests inspiriert wurde. Sie läuft jedoch auf Twisted und besitzt dessen typische Stärken: Sie arbeitet asynchron und bei Netzwerk-E/A hochgradig parallel.

Treq ist keineswegs nur auf Stresstests beschränkt. Es ist ein hervorragendes Tool, um allgemein hochparallele HTTP-Clients wie Webcrawler zu schreiben. Treq ist elegant, einfach zu bedienen und leistungsstark. Hier ist ein Beispiel:

                                

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

Das einfache Testskript

Nachfolgend finden Sie ein einfaches Skript, das Treq verwendet, um eine einzelne URL mit der maximal möglichen Anzahl an Anfragen zu bombardieren.

                                

                                     #!/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()
                                
                            

Die Ausgabe:

                                

                                    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
                                
                            

Die „Generated“-Anfragen sind diejenigen, die zwar vorbereitet, aber vom Twisted-Reactor noch nicht gesendet wurden. Der Einfachheit halber ignoriert dieses Skript alle Fehler. Das Hinzufügen der Statistiken für Timeouts überlassen wir als Übung der Leserschaft.

Das Skript kann als Ausgangspunkt dienen und verbessert sowie erweitert werden, und zwar mit Ihrer eigenen anwendungsspezifischen Logik. Eine empfohlene Verbesserung wäre die Verwendung von collections.Counter anstelle der unschönen globalen Variablen. Das Skript läuft in einem einzigen Thread. Um die maximale Anzahl an Anfragen aus einer Maschine herauszuholen, ist so etwas wie multiprocessing verwendet werden kann.

Viel Spaß bei den Stresstests!

Viele Grüße
Ihr Mailgun-Team