26. Benchmark-Suite

Die tests/NN-*-bench.sh-Skripte messen, wie schnell die Live-Pepsi-Pipeline Mail verarbeitet. Sie verwenden dieselben Hosts, Konten und Helper wie die Korrektheits-Suite (tests/test-accounts.ini, tests/lib.sh) plus ein gemeinsames tests/bench-lib.sh wieder und werden auf dieselbe Weise angetrieben:

tests/08-perstage-bench.sh tests/test-accounts.ini
tests/09-latency-bench.sh  tests/test-accounts.ini
tests/10-goodput-bench.sh  tests/test-accounts.ini
# or all three in order:
make benchmarks ACCOUNTS=tests/test-accounts.ini

Sie sind nicht Teil von make integrationtests (sie sind langsam und belasten den MTA bewusst). Jedes führt zuerst preflight_common aus, sodass es sauber SKIPpt, wenn die Live-Hosts/DB/Dienste unerreichbar sind. Führen Sie zuerst tests/01-deploy.sh aus, damit die Pipeline — einschließlich des DAVE-Lokale-Maildir-Zustellziels — existiert.

Dieses Kapitel beschreibt die Methode. Wie die Ergebnisse zu lesen sind — Kosten pro Stage, Leerlauf-Latenz und Sättigungs-Nutzdurchsatz — und was sich abstimmen lässt, beschreibt das Kapitel Leistung.

26.1. Wie die Zahlen gewonnen werden

  • Die Kosten pro Stage stammen aus der dispatchereigenen Zeitmessung: pepsi-dispatch zeichnet jede Stage-Ausführung in pepsi.stage_stats(stage, messages, duration_us) auf. Ein Schnappschuss dieser Tabelle vor/nach einer Nachricht ergibt exakte Mikrosekunden-pro-Nachricht je Stage — für eine Stage, die eine per SMTP gesendete Nachricht erreicht, ebenso wie für eine, in die direkt injiziert wird, da beide als gewöhnliche vom Dispatcher gestartete Worker laufen. Die Benchmarks senken [pepsi-dispatch] STATS_INTERVAL (und POLL_INTERVAL), sodass die Zähler umgehend weggeschrieben werden, und setzen außerdem [pepsi] LOG auf eine ruhige Stufe (warn standardmäßig, über BENCH_LOG_LEVEL überschreibbar), sodass das Logging pro Nachricht die Messungen nicht aufbläht — diese Stellschraube wird von jeder Komponente gelesen, sodass sie den Dispatcher und die von ihm gestarteten Stage-Worker abdeckt (die kein eigenes Log-Flag annehmen). Sowohl pepsi-dispatch als auch pepsi-ingress werden neu gestartet, damit die neue Stufe wirksam wird, und die Benchmarks stellen beim Beenden die installierte Konfiguration wieder her (eine einmalige pepsi.conf.bench.bak-Sicherung auf dem ADMIN-Host, durch ein trap … EXIT zurückgespielt).

  • Nutzdurchsatz ist das Delta des globalen Zählers pepsi.dispatch_stats.messages_processed über jedes 5-Sekunden-Fenster. Er wird alle STATS_INTERVAL geschrieben, hängt bei hoher Rate also um bis zu ein Intervall hinter der Wahrheit her; die Spalte deliv/s daneben — Dateien, die im Ziel-Maildir/new erscheinen — ist die unmittelbare Gegenprobe, und wenn die beiden um einen Schritt auseinanderliegen, ist das dieser Verzug und keine verlorene Nachricht.

  • Die Zustellung wird durch Zählen der Dateien im Ziel-Maildir/new gegengeprüft.

  • Der Lastgenerator sind mehrere Prozesse, nicht einer. load_burst verteilt einen Burst auf LOAD_PROCS (Standard 4) Betriebssystemprozesse, von denen jeder seinen Anteil der Threads über seinen eigenen Abschnitt des Nachrichtenbereichs laufen lässt, und führt deren JSON zusammen. Ein Python-Prozess ist ein GIL, und bei hohen Raten ist der Interpreter dasjenige, was die Zahl nicht weiter steigen lässt — womit ein Ein-Prozess-Generator aufhört, das getestete System zu messen, und anfängt, sich selbst zu messen.

