
Der ToxiProxy ist eine Software zur Simulation von Netzwerkeinschränkungen. ToxiProxy beinhaltet einen Proxy-Server, ein Command Line Interface zur Konfiguration des Servers sowie ca. ein Dutzend Bindings für verschiedene Programmiersprachen.
Der ToxiProxy kann verschiedene Umstände wie Timeouts, hohe Latenzen oder geringe Bandbreiten für TCP-basierte Services wie z.B. Datenbanken simulieren.
Warum ein Tool wie ToxiProxy?
In der heutigen Zeit ist es in vielen Bereichen unumgänglich, verteilte Systeme einzusetzen. Verteilte Systeme können die Komplexität erheblich erhöhen, erlauben es jedoch auch mehreren Teams, mit hoher Autarkie in kleinen Inkrementen ein gemeinsames System zu entwickeln und die Time-to-Market gering zu halten, um weiterhin konkurrenzfähig zu bleiben.
Mit der Anzahl der Komponenten, die miteinander interagieren, wächst auch die Wahrscheinlichkeit von Störungen. Die Inbetrachtnahme eines Ausfalls auf Netzwerkebene wird meiner Erfahrung nach leider viel zu häufig von Software-Entwicklern missachtet. Dies macht sich durch die häufige gänzliche Abwesenheit entsprechender automatisierter Tests bemerkbar, und auch dann, wenn entsprechendes Fehlverhalten einer Software erst durch eine auftretende Netzwerkstörung bemerkt wird.
Fallacies of Distributed Computing
Bei Sun Microsystems hatte man sich dazu in den 90er Jahren Gedanken gemacht und acht Irrtümer von verteilten Systemen aufgestellt. Diese sind, ins Deutsche übersetzt:
- Das Netzwerk ist zuverlässig.
- Die Latenz ist gleich null.
- Die Bandbreite ist unbegrenzt.
- Das Netzwerk ist sicher.
- Die Netzwerktopologie ändert sich nicht.
- Es gibt genau einen Netzwerkadministrator.
- Die Transportkosten sind gleich null.
- Das Netzwerk ist homogen.
Die Folgen von Netzwerkfehlern können fatal sein, wenn man sich um die Fehlerbehandlung keine Gedanken macht. Dies zeigt zum Beispiel ein vierstündiger Ausfall des verteilten Datenbankservices DynamoDB von AWS aus dem Jahr 2015. Durch ein neues Feature und eine kurzzeitige Netzwerkstörung hat eine Kombination aus Timeouts und einer, durch immer wieder neue Anfragen verursachten, ungewollten DDOS-Attacke dazu geführt, dass das komplette Serviceangebot mehrere Stunden nicht verfügbar war. Eine detaillierte Analyse ist am Ende des Beitrages verlinkt. Netzwerkstörungen können schnell zu einer Kaskade von Fehlern führen – bildlich zu einer Fehlerlawine, die ein ganzes System außer Gefecht setzen kann.
ToxiProxy in der Praxis
Ich habe den ToxiProxy kürzlich beruflich eingesetzt, um die Resilienz unseres Systems bei Ausfällen der Infrastrukturservices Apache Kafka, MongoDB und Elasticsearch zu testen. Dieser Test war händisch und sollte gewährleisten, dass unser System zum jetzigen Zeitpunkt von Ausfällen dieser Dienste nicht in Mitleidenschaft gezogen wird, auf HTTP-Anfragen in dieser Zeit in geeigneter Form reagiert – d.h. etwa mit dem HTTP-Status 503 Service Unavailable ohne Preisgabe sensibler Informationen wie einem Stacktrace – und vor allem sich von Störungen von selbst wieder erholt, ohne dass das System durchgestartet oder händisch eingegriffen werden muss.
Mittelfristig soll es aber nicht bei einem manuellen Test bleiben, sondern wir wollen automatisierte Resilizentests mithilfe des ToxiProxy Modules von Testcontainers in unseren Buildprozess integrieren. In diesem Artikel möchte ich mich allerdings auf ToxiProxy selbst beschränken und Testcontainers außen vor lassen.
Das Setup
Um ToxiProxy einfach mal auszuprobieren, empfehle ich Folgendes:
- einen einfachen Client
- einen einfachen Server
- ein geeignetes Terminalprogramm
Ich habe ToxiProxy auf MacOS eingesetzt und über Brew installiert. Installationshinweise für andere Systeme sind der am Ende des Beitrags verlinkten GitHub-Seite von ToxiProxy zu entnehmen. Als Terminal habe ich iTerm2 eingesetzt, da sich hiermit komfortabel mehrere Terminal-Sessions in einem Fenster anzeigen lassen und zu den einzelnen Shell-Befehlen Zeitstempel angezeigt werden können, die darüber Auskunft geben, wie lange die Ausführung eines Befehls gedauert hat.
Als Client habe ich ein kurzes Shellskript geschrieben, welches Curl verwendet um den Server aufzurufen:
#!/bin/bash
while true
do
for i in {1..10}
do
curl localhost:58080/small
done
curl localhost:58080/big > /dev/null
done
Der Client sendet zehnmal hintereinander eine Anfrage an /small
und dann einmal an /big
und wiederholt dies, so lange er läuft.
Den Server habe ich in Python mit der HTTP-Server-Standardbibliothek geschrieben:
#!/usr/bin/python3
from http.server import BaseHTTPRequestHandler, HTTPServer
from os import urandom, remove
count = 0
class Server(BaseHTTPRequestHandler):
def do_GET(self):
global count
if self.path == '/small':
count += 1
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.end_headers()
self.wfile.write(bytes('%d\n' % count, 'utf-8'))
elif self.path == '/big':
self.send_response(200)
self.send_header('Content-Type', 'application/octet-stream')
self.end_headers()
self.wfile.write(urandom(1024*1024*1024))
else:
self.send_response(404)
server = HTTPServer(('localhost', 8080), Server)
try:
print('server starting...')
server.serve_forever()
except KeyboardInterrupt:
pass
print('cleaning up...')
server.server_close()
print('bye!')
Der Server bietet zwei Endpunkte:
/small
liefert bei jedem Aufruf eine ab 1 beginnende aufsteigende Ganzzahl zurück/big
liefert ein Gigabyte an zufälligen Bytes zurück
Wie man sieht, ruft der Client localhost:58080
auf, während der Server auf Port 8080
lauscht. Hier kommt der ToxiProxy ins Spiel, der in die Kommunikation zwischengeschaltet werden soll. Der ToxiProxy-Server lässt sich nach Installation via Befehl toxiproxy-server
starten. Mittels der toxiproxy-cli
können dem ToxiProxy-Server zur Laufzeit nun Proxy-Einträge und sogenannte Toxics, die Netzwerkstörungen simulieren, hinzugefügt und auch wieder entfernt werden. Als erstes lege ich einen Proxy-Eintrag an, um auf dem Port 58080 zu lauschen und eingehenden Datenverkehr nach localhost:8080 weiterzuleiten:
toxiproxy-cli create --listen localhost:58080 --upstream localhost:8080 toxiproxy_test
Der folgende Screenshot zeigt Client (oben links), Server (unten rechts), ToxiProxy-Server (oben rechts) und ToxiProxy-CLI (unten links) im Einsatz:

