SMTP-Lasttest mit nummerierten Empfängern: jede Mail nachvollziehbar versenden
Ein Lasttest ist nur so gut wie seine Auswertung. smtp-source nummeriert mit der Option -N jede Mail über die Empfängeradresse durch, ohne den Durchsatz zu opfern. Wie der Lauf aufgebaut wird, wie viele Sessions sinnvoll sind und wie fehlende Nummern automatisch gefunden werden.
Wer einen Lasttest fährt, will hinterher zwei Fragen beantworten können: Sind alle Mails angekommen, und wenn nicht, welche fehlen? Mit identischen Testmails lässt sich nur zählen, und ein Zählerstand mit 13 fehlenden Nachrichten sagt nichts darüber, wann und wo sie verloren gingen. Trägt dagegen jede Mail eine fortlaufende Nummer, wird aus dem Zählen ein Abgleich: Jede Nummer ist in den Logs des Zielsystems einzeln auffindbar, Lücken zeigen den Zeitpunkt des Verlusts, und die Reihenfolge der Zustellung lässt sich prüfen.
Die verbreitete Reflexreaktion ist ein Skript, das den Betreff hochzählt. Das funktioniert, kostet aber Durchsatz, denn der Lastgenerator smtp-source aus dem Postfix-Paket setzt den Betreff pro Aufruf fix, und eine Schleife mit einem Aufruf pro Mail erzwingt für jede Nachricht einen eigenen Verbindungsaufbau. Die bessere Nachrichtenkennung ist bereits eingebaut: Die Option -N nummeriert die Empfängeradresse pro Nachricht durch, und zwar innerhalb eines einzigen Aufrufs mit parallelen Sessions. Für die Auswertung ist die Empfängeradresse genauso brauchbar wie der Betreff, denn sie steht in jedem Tracking-Log.
Dieser Testaufbau sendet, anders als ein reiner Loopback-Funktionstest, an ein anderes System über das Netz. Falls auf dem Quellsystem kein Postfix installiert ist, zeigt der Beitrag smtp-source ohne Postfix-Installation, wie Sie die Werkzeuge aus dem RPM entpacken.
Die wichtigsten Optionen von smtp-source
Zur Orientierung vorab die Optionen, die in diesem Beitrag vorkommen, sinngemäss aus der Manpage übersetzt:
Die vollständige Liste inklusive TLS-, LMTP- und Timing-Optionen steht in der Manpage smtp-source(1); das Gegenstück für die Empfangsseite ist smtp-sink(1) und kommt bei der Auswertung weiter unten zum Einsatz.
Wie -N die Empfänger nummeriert
-N aktiviert einen per-Prozess-Zähler, der in die Empfängeradresse eingebaut wird. Drei Eigenschaften bestimmen den Testaufbau, alle drei sind im Quellcode von smtp-source.c nachlesbar:
Erstens hängt die genaue Adressform von der Postfix-Version ab. Postfix 3.5, wie es RHEL 8 ausliefert, stellt die Nummer der gesamten Adresse voran (RCPT TO:<%d%s>): Aus -t test@example.com werden 1test@example.com, 2test@example.com und so weiter, und der Zähler beginnt bei 1. Aktuelle Postfix-Versionen fügen die Nummer stattdessen am Ende des Local-Parts ein und beginnen bei 0 (test0@ bis test49999@); für diese Variante empfiehlt die Manpage Plus-Adressierung (-t 'test+@example.com' ergibt test+0@ und folgende), damit ein Zielsystem mit Subadressierung alles demselben Postfach zuordnet. Prüfen Sie die Form vor dem grossen Lauf mit einer Handvoll Mails gegen einen smtp-sink oder im Log des Ziels; davon hängen die Sollmenge und das Suchmuster der Auswertung ab.
Zweitens ist der Zähler prozessweit und wird von allen parallelen Sessions geteilt. Bei -s 8 vergeben die acht Sessions die Nummern gemeinsam, jede Nummer kommt genau einmal vor. Die Reihenfolge über die Sessions hinweg ist nicht deterministisch, die Vollständigkeit der Nummernmenge aber garantiert.
Drittens ist der Startwert nicht konfigurierbar: 1 bei Postfix 3.5, 0 bei aktuellen Versionen. Die Mails tragen also die Nummern 1 bis zur Gesamtzahl aus -m beziehungsweise 0 bis Gesamtzahl minus 1, und die Sollmenge für den Abgleich muss dazu passen.
Der Testlauf in einem Aufruf
Wie viele Mails der Lauf umfasst, spielt für das Vorgehen keine Rolle; -m bestimmt die Gesamtzahl, und die Beispiele in diesem Beitrag verwenden dafür 50’000 als beliebigen Platzhalter.
smtp-source -c -d -N -s 8 -m 50000 -l 5120 \
-f lasttest@example.com \
-t test@example.com \
gateway.example.com:25
-d ist für das Lastbild entscheidend: Ohne diese Option trennt smtp-source nach jeder Nachricht die Verbindung und baut für die nächste eine neue auf; mit -d bleiben die acht Verbindungen stehen und liefern nacheinander alle Nachrichten aus, wie es ein Massenversender tut.
Bewusst fehlt das aus Funktionstests bekannte -v: Es protokolliert jeden einzelnen SMTP-Dialog von HELO bis QUIT und erzeugt bei einem grossen Lauf hunderttausende Logzeilen ohne Mehrwert für die Auswertung. -c liefert stattdessen die Zusammenfassung, an der sich der Fortschritt des Laufs live ablesen lässt. Die Gesamtdauer für die Ratenberechnung liefert ein vorangestelltes time.
Voraussetzung für den ganzen Ansatz: Das Zielsystem nimmt die generierten Adressen an. Ein smtp-sink, eine Catch-all-Domain, eine Verwerf-Domain des Providers oder ein Gateway, das Empfänger erst nach der Annahme auflöst, erfüllen das. Prüft das Ziel dagegen jeden Empfänger gegen ein Verzeichnis, lehnt es die nummerierten Adressen ab, und es bleibt nur die Betreff-Variante.
Eigene Header setzen
Manche Lasttests brauchen einen eigenen Header, etwa als Marker, an dem das Gateway die Testmails erkennt oder eine Regel greift. Eine Option dafür kennt smtp-source nicht, aber -F liest eine vollständig vorformatierte Nachricht aus einer Datei, und dort steht jeder gewünschte Header. Die Datei besteht aus den Header-Zeilen, einer Leerzeile und dem Body, alle Zeilen mit \r\n beendet:
{ printf 'X-Lasttest: aktiv\r\n'
printf 'Subject: Lasttest\r\n'
printf '\r\n'
head -c 5120 /dev/zero | tr '\0' 'x'
printf '\r\n'; } > lasttest.eml
smtp-source -c -d -N -s 8 -m 50000 -F lasttest.eml \
-f lasttest@example.com \
-t test@example.com \
gateway.example.com:25
Zwei Konsequenzen: -F verdrängt -l und -S, weil Grösse und Betreff nun aus der Datei kommen (beides gehört deshalb hinein). -N bleibt dagegen wirksam, die Empfänger werden weiter durchnummeriert; der Header ist in allen Nachrichten identisch, da er aus der festen Datei stammt.
Wie viele Sessions?
Die passende Session-Zahl ermitteln Sie am zuverlässigsten durch Messung, und zwar mit exakt denselben Optionen wie der geplante Hauptlauf: gleiche Nachrichtenquelle (dieselbe -F-Datei beziehungsweise dasselbe -l), gleicher Absender, gleiches Ziel. Nur die Menge ist auf 2’000 pro Stufe reduziert, und -s variiert. Ein kurzer Kalibrierlauf mit steigender Session-Zahl zeigt, ab wann zusätzliche Sessions nichts mehr bringen:
for s in 1 2 4 8 16 32; do
t0=$(date +%s%N)
smtp-source -d -N -s "$s" -m 2000 -F lasttest.eml \
-f lasttest@example.com -t '@blackhole.example.com' \
gateway.example.com:25
t1=$(date +%s%N)
echo "$s Sessions: $(( 2000000000000 / (t1 - t0) )) Mails/s"
done
Zwei Details am Aufruf: Auf -c wird hier bewusst verzichtet, damit zwischen den Messzeilen keine laufenden Zählerausgaben stehen; die Schleife liefert pro Stufe genau eine Ergebniszeile. Und der leere Local-Part in -t funktioniert bei einer Verwerf-Domain gut mit der Nummerierung zusammen: Beim vorangestellten Zähler von Postfix 3.5 entstehen daraus rein numerische Empfängeradressen (1@blackhole.example.com, 2@…), was die Auswertung in den Logs übersichtlich hält.
Im Einzelnen passiert Folgendes: Die äussere Schleife durchläuft die Session-Zahlen 1 bis 32 in Verdopplungsschritten. Vor und nach jedem Lauf hält date +%s%N die aktuelle Zeit als eine grosse Zahl fest, nämlich Unix-Sekunden direkt gefolgt vom Nanosekunden-Anteil. Dazwischen versendet smtp-source 2’000 Nachrichten (Inhalt, Header und Grösse kommen aus der -F-Datei) über die jeweilige Zahl paralleler, dank -d stehen bleibender Verbindungen; die Schleife wartet, bis der Aufruf komplett fertig ist. Die echo-Zeile rechnet die Zeitdifferenz in eine Rate um: 2’000 Mails geteilt durch die Laufzeit in Sekunden, wobei die Laufzeit in Nanosekunden vorliegt. Aus 2’000 mal 10⁹ entsteht so die Konstante 2000000000000. Die Bash-Arithmetik $(( )) rechnet ganzzahlig und schneidet Nachkommastellen ab, was für diese Messung genau genug ist.
Drei praktische Hinweise dazu: %N liefert nur GNU date Nanosekunden (auf RHEL und den meisten Linux-Systemen der Fall; BusyBox und macOS kennen es nicht). Der komplette Durchlauf versendet 6 × 2’000 = 12’000 Mails, auch die brauchen eine kontrollierte Empfängeradresse, und die -N-Nummerierung beginnt in jedem Aufruf wieder beim Startwert. Und falls ein smtp-source-Aufruf mit einer Fehlermeldung abbricht, ist die Rate dieser Zeile bedeutungslos; erst die Ursache beheben, dann neu messen.
Die erwartete Ausgabe ist eine Zeile pro Stufe. Mit erfundenen, aber typischen Beispielwerten sieht das so aus:
1 Sessions: 11 Mails/s
2 Sessions: 21 Mails/s
4 Sessions: 40 Mails/s
8 Sessions: 71 Mails/s
16 Sessions: 79 Mails/s
32 Sessions: 80 Mails/s
Die Lesart: Solange sich die Rate mit der Session-Zahl etwa verdoppelt, überdecken die parallelen Sessions die Wartezeit auf die Antworten des Ziels; der Engpass ist dann die Latenz der Strecke, nicht die Kapazität. Ab dem Punkt, an dem die Kurve abflacht (im Beispiel zwischen 8 und 16 Sessions), ist entweder das Zielsystem gesättigt oder die Quelle am Limit. Nehmen Sie den kleinsten Wert, bei dem die Rate nicht mehr nennenswert zulegt, im Beispiel also 8 bis 16; noch mehr Sessions erhöhen dann nur die Last durch Parallelität, nicht den Durchsatz. Für den Hauptlauf lässt sich aus der gemessenen Rate auch gleich die erwartete Dauer abschätzen: die Gesamtzahl aus -m geteilt durch die Rate.
Auswertung auf der Empfangsseite
Steht auf dem Zielsystem ein eigener Testempfänger, übernimmt smtp-sink die Protokollierung gleich mit:
smtp-sink -c -d "mails/%Y%m%d-%H%M%S." 0.0.0.0:2525 200
Nach dem Lauf extrahieren Sie die empfangenen Nummern und vergleichen sie mit der Sollmenge. Da die Nummern keine führenden Nullen tragen, werden beide Listen vor dem Abgleich auf eine feste Stellenzahl gebracht, damit die alphabetische Sortierung von comm der numerischen entspricht. Das Suchmuster passt zur Adressform von Postfix 3.5 (Nummer vor der Adresse); bei aktuellen Versionen entsprechend test[0-9]+@ und seq ab 0:
grep -rhoE '[0-9]+test@example\.com' mails/ | \
grep -oE '^[0-9]+' | sort -u | \
awk '{printf "%08d\n", $1}' | sort > empfangen.txt
seq 1 50000 | awk '{printf "%08d\n", $1}' | \
comm -23 - empfangen.txt
comm -23 gibt genau die Nummern aus, die in der Sollmenge stehen, aber nicht in der Empfangsliste: die fehlenden Mails. Eine leere Ausgabe bedeutet vollständige Zustellung. Tauchen Nummern doppelt auf (erkennbar am Unterschied zwischen sort und sort -u), hat unterwegs ein System die Nachricht dupliziert, was ebenfalls ein Befund ist.
Ist das Ziel ein produktnahes System statt eines smtp-sink, übernimmt dessen Logging die Rolle der Dump-Dateien. Auf einem Exchange-Server etwa liefert Get-MessageTrackingLog -Recipients beziehungsweise ein Filter auf die Empfängeradresse die angekommenen Nummern, auf einem Postfix-System ein grep auf to= und die Basisadresse über das Maillog. Genau das ist der Vorteil der Nummer in der Adresse: Der Empfänger steht in jedem Message-Tracking, während der Betreff dort je nach System fehlt oder erst eingeschaltet werden muss.
Wenn die Nummer im Betreff stehen muss
Manche Auswertungen hängen am Betreff, etwa wenn das Zielsystem Empfängeradressen umschreibt oder die Logs den Empfänger nur maskiert zeigen. Dann bleibt die Schleifen-Variante: ein smtp-source-Aufruf pro Mail mit -m 1 und einem Betreff, den die Shell hochzählt, verteilt auf mehrere parallele Worker mit zusammenhängenden Nummernbereichen.
worker() {
local i
for ((i = $1; i <= $2; i++)); do
smtp-source -s 1 -m 1 -l 5120 \
-S "$(printf 'Lasttest %05d' "$i")" \
-f lasttest@example.com -t test@example.com \
gateway.example.com:25 || echo "$i" >> fehlend.log
done
}
for w in 0 1 2 3; do
worker $(( w * 12500 + 1 )) $(( (w + 1) * 12500 )) &
done
wait
Der Preis ist ein vollständiger Verbindungsaufbau pro Mail: TCP-Handshake, Banner, HELO, Versand, QUIT. Dieser Lauf misst damit nicht den maximalen Durchsatz des Zielsystems, sondern einen bewusst verbindungsintensiven Fall. Die Worker-Zahl ermitteln Sie analog zum Kalibrierlauf oben, nur mit der Worker-Schleife statt -s. Die führenden Nullen im Betreff ersparen beim Abgleich das Umformatieren, das die -N-Variante braucht.
Regeln für Tests gegen andere Systeme
Sobald der Test das eigene System verlässt, gelten drei Bedingungen. Erstens: Der Betreiber des Zielsystems weiss Bescheid und hat dem Zeitfenster zugestimmt; ein Lasttest sieht für jedes Monitoring wie ein Angriff oder eine Spamwelle aus. Zweitens: Die Empfängeradresse endet kontrolliert, in einem dedizierten Testpostfach, einer Verwerf-Regel auf dem Ziel oder einer vom Provider dafür vorgesehenen Verwerf-Domain; produktive Adressen gehören nicht in einen Lasttest. Drittens: Ein Abbruchkriterium steht vor dem Start fest, etwa eine wachsende Queue auf dem Ziel oder eine Fehlerrate über einem Schwellwert, und jemand beobachtet diese Werte während des Laufs.
Mit diesen drei Punkten und der Nummerierung liefert der Lauf am Ende nicht nur eine Durchsatzzahl, sondern eine belegbare Aussage: welche Mails angekommen sind, welche fehlen und wo auf der Strecke sie zuletzt gesehen wurden.

Kommentare
Die Kommentare werden von GitHub / Giscus geladen.