Damit die eingehende Pay-to-Send-(Anti-Spam-)Stage die Last nicht abriegelt, setzen die Benchmarks den Absender vorab auf die weiße Liste (bench_whitelist_sender), sodass check-whitelist state.spam=false setzt und anti-spam kurzschließt.

Bemerkung

Die Rampe kann über SMTP keinen Rückstau aufbauen, und das ist kein Fehler der Rampe. pepsi-ingress schreibt jede Annahme synchron fest — ein 250 bedeutet, dass die Nachricht die Platte erreicht hat —, sodass auf einem Host, auf dem die Annahme das Engere von beiden ist, die Warteschlange nahezu leer bleibt, wie stark der Generator auch drückt, und die Rampe das Front-End misst. Um die Pipeline mit einer tiefen Warteschlange zu messen, fügen Sie Datensätze direkt in pepsi.workqueue mit status = 'pending' in einer Transaktion ein und messen das Leerlaufen; das Kapitel Performance zeigt, wie, und der Unterschied zwischen den beiden Zahlen ist der interessante Teil.

26.2. Die drei Benchmarks

  1. ``08-perstage-bench.sh`` — Kosten pro Stage gegenüber der Nachrichtengröße. Gibt eine einzige stage × size-Matrix in µs/Nachricht aus, die jede Stage abdeckt, die Pepsi hat, gefüllt von zwei Hälften, die zwei verschiedene Fragen beantworten: Die Szenarien messen die Stages der installierten Pipeline in situ, und die isolierten Stages messen jedes Stage-Programm, durch das die installierte Pipeline keine Nachricht leitet.

    Die Szenarien. Eine einzelne Nachricht durchläuft nur einen Pfad und kann daher nur die Stages auf diesem Pfad messen; um die gesamte bidirektionale, verzweigte Pipeline abzudecken, sind deshalb mehrere Nachrichten nötig — eine pro Pfad. Der Benchmark treibt eine Reihe von SCENARIOS an, jedes eine Nachricht, deren Weg eine andere Gruppe von Stages aufdeckt, und summiert ihre Kosten pro Stage in der Matrix:

    Szenario

    Nachricht

    Stages, die es neu aufdeckt

    inbound-local

    CAROL → DAVE (fremder Eingang → lokales Maildir)

    init (arc), route (if), decrypt, detect-language, block-language, check-whitelist, anti-spam (Kurzschluss), list (Listen-Router), forward (dot-forward), local (maildir)

    inbound-relay

    CAROL → ALICE (eingehend → weitergeleitetes Relay)

    srs, dkim-sign-relay, smarthost (der nicht-lokale Empfänger wird von local auf dieses Endstück abgespalten)

    outbound

    ALICE → CAROL (Submission lokalen Ursprungs → direktes Relay)

    list-out (Listen-Router), edit-settings, auto-whitelist, delay-route (if), encrypt, dkim-sign, internet (relay-to-internet)

    bounce

    CAROL → dave-broken (lokale Zustellung schlägt fehl)

    bounce (der Maildir-Helper schlägt fehl → ein Bounce-SideClone wird zur Bounce-Stage geleitet → dkim-sign)

    bounce-language

    CAROL → DAVE, französischer Nachrichtentext (Sprache auf der schwarzen Liste)

    bounce-language (block-language erzeugt einen Bounce für eine fr-Nachricht)

    payment (Opt-in)

    CAROL (nicht auf der weißen Liste) → DAVE (Pay-to-Send-Schranke)

    bounce-payment (verkürzt PAYMENT_DEADLINE für den Lauf)

    delay (Opt-in)

    ALICE → blackhole.invalid, ENVID=PEPSIDELAY (DELAY DSN)

    delaytest (relay-to-smarthost an einen nicht routebaren MTA)

    Die Standard-SCENARIOS sind die fünf schnellen; hängen Sie payment und/oder delay an, um auch die letzten beiden installierten Stages zu messen (sie sind langsamer — händler- und wiederholungsgebunden). Die payment/delay-Szenarien und die beiden Opt-in-Stages benötigen einen Absender, der nicht lokalen Ursprungs ist, sowie das defekte lokale Ziel, was alles 01-deploy.sh einrichtet. SCENARIOS="" zu setzen führt allein die isolierte Hälfte aus.

    Die isolierten Stages. pepsi-ingress lässt eine Nachricht immer bei init zu (pepsi_common::stage::INITIAL_STAGE), sodass ein Szenario immer nur die Stages erreichen kann, durch die die installierte Pipeline sie leitet. Jedes Stage-Programm, das nicht in diese Pipeline verdrahtet ist, ist daher für keine Nachricht erreichbar, wie auch immer sie konstruiert ist.

    Deshalb hat der Lauf eine zweite Hälfte. Er hängt benchmark-eigene [stage-bench-*]-Abschnitte an die installierte Konfiguration an — jeder mit den sinnvollen Standardwerten der betreffenden Stage, und jede Kante zeigt auf eine gemeinsame bench-discard-Senke (pepsi-stage-discard), sodass nichts, was eine isolierte Stage weiterschaltet, in die Live-Pipeline oder ins Netz entkommen kann — und injiziert dann eine Nachricht der jeweiligen Größe direkt in jede von ihnen mit der SQL-Funktion pepsi.workqueue_inject, also genau so, wie es eine Stage tut, die Mail erzeugt (StageContext::enqueue_new). workqueue_inject benachrichtigt den workqueue-Kanal, sodass der Dispatcher den Datensatz sofort aufgreift, die Stage als gewöhnlicher Worker läuft und ihre Kosten wie alle anderen in pepsi.stage_stats landen. Zu injizieren statt zu senden nimmt außerdem pepsi-ingress, SMTP und die Zulassungsgrenzen aus der Messung heraus, sodass eine isolierte Zahl die Kosten der Stage selbst sind und nichts sonst.

    isolierte Stage

    Programm

    was konfiguriert ist und was es daher misst

    bench-discard

    pepsi-stage-discard

    DISPOSITION = success, BOUNCE = no — die gemeinsame Senke, sodass ihre eigene Zeile zugleich die blanke Untergrenze der Pipeline je Nachricht ist

    bench-aliases

    pepsi-stage-aliases

    eine ALIASES-Map mit drei Einträgen (eine Expansion auf zwei Ziele, ein transitiver Hop, ein @domain-Catch-All) und ein Empfänger, der expandiert

    bench-route

    pepsi-stage-route

    eine ausdrückliche ROUTES-Ausnahme plus MANAGED_STAGE; der Empfänger liegt bei einer verwalteten Domain, sodass der verwaltete Zweig genommen wird

    bench-vacation

    pepsi-stage-vacation

    VACATION_RANGES, die den heutigen Tag abdecken (in der Zeitzone des Servers), und SUPPRESS_DAYS = 0, sodass jede Nachricht wirklich eine Benachrichtigung verfasst und injiziert, statt den Ausgang „niemand ist abwesend“ zu nehmen

    bench-autocrypt-learn

    pepsi-stage-autocrypt-learn

    Standardwerte, auf einer Nachricht ohne Autocrypt:-Header — der Durchlauf über eine Nachricht, an der es nichts zu lernen gibt, was bis auf einen winzigen Bruchteil alle echte eingehende Mail ist

    bench-vks-confirm

    pepsi-stage-vks-confirm

    ein VKS_HOST, der der Bench-Absender nicht ist, sodass die Nachricht durchläuft und keinem Link gefolgt wird (keine Netzwerk-E/A)

    bench-auto-pay

    pepsi-stage-auto-pay

    das Wallet, das wie das installierte [stage-pay] konfiguriert ist, auf einer Nachricht, die keine Zahlungsforderung ist — wieder der häufige eingehende Fall. Die zahlende Hälfte braucht einen laufenden Händler und ein Wallet; 12/13 treiben das an.

    bench-secure-link

    pepsi-stage-secure-link

    Standardwerte; terminal. Das globale [pepsi-secure-link] NOTIFY_STAGE wird für den Lauf auf die Senke gerichtet, sodass das Messen der Stage keine Mail auf die Leitung bringt.

    bench-lmtp

    pepsi-stage-relay-to-lmtp

    der lokale Dovecot-Socket und das Wegwerf-Postfach, das 11-lmtp-test.sh einrichtet; übersprungen, sofern nicht beide vorhanden sind

    bench-milter

    pepsi-stage-milter

    übersprungen, sofern nicht in BENCH_MILTER_ADDR ein Milter benannt ist (z. B. inet:8890@127.0.0.1)

    Eine isolierte Stage, deren Programm nicht installiert ist oder deren externe Abhängigkeit fehlt, wird vor dem Schreiben der Abschnitte fallen gelassen; und wenn der Dispatcher mit ihnen nicht wieder hochkommt, wird das Anhängen zurückgerollt und die isolierte Hälfte aufgegeben, statt den Host ohne seinen Koordinator zu lassen. ISO_STAGES="" überspringt die Hälfte vollständig.

    Bewusst nicht isoliert: pepsi-stage-encrypt, -decrypt, -dot-forward, -srs, -dkim-sign und -if — die installierte Pipeline leitet durch sie alle, sodass die Szenarien sie bereits in situ messen, was die ehrlichere Zahl ist.

    Die Matrix lesen. Stages, die den Nachrichtentext lesen (arc verify, decrypt, detect-language, encrypt, dkim-sign, secure-link, relay/maildir), steigen mit der Größe; reine Metadaten-Stages (if-Zweige, srs, whitelist, aliases, route) bleiben flach. Die Relay-Stages (smarthost, internet, delaytest, bench-lmtp) sind netzwerk- oder MDA-inklusive — sie halten die ausgehende Transaktion offen, sodass ihre Zahl der Round-Trip zum nächsten Hop ist, nicht lokale CPU-Zeit; der Benchmark gibt einen ausdrücklichen Hinweis aus, wenn eine solche Stage öfter lief, als Nachrichten injiziert wurden, d. h. wenn der nächste Hop aufgeschoben hat und die Zahl Wiederholungen mit einmittelt.

  2. ``09-latency-bench.sh`` — Leerlauf-Ende-zu-Ende-Latenz. Führt die gesamte Inject→Beobachten-Schleife auf dem ADMIN-Host aus (eine Uhr, kein ssh-pro-Poll) für N kurze Nachrichten (Standard 200) und berichtet Min/Mittelwert/Median/p95/Max sowie die Summe der mittleren Verarbeitungszeit pro Stage als Untergrenze. POLL_MS (Standard 2) ist, wie oft nach der zugestellten Nachricht gesehen wird, und damit die Quantisierung jeder ausgegebenen Zahl; es muss daher klein gegenüber der gemessenen Latenz sein: Ein Poll-Intervall in der Nähe der echten Latenz rundet die Verteilung in wenige Eimer und lässt das Polling anstelle der Pipeline über den p95 entscheiden.

  3. ``10-goodput-bench.sh`` — Sättigungs-Nutzdurchsatz, drei Lastpfade, zwei Instrumente. Die drei Pfade sind A reine Pipeline (lokales Inject → lokales Maildir), B Netzwerk + Front-End (entfernter Absender → lokales Maildir) und C ausgehendes Relay (autorisierter ALICE-Ursprung → relay-to-internet → MX von CAROL). Jeder wird zweimal gemessen:

    • ein Burst-Leerlaufen mit fester Größe (run_burst) bietet BURST_MSGS Nachrichten am Anschlag an und misst, wie lange die Warteschlange zum Leerwerden braucht, und berichtet die Rate sowie CPU, Platten-%util und Leitungsauslastung des Hosts über genau dieses Fenster;

    • eine Rampe (run_ramp) verdoppelt die angebotene Last alle STEP_SECS, bis die Warteschlange über SAT_QUEUE liegt und weiter wächst.

    Der Burst ist die Zahl; die Rampe ist die Gestalt. Eine Rampe schreibt Abschlüsse demjenigen festen Fenster zu, in das sie fallen, und einen Generator zu starten kostet einen ssh-Umlauf und einen Python-Start auf dem injizierenden Host — einen großen Teil eines Fünf-Sekunden-Fensters, wenn dieser Host entfernt ist, und einen noch größeren, wenn er klein ist; daher liest die Rampe für denselben Pfad deutlich weniger als der Burst. Lesen Sie die Rampe dafür, wo die Warteschlange zu wachsen beginnt und was der Host dort tut.

    Der Burst jedes Szenarios läuft vor der Rampe dieses Szenarios, denn eine Rampe endet absichtlich gesättigt: Sie hinterlässt einen großen Rückstau und ein Maildir mit ebenso vielen Dateien darin, und ein Burst, der auf dieser Erholung gemessen wird, liest weit weniger als einer auf einer beruhigten Maschine.

    Ein Burst, der auf ein Postfach auf einem anderen Host zielt, verwendet BURST_MSGS_REMOTE (2 000) statt BURST_MSGS (20 000) und leert dieses Postfach danach. Beides betrifft das nächste Skript und nicht dieses: Zehntausende an ein kleines Gegenüber relayte Nachrichten lassen es lange Zeit mit Zustellen beschäftigt, und jeder Helfer, der eine Nachricht findet, tut das durch Durchsuchen des Postfachs — ein Gegenüber, das sie noch hält, lässt also die Suchen des nächsten Skripts in einen Timeout laufen, obwohl es jede Nachricht zugestellt hat.

    Vier Dinge darüber, wie es die Rampe fährt, sollte man wissen, bevor man eine Zahl daraus abliest.

    • Die Rampe ist geometrisch. Jeder Schritt multipliziert die angebotene Last mit RAMP_FACTOR (Standard 2), anstatt eine feste RATE_UNIT zu addieren. Eine lineare Rampe kann nicht beide Enden des Bereichs bedienen, auf den diese Suite gerichtet wird: Eine kleine VM und ein großer Server erreichen ihr Plateau um Größenordnungen voneinander entfernt, sodass eine Einheit, die das Knie des kleinen Hosts findet, beim großen weit zu kurz aufhört und MAX_STEPS als dessen Obergrenze meldet. Was es kostet, ist Auflösung am Knie — die angebotene Rate, die das Plateau auslöste, ist nur bis auf einen Faktor RAMP_FACTOR eingegrenzt. Die nachhaltige Zahl ist davon nicht betroffen: Das ist die gemessene Abschlussrate. RAMP_FACTOR=1 ergibt eine lineare Rampe in RATE_UNIT-Schritten.

    • Sättigung ist eine wachsende Warteschlange, keine flache Rate. Die Rampe hält an, wenn die Warteschlange „über SAT_QUEUE liegt und STALL_STEPS (Standard 2) aufeinanderfolgende Schritte lang weiter wächst“, denn genau das ist Sättigung — mehr angenommen als abgeschlossen. Ein Plateau der verarbeiteten Rate wäre das falsche Signal: Die Rate pro Fenster schwankt so oder so noch lange weiter, nachdem die Pipeline bereits voll ausgelastet ist, sodass einem Plateau-Test die Schritte ausgehen können, während die Warteschlange schon tief ist.

    • Es berichtet drei Auslastungen, und nur von einer darf extrapoliert werden. CPU, das %util der Platte, die PGDATA trägt, und die Leitungsauslastung der Netzwerkkarte werden über jeden Burst und jeden Rampenschritt erfasst. Die CPU folgt der Last; die Leitung ist weit von der Grenze entfernt, könnte es aber auf einer stärker belegten sein; Platten-%util darf nicht verwendet werden, denn es zählt Wanduhrzeit mit nicht leerer Anfragewarteschlange und nicht Gerätesättigung, und auf einem parallelen NVMe-Gerät muss es mit dem Durchsatz überhaupt nicht steigen. Die extrapolierte Spalte der Zusammenfassung skaliert daher mit der CPU und kappt das Ergebnis auf die beste Rate, die ein rein lokaler Pfad tatsächlich erreicht hat (siehe Hochrechnen über einen entfernten Engpass hinaus).

    • Sie erhöht die Worker-Zahl der Pipeline selbst, nicht nur die Annahmegrenzen: BENCH_STAGE_PAR (Standard 16) wird für den Lauf als PARALLELISM in jeden [stage-*]-Abschnitt geschrieben. Der ausgelieferte Standard von 4 existiert, damit eine Installation von der Stange zu einem max_connections von der Stange passt; ihn zu messen und die Antwort die Obergrenze der Maschine zu nennen heißt, den Standard zu messen. Da jeder Stage-Worker genau eine Datenbankverbindung hält, gibt das Skript anschließend Σ PARALLELISM plus die Pools des Dispatchers und der Server gegen PostgreSQLs max_connections aus und warnt, wenn es nicht passt — ein Worker, der keine Verbindung bekommt, meldet Datenbanküberlast, der Dispatcher reiht wieder ein und bremst, und die Rampe meldet stillschweigend eine niedrigere Zahl, sodass ein ungeprüfter Budgetfehler genau wie eine Kapazitätszahl aussieht.