Auf meinem MacBook dauert eine Iteration, also zehn kleine Anfragen und eine große, etwa vier Sekunden.
Toxics
Die bei mir installierte Version 2.4.0 von ToxiProxy unterstützt laut Befehl toxiproxy-cli toxic
folgende Toxics:
- latency
- bandwidth
- slow_close
- timeout
- reset_peer
- slicer
In der Dokumentation auf GitHub finde ich ein weiteres Toxic limit_data
, welches ebenfalls funktioniert. Dafür verhält sich das Toxic timeout
nicht wie erwartet. Mit folgendem Befehl setze ich einen Timeout von 5000 Millisekunden für meinen Proxy-Eintrag:
toxiproxy-cli toxic add -n timeout_test -t timeout -a timeout=5000 toxiproxy_test
Das Ergebnis ist jedoch, dass Curl den Verbindungsversuch unmittelbar mit der Meldung curl: (52) Empty reply from server
quittiert. Auch Firefox bestätigt das Verhalten des ToxiProxy-Servers, denn ein HTTP-Request an localhost:58080/small
führt sofort zu der Meldung Fehler: Verbindung unterbrochen
. Bei den anderen Toxics konnte ich zum Glück keine Probleme feststellen. Das fehlerhafte Toxic deinstalliere ich mit folgendem Befehl wieder:
toxiproxy-cli toxic delete -n timeout_test toxiproxy_test
Eine funktionierende Alternative zum Timeout ist das Toxic reset_peer, welches pünktlich alle fünf Sekunden zur Meldung curl: (56) Recv failure: Connection reset by peer
führt, wenn ich folgenden Befehl absetze:
toxiproxy-cli toxic add -n reset_test -t reset_peer -a timeout=5000 toxiproxy_test
Mit dem Toxic limit_data kann ich testen, wie meine Software sich verhält, wenn die Verbindung nach einer bestimmten Anzahl Bytes getrennt wird:
toxiproxy-cli toxic add -n limit_test -t limit_data -a bytes=10000 toxiproxy_test
Der obenstehende Eintrag sorgt dafür, dass für jede Anfrage nach 10000 Bytes Schluss ist. Ergebnis: Curl lädt bei der Anfrage an /big
regelmäßig nur 9869 Bytes und der Python-Server meldet einen ConnectionResetError
.
Die Toxics latency, bandwidth, slow_close und slicer bremsen den Datenverkehr, führen im Gegensatz zu den anderen Toxics aber nicht unbedingt zu einem Fehler:
- latency simuliert eine mit
latency
angegebene Latenz in Millisekunden und eine mitjitter
angegebene Varianz dieser Latenz nach oben und unten in der gleichen Einheit - bandwidth simuliert eine mit
rate
in Bytes angegebene Beschränkung der Bandbreite – dieses Toxic finde ich besonders interessant, da eine geringe Bandbreite Timeouts zur Folge haben kann, z.B. beim Einsatz eines Circuit Breakers - slow_close simuliert eine mit
delay
in Millisekunden verzögerte Reaktion des TCP-Sockets beim Beenden der Verbindung - slicer bricht die TCP-Pakete in kleinere Pakete runter, wobei mit
average_size
die Paketgröße in Bytes, mitsize_variation
eine Varianz nach oben und unten in Bytes und mitdelay
eine zeitliche Verzögerung in Millisekunden zwischen den Paketen angegeben werden kann
Die folgenden vier Toxics sorgen dafür, dass der Download der ein Gigabyte großen Datei statt etwa vier Sekunden nun fasst neun Minuten benötigt:
toxiproxy-cli toxic add -n latency_test -t latency -a latency=100 -a jitter=100 toxiproxy_test
toxiproxy-cli toxic add -n bandwidth_test -t bandwidth -a rate=1000 toxiproxy_test
toxiproxy-cli toxic add -n slow_test -t slow_close -a delay=100 toxiproxy_test
toxiproxy-cli toxic add -n slicer_test -t slicer -a average_size=1000 -a size_variation=800 -a delay=10 toxiproxy_test
Auf dem nachfolgenden Screenshot sieht man das Ergebnis:

ToxiProxy-API
Die ToxiProxy-CLI verwendet die REST-API des ToxiProxy-Servers. Über folgenden Befehl kann man sich etwa einen Überblick verschaffen, welche Toxics alle auf einen gegebenen Proxy-Eintrag wirken:
toxiproxy-cli inspect toxiproxy_test
Name: toxiproxy_test Listen: 127.0.0.1:58080 Upstream: localhost:8080
======================================================================
Upstream toxics:
Proxy has no Upstream toxics enabled.
Downstream toxics:
latency_test: type=latency stream=downstream toxicity=1.00 attributes=[ jitter=100 latency=100 ]
bandwidth_test: type=bandwidth stream=downstream toxicity=1.00 attributes=[ rate=1000 ]
slow_test: type=slow_close stream=downstream toxicity=1.00 attributes=[ delay=100 ]
slicer_test: type=slicer stream=downstream toxicity=1.00 attributes=[ average_size=1000 delay=10 size_variation=800 ]
Analog kann man auch die API des ToxiProxy-Servers direkt verwenden:
curl localhost:8474/proxies/toxiproxy_test
{
"name":"toxiproxy_test",
"listen":"127.0.0.1:58080",
"upstream":"localhost:8080",
"enabled":true,
"toxics":[
{
"attributes":{
"latency":100,
"jitter":100
},
"name":"latency_test",
"type":"latency",
"stream":"downstream",
"toxicity":1
},
{
"attributes":{
"rate":1000
},
"name":"bandwidth_test",
"type":"bandwidth",
"stream":"downstream",
"toxicity":1
},
{
"attributes":{
"delay":100
},
"name":"slow_test",
"type":"slow_close",
"stream":"downstream",
"toxicity":1
},
{
"attributes":{
"average_size":1000,
"size_variation":800,
"delay":10
},
"name":"slicer_test",
"type":"slicer",
"stream":"downstream",
"toxicity":1
}
]
}
Laut Dokumentation auf GitHub bietet der ToxiProxy-Server auch einen Endpunkt /metrics
, der Prometheus-kompatible Metriken des ToxiProxy-Servers bereitstellt. In der von mir eingesetzten Version wird der Aufruf aber leider mit einem HTTP 404 Page not found beantwortet.
Fazit
Als Entwickler kennt man seine Software erst dann wirklich gut, wenn man auch weiß, wie sie sich im Störungsfall verhält. Bei verteilten Systemen finde ich es daher unerlässlich, auch das Verhalten bei Netzwerkproblemen regelmäßig und am besten automatisiert zu testen, gerade wenn man als Team DevOps praktiziert und viele Schnittstellen zu anderen Services und Teams hat. Der ToxiProxy ist eine sehr solide Software für derartige Tests und ich persönlich bin sehr gespannt darauf, den ToxiProxy auch im Kontext von Testcontainers-Integrationstests erproben zu können.
- ToxiProxy auf GitHub - ToxiProxy Module von Testcontainers - Analyse des DynamoDB-Ausfalls bei AWS (2015)