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