26.3. Ratenbegrenzungs-Berichterstattung

Der parallele Lastgenerator hält die SMTP-Antwort jeder Ablehnung fest, sodass die eigene Front-End-Drosselung von Pepsi ([pepsi-ingress] CONN_RATE_PER_SECOND / CONN_RATE_BURST) als 421s erscheint und mit der zu erhöhenden Stellschraube berichtet wird.

Für den ausgehenden Pfad (Szenario C), der MTAs berührt, die Pepsi nicht kontrolliert, liest das Skript Pepsis Sicht auf den nächsten Hop — state.bounce-SMTP-Codes/-Text, pepsi.tls_session, das Relay-Journal — und meldet, wenn es 4xx- / „rate“/„too many“/„try again“/„throttle“-Aufschübe sieht, externe Ratenbegrenzung mit konkreten Lösungen:

  • Auf dem entfernten MTA (Postfix): erhöhen Sie smtpd_client_connection_rate_limit, smtpd_client_message_rate_limit, das *_connection_count_limit, weiten Sie anvil_rate_time_unit aus oder nehmen Sie die IP des Pepsi-Hosts über smtpd_client_event_limit_exceptions / einen dedizierten Transport aus.

  • Auf der Pepsi-Seite (um unter einem Limit zu bleiben, das Sie nicht ändern können): senken Sie [stage-internet] PARALLELISM und takten Sie Wiederholungen über RETRY_INITIAL / RETRY_MAX_INTERVAL / RETRY_FACTOR.

