20. Benchmark Suite¶
The tests/NN-*-bench.sh scripts measure how fast the live Pepsi pipeline
processes mail. They reuse the same hosts, accounts and helpers as the
correctness suite (tests/test-accounts.ini,
tests/lib.sh) plus a shared tests/bench-lib.sh, and are driven the same
way:
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
They are not part of make integrationtests (they are slow and
deliberately load the MTA). Each one runs preflight_common first, so it SKIPs
cleanly if the live hosts/DB/services are unreachable. Run tests/01-deploy.sh
first so the pipeline — including the DAVE local-Maildir delivery target —
exists.
This chapter describes the method; for a worked set of measured results (per-stage cost, idle latency and saturation goodput, with the test host’s hardware), see the Performance chapter.
20.1. How the numbers are obtained¶
Per-stage cost comes from the dispatcher’s own timing:
pepsi-dispatchrecords every stage execution intopepsi.stage_stats(stage, messages, duration_us). Snapshotting that table before/after a message gives exact microseconds-per-message per stage — for a stage reached by an SMTP-sent message and for one injected straight at it alike, since both run as ordinary dispatched workers. The benchmarks lower[pepsi-dispatch] STATS_INTERVAL(andPOLL_INTERVAL) so the counters flush promptly, and also set[pepsi] LOGto a quiet level (warnby default, overridable viaBENCH_LOG_LEVEL) so per-message logging does not inflate the timings — that knob is read by every component, so it covers the dispatcher and the stage workers it spawns (which take no log flag of their own). Bothpepsi-dispatchandpepsi-ingressare restarted so the new level takes effect, and the benchmarks restore the deployed config on exit (a one-shotpepsi.conf.bench.bakbackup on the ADMIN host, replayed by atrap … EXIT).Goodput is the delta of the global
pepsi.dispatch_stats.messages_processedcounter over each 5-second window.Delivery is cross-checked by counting files in the target
Maildir/new.
To keep the inbound pay-to-send (anti-spam) stage from gating the load, the
benchmarks pre-whitelist the sender (bench_whitelist_sender), so
check-whitelist sets state.spam=false and anti-spam short-circuits.
20.2. The three benchmarks¶
``08-perstage-bench.sh`` — per-stage cost vs message size. Prints one
stage × sizematrix of µs/msg covering every stage Pepsi has, filled in by two halves that answer two different questions: the scenarios time the stages of the deployed pipeline in situ, and the isolated stages time every stage program the deployed pipeline routes no message through.The scenarios. A single message only traverses one path, so it can only time the stages on that path; covering the whole bidirectional, branching pipeline therefore takes several messages — one per path. The benchmark drives a set of
SCENARIOS, each a message whose journey reveals a different group of stages, and accumulates their per-stage costs into the matrix:scenario
message
stages it newly reveals
inbound-localCAROL→DAVE(foreign inbound → local Maildir)init(arc),route(if),decrypt,detect-language,block-language,check-whitelist,anti-spam(short-circuit),forward(dot-forward),local(maildir)inbound-relayCAROL→ALICE(inbound → onward relay)srs,dkim-sign-relay,smarthost(the non-local recipient is peeled bylocalonto that tail)outboundALICE→CAROL(local-origin submission → direct relay)edit-settings,auto-whitelist,delay-route(if),encrypt,dkim-sign,internet(relay-to-internet)bounceCAROL→dave-broken(local delivery fails)bounce(the maildir helper fails → a bounceSideCloneroutes to the bounce stage →dkim-sign)bounce-languageCAROL→DAVE, French body (blacklisted language)bounce-language(block-languagebounces anfrmessage)payment(opt-in)CAROL(not whitelisted) →DAVE(pay-to-send gate)bounce-payment(shortensPAYMENT_DEADLINEfor the run)delay(opt-in)ALICE→blackhole.invalid,ENVID=PEPSIDELAY(DELAY DSN)delaytest(relay-to-smarthost to an unrouteable MTA)The default
SCENARIOSare the five fast ones; appendpaymentand/ordelayto also time the last two deployed stages (they are slower — merchant- and retry-bound). Thepayment/delayscenarios and the two opt-in stages need a sender that is not local-origin and the broken local target, all of which01-deploy.shprovisions. SettingSCENARIOS=""runs the isolated half alone.The isolated stages.
pepsi-ingressalways admits a message atinit(pepsi_common::stage::INITIAL_STAGE), so a scenario can only ever reach the stages the deployed pipeline routes it through. Every stage program that is not wired into that pipeline is therefore unreachable by any message, however constructed — which is why a third of the tree used to go unmeasured.So the run has a second half. It appends benchmark-only
[stage-bench-*]sections to the deployed configuration — each with that stage’s own sensible defaults, and every edge pointing at one sharedbench-discardsink (pepsi-stage-discard), so nothing an isolated stage advances can escape into the live pipeline or onto the network — and then injects a sized message straight at each of them with thepepsi.ingress_injectSQL function, which is exactly what a stage that originates mail does (StageContext::enqueue_new).ingress_injectnotifies theingresschannel, so the dispatcher picks the row up at once, the stage runs as an ordinary worker, and its cost lands inpepsi.stage_statslike any other. Injecting rather than sending also takespepsi-ingress, SMTP and the admission limits out of the measurement, so an isolated figure is the stage’s own cost and nothing else.isolated stage
program
what is configured, and what it therefore measures
bench-discardpepsi-stage-discardDISPOSITION = success,BOUNCE = no— the shared sink, so its own row is also the pipeline’s bare per-message floorbench-aliasespepsi-stage-aliasesa three-entry
ALIASESmap (a two-target expansion, a transitive hop, an@domaincatch-all) and a recipient that expandsbench-routepepsi-stage-routeone explicit
ROUTEScarve-out plusMANAGED_STAGE; the recipient is at a managed domain, so the managed branch is takenbench-vacationpepsi-stage-vacationVACATION_RANGEScovering today (in the server’s timezone) andSUPPRESS_DAYS = 0, so every message really composes and injects a notice rather than taking the “nobody is away” exitbench-autocrypt-learnpepsi-stage-autocrypt-learndefaults, on a message with no
Autocrypt:header — the pass over a message with nothing to learn, which is what all but a sliver of real inbound mail isbench-vks-confirmpepsi-stage-vks-confirma
VKS_HOSTthe bench sender is not, so the message passes through and no link is followed (no network I/O)bench-auto-paypepsi-stage-auto-paythe wallet configured as the deployed
[stage-pay], on a message that is not a payment demand — again the common inbound case. The paying half needs a live merchant and wallet;12/13drive that.bench-secure-linkpepsi-stage-secure-linkdefaults; terminal. The global
[pepsi-secure-link] NOTIFY_STAGEis pointed at the sink for the run, so timing the stage puts no mail on the wire.bench-lmtppepsi-stage-relay-to-lmtpthe local Dovecot socket and the throwaway mailbox
11-lmtp-test.shprovisions; skipped unless both are presentbench-milterpepsi-stage-milterskipped unless a milter is named in
BENCH_MILTER_ADDR(e.g.inet:8890@127.0.0.1)An isolated stage whose program is not installed, or whose external dependency is missing, is dropped before the sections are written; and if the dispatcher does not come back up with them, the append is rolled back and the isolated half is abandoned rather than leaving the host without its coordinator.
ISO_STAGES=""skips the half entirely.Deliberately not isolated:
pepsi-stage-encrypt,-decrypt,-dot-forward,-srs,-dkim-signand-if— the deployed pipeline routes through all of them, so the scenarios already time them in situ, which is the more honest figure.Reading the matrix. Body-reading stages (arc verify, decrypt, detect-language, encrypt, dkim-sign, secure-link, relay/maildir) climb with size; metadata-only stages (if-branches, srs, whitelist, aliases, route) stay flat. The relay stages (
smarthost,internet,delaytest,bench-lmtp) are network- or MDA-inclusive — they hold the outbound transaction open, so their figure is the next hop’s round-trip, not local CPU; the benchmark prints an explicit note when such a stage ran more times than messages were injected, i.e. when the next hop deferred and the figure averages retries in.``09-latency-bench.sh`` — idle end-to-end latency. Runs the whole inject→observe loop on the ADMIN host (one clock, no ssh-per-poll) for
Nshort messages and reports min/mean/median/p95/max, plus the sum of mean per-stage processing time as a floor.``10-goodput-bench.sh`` — saturation goodput, three load paths. Ramps the offered load every 5 s until the processed rate plateaus while the queue grows, for: A pure pipeline (local inject → local Maildir), B network + front-end (remote sender → local Maildir), C outbound relay (authorized enoki origin → relay-to-internet → external MX). It prints a per-step table and a knee comparison (A vs B isolates the network/SMTP front-end; B vs C isolates the outbound relay).
20.3. Rate-limit reporting¶
The parallel load generator records the SMTP reply of every rejection, so
tripwire’s own front-end throttle ([pepsi-ingress] CONN_RATE_PER_SECOND /
CONN_RATE_BURST) shows up as 421s and is reported with the knob to
raise.
For the outbound path (scenario C), which touches MTAs Pepsi does not
control, the script reads Pepsi’s view of the next hop — state.bounce SMTP
codes/text, pepsi.tls_session, the relay journal — and, if it sees 4xx /
“rate”/”too many”/”try again”/”throttle” deferrals, reports external
rate-limiting with concrete fixes:
On the remote MTA (Postfix): raise
smtpd_client_connection_rate_limit,smtpd_client_message_rate_limit, the*_connection_count_limit, widenanvil_rate_time_unit, or exempt tripwire’s IP viasmtpd_client_event_limit_exceptions/ a dedicated transport.On the Pepsi side (to stay under a limit you cannot change): lower
[stage-internet] PARALLELISMand pace retries viaRETRY_INITIAL/RETRY_MAX_INTERVAL/RETRY_FACTOR.
20.4. Useful knobs¶
script |
knobs |
|---|---|
all |
|
08 |
|
09 |
|
10 |
|