27. August 2026 8 Min. Lesezeit

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:

Optionen im Überblick
OptionBedeutung
-s nAnzahl paralleler SMTP-Sessions (Standard: 1)
-m nAnzahl zu sendender Nachrichten insgesamt (Standard: 1)
-l nGrösse des Nachrichtentexts in Bytes, ohne Header
-f adresseAbsenderadresse
-t adresseEmpfängeradresse (Standard: foo@hostname)
-S textBetreffzeile, fix für alle Nachrichten des Aufrufs
-F dateiSendet Header und Body unverändert aus einer Datei; verdrängt -l und -S
-NNummeriert die Empfängeradresse pro Nachricht durch (per-Prozess-Zähler; Position und Startwert versionsabhängig, siehe unten)
-r nAnzahl Empfänger pro Nachricht (Standard: 1), Adressbildung wie bei -N
-dNach einer Nachricht nicht trennen, die nächste über dieselbe Verbindung senden
-cLaufenden Zähler anzeigen, der mit jedem abgeschlossenen DATA hochzählt
-w nFeste Wartezeit von n Sekunden zwischen Nachrichten (pro Session)
-vAusführliche Ausgabe für die Fehlersuche
host:portZiel der Einlieferung via TCP; ohne Portangabe der Standardport smtp

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
Optionen erklärt
OptionWirkung
-cLaufender Zähler abgeschlossener Zustellungen als einzeilige Fortschrittsanzeige
-dVerbindungen bleiben über alle Nachrichten stehen; ohne -d neue Verbindung pro Nachricht
-NEmpfänger-Nummerierung: hängt den per-Prozess-Zähler an den Local-Part an
-s 8Acht parallele SMTP-Sessions
-m 50000Gesamtzahl der Nachrichten, verteilt auf die Sessions
-l 5120Nachrichtengrösse in Bytes (ohne Header), hier 5 KB
-fAbsenderadresse
-tEmpfänger-Basisadresse; -N macht daraus 1test@, 2test@ und so weiter (Postfix 3.5) bzw. test0@, test1@ und so weiter (aktuelle Versionen)
gateway.example.com:25Zielhost und Port

-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
Optionen erklärt
OptionWirkung
head -c 5120Gibt die ersten 5120 Bytes der Eingabe aus, hier aus /dev/zero
tr '\0' 'x'Ersetzt jedes Nullbyte durch das Zeichen x und erzeugt so den 5-KB-Fülltext
> lasttest.emlSchreibt die zusammengesetzte Nachricht in die Datei für -F
smtp-source -c -d -N -s 8 -m 50000 -F lasttest.eml \
  -f lasttest@example.com \
  -t test@example.com \
  gateway.example.com:25
Optionen erklärt
OptionWirkung
-F dateiSendet Header und Body unverändert aus der Datei; ersetzt den generierten Nachrichteninhalt

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
Optionen erklärt
OptionWirkung
date +%s%NGibt Unix-Sekunden direkt gefolgt vom Nanosekunden-Anteil als eine Zahl aus
-dVerbindungen bleiben über alle Nachrichten der Stufe stehen
-NEmpfänger-Nummerierung über den per-Prozess-Zähler
-s "$s"Anzahl paralleler Sessions, pro Schleifendurchlauf 1 bis 32
-m 20002’000 Nachrichten pro Messstufe
-F lasttest.emlDieselbe Nachrichtendatei wie im geplanten Hauptlauf
-fAbsenderadresse
-t '@blackhole.example.com'Empfänger-Basisadresse mit leerem Local-Part auf einer Verwerf-Domain
gateway.example.com:25Zielhost und Port

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
Optionen erklärt
OptionWirkung
-cLaufende Zähler statt des vollen SMTP-Dialogs
-d "mails/…"Beim Sink: Dump, nicht Verbindungshaltung. Schreibt jede angenommene Nachricht in eine eigene Datei (Namensmuster per strftime), inklusive eines X-Rcpt-Args-Headers mit der Empfängeradresse
0.0.0.0:2525Lauscht auf allen Interfaces auf Port 2525
200Backlog: maximale Länge der Warteschlange wartender Verbindungen gemäss listen(2)

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
Optionen erklärt
OptionWirkung
grep -rDurchsucht das Verzeichnis mails/ rekursiv
grep -hUnterdrückt die Dateinamen vor den Treffern
grep -oGibt nur den passenden Adressteil aus, nicht die ganze Zeile
grep -EErweiterte reguläre Ausdrücke, hier für [0-9]+
sort -uSortiert und entfernt Duplikate (jede Nummer einmal)
awk '{printf "%08d\n", $1}'Füllt jede Nummer mit führenden Nullen auf acht Stellen auf
sortSortiert die aufgefüllten Nummern für den Abgleich mit comm
seq 1 50000 | awk '{printf "%08d\n", $1}' | \
  comm -23 - empfangen.txt
Optionen erklärt
OptionWirkung
seq 1 50000Erzeugt die Sollmenge der Nummern; der Endwert entspricht der versendeten Gesamtzahl aus -m
comm -23Unterdrückt Spalte 2 (nur in Datei 2) und Spalte 3 (in beiden); übrig bleiben die Zeilen, die nur in der Sollmenge stehen
-Liest die erste Vergleichsliste aus der Pipe statt aus einer Datei
empfangen.txtZweite Vergleichsliste: die tatsächlich empfangenen Nummern

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
Optionen erklärt
OptionWirkung
-s 1Eine Session pro Aufruf; die Parallelität liefern die vier Worker
-m 1Genau eine Nachricht pro Aufruf, damit der Betreff pro Mail gesetzt werden kann
-l 5120Nachrichtengrösse in Bytes (ohne Header), hier 5 KB
-S "$(printf 'Lasttest %05d' "$i")"Betreff mit der auf fünf Stellen aufgefüllten laufenden Nummer
-f / -tAbsender- und Empfängeradresse
gateway.example.com:25Zielhost und Port

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.

Quellen

  1. Postfix: smtp-source(1)

    Manpage des Lastgenerators; beschreibt das -N-Verhalten der aktuellen Version (Zähler am Local-Part, Plus-Adressierung).

    https://www.postfix.org/smtp-source.1.html
  2. Postfix-Quellcode 3.5.8: smtp-source.c

    belegt für die RHEL-8-Version das Voranstellen der Nummer (RCPT TO:<%d%s>) mit Startwert 1; im aktuellen Stand wird die Nummer stattdessen am Local-Part angefügt, ab 0.

    https://github.com/vdukhovni/postfix/blob/v3.5.8/postfix/src/smtpstone/smtp-source.c
  3. Postfix: smtp-sink(1)

    Manpage des Test-Empfängers mit den Dump-Optionen und den aufgezeichneten X-Headern.

    https://www.postfix.org/smtp-sink.1.html
  4. GNU Coreutils: comm

    Mengenvergleich zweier sortierter Listen, hier für den Abgleich von Soll- und Empfangsnummern.

    https://www.gnu.org/software/coreutils/manual/html_node/comm-invocation.html

Kommentare

Die Kommentare werden von GitHub / Giscus geladen.