26.4. Nützliche Stellschrauben

Skript

Stellschrauben

alle

BENCH_STATS_INTERVAL, BENCH_POLL_INTERVAL, BENCH_LOG_LEVEL, MSGS_PER_CONN, LOAD_PROCS, BENCH_WHITELIST_NAME, BENCH_CONFIRM_GRACE; und, für die Läufe, die bench_set_admission aufrufen, BENCH_CONN_RATE / BENCH_CONN_BURST / BENCH_MAX_CONN / BENCH_DB_POOL / BENCH_RELAY_PAR_MULT / BENCH_QUEUE_LIMIT / BENCH_STAGE_PAR / BENCH_DISPATCH_POOL

08

SIZES, REPEAT, SCENARIOS, PERMSG_TIMEOUT, BOUNCE_TIMEOUT_S, PAY_DEADLINE_S, PAY_TIMEOUT_S; und für die isolierte Hälfte ISO_STAGES, ISO_SIZES, ISO_REPEAT, ISO_TIMEOUT, ISO_LOCAL_RCPT, ISO_SENDER, ISO_LMTP_SOCKET, ISO_LMTP_USER, BENCH_MILTER_ADDR

09

N, WARMUP, MSG_BYTES, POLL_MS, LATENCY_MAX_MS, PERMSG_TIMEOUT_MS

10

RATE_UNIT, RAMP_FACTOR, PAR_UNIT, PAR_MAX, MAX_STEPS, STEP_SECS, MSG_BYTES, STALL_STEPS, SAT_QUEUE, DRAIN_TIMEOUT, WARMUP_MSGS, WARMUP_DRAIN, RUN_A/RUN_B/RUN_C; und für die Burst-Hälfte BURST_MSGS, BURST_MSGS_REMOTE, BURST_PAR, BURST_TIMEOUT, BURST_SETTLE