26. Suite de benchmarks

Les scripts tests/NN-*-bench.sh mesurent la vitesse à laquelle le pipeline Pepsi en service traite le courrier. Ils réutilisent les mêmes hôtes, comptes et helpers que la suite de tests (tests/test-accounts.ini, tests/lib.sh) plus un tests/bench-lib.sh partagé, et se pilotent de la même manière

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

Ils ne font pas partie de make integrationtests (ils sont lents et chargent délibérément le MTA). Chacun exécute d’abord preflight_common, de sorte qu’il SKIP proprement si les hôtes, la base de données ou les services en fonctionnement sont injoignables. Exécutez d’abord tests/01-deploy.sh pour que le pipeline — y compris la cible de remise locale en Maildir DAVE — existe.

Ce chapitre décrit la méthode. Pour savoir comment lire les résultats — coût par étape, latence au repos et goodput à saturation — et quoi régler, voir le chapitre Performance.

26.1. Comment les chiffres sont obtenus

  • Le coût par étape provient du chronométrage propre du dispatcher : pepsi-dispatch note chaque exécution d’étape dans pepsi.stage_stats(stage, messages, duration_us). Prendre un instantané de cette table avant et après un message donne le nombre exact de microsecondes par message et par étape — aussi bien pour une étape atteinte par un message envoyé en SMTP que pour une étape à laquelle un message est injecté directement, puisque les deux s’exécutent comme des workers dispatchés ordinaires. Les benchmarks abaissent [pepsi-dispatch] STATS_INTERVAL (et POLL_INTERVAL) pour que les compteurs soient vidés rapidement, et règlent aussi [pepsi] LOG à un niveau discret (warn par défaut, redéfinissable via BENCH_LOG_LEVEL) afin que la journalisation par message ne gonfle pas les mesures — ce réglage est lu par chaque composant, donc il couvre le dispatcher et les workers d’étape qu’il lance (lesquels ne prennent pas d’indicateur de journal propre). pepsi-dispatch et pepsi-ingress sont tous deux redémarrés pour que le nouveau niveau prenne effet, et les benchmarks restaurent la configuration déployée à la sortie (une sauvegarde unique pepsi.conf.bench.bak sur l’hôte ADMIN, rejouée par un trap … EXIT).

  • Le débit utile est le delta du compteur global pepsi.dispatch_stats.messages_processed sur chaque fenêtre de 5 secondes. Il est écrit tous les STATS_INTERVAL, il retarde donc, à haut débit, d’au plus un intervalle sur la vérité ; la colonne deliv/s à côté — les fichiers apparaissant dans le Maildir/new de destination — est le contrôle instantané, et si les deux diffèrent de la valeur d’un pas, c’est ce retard et non un message perdu.

  • La remise est recoupée en comptant les fichiers dans le Maildir/new cible.

  • Le générateur de charge est plusieurs processus, pas un seul. load_burst répartit une rafale sur LOAD_PROCS (4 par défaut) processus système, chacun exécutant sa part des threads sur sa propre tranche de la plage de messages, puis fusionne leur JSON. Un processus Python est un GIL, et à débit élevé c’est l’interpréteur qui empêche le chiffre de monter — ce qui signifie qu’un générateur mono-processus cesse de mesurer le système testé et commence à se mesurer lui-même.

Pour empêcher l’étape entrante de péage à l’envoi (anti-spam) de faire barrière à la charge, les benchmarks mettent au préalable l’expéditeur en liste blanche (bench_whitelist_sender), de sorte que check-whitelist met state.spam=false et qu”anti-spam court-circuite.

Note

La rampe ne peut pas constituer d’arriéré via SMTP, et ce n’est pas un défaut de la rampe. pepsi-ingress valide chaque admission de façon synchrone — un 250 signifie que le message a atteint le disque —, de sorte que sur un hôte où l’admission est la plus étroite des deux, la file reste quasi vide quelle que soit la pression du générateur et la rampe mesure le frontal. Pour mesurer le pipeline avec une file profonde, insérez les enregistrements directement dans pepsi.workqueue avec status = 'pending' en une seule transaction et chronométrez le vidage ; le chapitre Performance montre comment faire, et l’écart entre les deux chiffres est la partie intéressante.

26.2. Les trois benchmarks

  1. ``08-perstage-bench.sh`` — coût par étape en fonction de la taille du message. Affiche une unique matrice stage × size de µs/msg couvrant chaque étape que Pepsi possède, remplie par deux moitiés qui répondent à deux questions différentes : les scénarios chronomètrent les étapes du pipeline déployé in situ, et les étapes isolées chronomètrent chaque programme d’étape par lequel le pipeline déployé n’achemine aucun message.

    Les scénarios. Un seul message ne traverse qu’un seul chemin, il ne peut donc chronométrer que les étapes de ce chemin ; couvrir l’ensemble du pipeline bidirectionnel et ramifié demande donc plusieurs messages — un par chemin. Le benchmark pilote un ensemble de SCENARIOS, chacun un message dont le parcours révèle un groupe différent d’étapes, et accumule leurs coûts par étape dans la matrice :

    scénario

    message

    étapes qu’il révèle pour la première fois

    inbound-local

    CAROL → DAVE (entrant étranger → Maildir local)

    init (arc), route (if), decrypt, detect-language, block-language, check-whitelist, anti-spam (court-circuit), list (routeur de listes), forward (dot-forward), local (maildir)

    inbound-relay

    CAROL → ALICE (entrant → relais ultérieur)

    srs, dkim-sign-relay, smarthost (le destinataire non local est détaché par local vers cette queue)

    outbound

    ALICE → CAROL (soumission d’origine locale → relais direct)

    list-out (routeur de listes), edit-settings, auto-whitelist, delay-route (if), encrypt, dkim-sign, internet (relay-to-internet)

    bounce

    CAROL → dave-broken (la remise locale échoue)

    bounce (le helper maildir échoue → un SideClone de rebond est acheminé vers l’étape de rebond → dkim-sign)

    bounce-language

    CAROL → DAVE, corps en français (langue sur liste noire)

    bounce-language (block-language fait rebondir un message fr)

    payment (sur consentement explicite)

    CAROL (pas en liste blanche) → DAVE (barrière de péage à l’envoi)

    bounce-payment (raccourcit PAYMENT_DEADLINE pour l’exécution)

    delay (sur consentement explicite)

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

    delaytest (relay-to-smarthost vers un MTA non routable)

    Les SCENARIOS par défaut sont les cinq rapides ; ajoutez payment et/ou delay pour chronométrer aussi les deux dernières étapes déployées (elles sont plus lentes — limitées par le marchand et par les réessais). Les scénarios payment/delay et les deux étapes sur consentement explicite nécessitent un expéditeur qui n’est pas d’origine locale ainsi que la cible locale défectueuse, que 01-deploy.sh provisionne tous. Régler SCENARIOS="" n’exécute que la moitié isolée.

    Les étapes isolées. pepsi-ingress admet toujours un message à init (pepsi_common::stage::INITIAL_STAGE), de sorte qu’un scénario ne peut jamais atteindre que les étapes par lesquelles le pipeline déployé l’achemine. Tout programme d’étape qui n’est pas câblé dans ce pipeline est donc inatteignable par n’importe quel message, quelle que soit sa construction.

    L’exécution a donc une seconde moitié. Elle ajoute à la configuration déployée des sections [stage-bench-*] propres au benchmark — chacune avec les valeurs par défaut sensées de son étape, et chaque arête pointant vers un unique puits partagé bench-discard (pepsi-stage-discard), de sorte que rien de ce qu’une étape isolée fait avancer ne peut s’échapper dans le pipeline en service ni sur le réseau — puis injecte un message calibré directement à chacune d’elles avec la fonction SQL pepsi.workqueue_inject, ce qui est exactement ce que fait une étape qui émet du courrier (StageContext::enqueue_new). workqueue_inject notifie le canal workqueue, de sorte que le dispatcher reprend la ligne aussitôt, que l’étape s’exécute comme un worker ordinaire et que son coût atterrit dans pepsi.stage_stats comme n’importe quel autre. Injecter plutôt qu’envoyer sort aussi pepsi-ingress, SMTP et les limites d’admission de la mesure, de sorte qu’un chiffre isolé est le coût propre de l’étape et rien d’autre.

    étape isolée

    programme

    ce qui est configuré, et donc ce qui est mesuré

    bench-discard

    pepsi-stage-discard

    DISPOSITION = success, BOUNCE = no — le puits partagé, de sorte que sa propre ligne est aussi le plancher nu par message du pipeline

    bench-aliases

    pepsi-stage-aliases

    une table ALIASES à trois entrées (une expansion vers deux cibles, un saut transitif, un attrape-tout @domain) et un destinataire qui se développe

    bench-route

    pepsi-stage-route

    une exception ROUTES explicite plus MANAGED_STAGE ; le destinataire est à un domaine géré, la branche gérée est donc prise

    bench-vacation

    pepsi-stage-vacation

    des VACATION_RANGES couvrant aujourd’hui (dans le fuseau horaire du serveur) et SUPPRESS_DAYS = 0, de sorte que chaque message compose et injecte réellement un avis au lieu de prendre la sortie « personne n’est absent »

    bench-autocrypt-learn

    pepsi-stage-autocrypt-learn

    les valeurs par défaut, sur un message sans en-tête Autocrypt: — la passe sur un message où il n’y a rien à apprendre, ce qu’est tout le courrier entrant réel à une frange près

    bench-vks-confirm

    pepsi-stage-vks-confirm

    un VKS_HOST que l’expéditeur du benchmark n’est pas, de sorte que le message passe et qu’aucun lien n’est suivi (aucune E/S réseau)

    bench-auto-pay

    pepsi-stage-auto-pay

    le wallet configuré comme le [stage-pay] déployé, sur un message qui n’est pas une demande de paiement — là encore le cas entrant courant. La moitié qui paie nécessite un marchand et un wallet en service ; 12/13 pilotent cela.

    bench-secure-link

    pepsi-stage-secure-link

    les valeurs par défaut ; terminale. Le [pepsi-secure-link] NOTIFY_STAGE global est pointé vers le puits pour la durée de l’exécution, de sorte que chronométrer l’étape ne met aucun courrier sur le fil.

    bench-lmtp

    pepsi-stage-relay-to-lmtp

    la socket Dovecot locale et la boîte aux lettres jetable que provisionne 11-lmtp-test.sh ; ignorée si les deux ne sont pas présentes

    bench-milter

    pepsi-stage-milter

    ignorée sauf si un milter est nommé dans BENCH_MILTER_ADDR (p. ex. inet:8890@127.0.0.1)

    Une étape isolée dont le programme n’est pas installé, ou dont la dépendance externe manque, est écartée avant que les sections soient écrites ; et si le dispatcher ne se relève pas avec elles, l’ajout est annulé et la moitié isolée est abandonnée plutôt que de laisser l’hôte sans son coordinateur. ISO_STAGES="" saute entièrement cette moitié.

    Délibérément non isolés : pepsi-stage-encrypt, -decrypt, -dot-forward, -srs, -dkim-sign et -if — le pipeline déployé achemine par eux tous, de sorte que les scénarios les chronomètrent déjà in situ, ce qui est le chiffre le plus honnête.

    Lire la matrice. Les étapes qui lisent le corps (vérification arc, decrypt, detect-language, encrypt, dkim-sign, secure-link, relay/maildir) croissent avec la taille ; les étapes qui ne lisent que les métadonnées (branches if, srs, whitelist, aliases, route) restent plates. Les étapes de relais (smarthost, internet, delaytest, bench-lmtp) sont inclusives du réseau ou du MDA — elles maintiennent ouverte la transaction sortante, de sorte que leur chiffre est l’aller-retour vers le saut suivant et non le CPU local ; le benchmark imprime une note explicite lorsqu’une telle étape s’est exécutée plus de fois qu’il n’a été injecté de messages, c’est-à-dire lorsque le saut suivant a différé et que le chiffre intègre des réessais dans sa moyenne.

  2. ``09-latency-bench.sh`` — latence de bout en bout au repos. Exécute toute la boucle injection→observation sur l’hôte ADMIN (une seule horloge, pas de ssh par sondage) pour N messages courts (200 par défaut) et rapporte min/moyenne/médiane/p95/max, plus la somme du temps de traitement moyen par étape comme plancher. POLL_MS (2 par défaut) est la fréquence à laquelle il cherche le message remis, et donc la quantification de chaque chiffre qu’il imprime ; elle doit donc être petite devant la latence mesurée : un intervalle de sondage proche de la latence réelle arrondit la distribution en quelques paniers et laisse le sondage, plutôt que le pipeline, décider du p95.

  3. ``10-goodput-bench.sh`` — débit utile à saturation, trois chemins de charge, deux instruments. Les trois chemins sont A pipeline pur (injection locale → Maildir local), B réseau + frontal (expéditeur distant → Maildir local) et C relais sortant (origine ALICE autorisée → relay-to-internet → MX de CAROL). Chacun est mesuré deux fois :

    • un vidage par rafale de taille fixe (run_burst) offre BURST_MSGS messages à plein régime et chronomètre le vidage de la file, en rapportant le débit ainsi que le CPU de l’hôte, le %util du disque et l’utilisation du lien sur exactement cette fenêtre ;

    • une rampe (run_ramp) double la charge offerte tous les STEP_SECS jusqu’à ce que la file dépasse SAT_QUEUE et continue de croître.

    La rafale donne le chiffre ; la rampe donne la forme. Une rampe attribue les achèvements à la fenêtre fixe dans laquelle ils tombent, et lancer un générateur coûte un aller-retour ssh et un démarrage de Python sur l’hôte injecteur — une large part d’une fenêtre de cinq secondes quand cet hôte est distant, et plus large encore quand il est petit, de sorte que la rampe affiche bien moins que la rafale pour le même chemin. Lisez la rampe pour savoir où la file commence à croître et ce que fait l’hôte à cet endroit.

    La rafale de chaque scénario est exécutée avant la rampe de ce scénario, car une rampe se termine délibérément saturée : elle laisse un gros arriéré et un Maildir contenant autant de fichiers, et une rafale mesurée par-dessus cette récupération affiche bien moins qu’une rafale sur une machine apaisée.

    Une rafale visant une boîte sur un autre hôte utilise BURST_MSGS_REMOTE (2 000) au lieu de BURST_MSGS (20 000), et vide cette boîte ensuite. Les deux concernent le script suivant plutôt que celui-ci : des dizaines de milliers de messages relayés vers un petit pair le laissent remettre pendant longtemps, et chaque helper qui trouve un message le fait en balayant la boîte — un pair qui les détient encore fait donc expirer les recherches du script suivant, alors même qu’il a remis chaque message.

    Quatre choses sur la façon dont elle monte en charge méritent d’être connues avant d’en lire un chiffre.

    • La rampe est géométrique. Chaque pas multiplie la charge offerte par RAMP_FACTOR (2 par défaut) au lieu d’ajouter une RATE_UNIT fixe. Une rampe linéaire ne peut pas servir les deux extrémités de la plage sur laquelle cette suite est pointée : une petite VM et un gros serveur plafonnent à des ordres de grandeur d’écart, si bien qu’une unité qui trouve le coude du petit hôte s’arrête bien avant celui du grand et rapporte MAX_STEPS comme son plafond. Ce que cela coûte, c’est de la résolution au coude — le débit offert qui a provoqué le plateau n’est encadré qu’à un facteur RAMP_FACTOR près. Le chiffre soutenu n’en est pas affecté : c’est le taux d’achèvement mesuré. RAMP_FACTOR=1 donne une rampe linéaire par pas de RATE_UNIT.

    • La saturation est une file qui croît, pas un débit plat. La rampe s’arrête lorsque la file est « au-dessus de SAT_QUEUE et croît encore pendant STALL_STEPS (2 par défaut) pas consécutifs », ce qui est la définition même de la saturation — plus d’admis que d’achevés. Un plateau du débit traité serait le mauvais signal : le débit par fenêtre continue d’osciller dans un sens ou dans l’autre longtemps après que le pipeline tourne à plein régime, de sorte qu’un test de plateau peut épuiser ses pas alors que la file est déjà profonde.

    • Il rapporte trois utilisations, et une seule autorise une extrapolation. Le CPU, le %util du disque portant PGDATA et l’utilisation du lien de la carte réseau sont échantillonnés sur chaque rafale et chaque pas de rampe. Le CPU suit la charge ; le lien est loin d’être contraignant mais pourrait l’être sur un lien plus chargé ; le %util du disque ne doit pas être utilisé, car il compte le temps d’horloge avec une file de requêtes non vide et non la saturation du périphérique, et sur un périphérique NVMe parallèle il n’a nullement besoin de croître avec le débit. La colonne extrapolée du récapitulatif met donc à l’échelle par le CPU et plafonne le résultat au meilleur débit qu’un chemin entièrement local a réellement atteint (voir Extrapoler au-delà d’un goulot d’étranglement distant).

    • Elle augmente le nombre de workers du pipeline lui-même, et pas seulement les plafonds d’admission : BENCH_STAGE_PAR (16 par défaut) est écrit comme PARALLELISM dans chaque section [stage-*] pour la durée du run. Le défaut déployé de 4 existe pour qu’une installation sortie de la boîte tienne dans un max_connections sorti de la boîte ; le mesurer et appeler la réponse le plafond de la machine, c’est mesurer le défaut. Comme chaque worker d’étape détient exactement une connexion à la base, le script imprime ensuite Σ PARALLELISM plus les pools du dispatcher et des serveurs face au max_connections de PostgreSQL et avertit lorsque cela ne tient pas — un worker qui n’obtient pas de connexion signale une surcharge de la base, le dispatcher remet en file et freine, et la rampe rapporte discrètement un chiffre plus bas, si bien qu’une erreur de budget non vérifiée ressemble exactement à un chiffre de capacité.

26.3. Rapport de limitation de débit

Le générateur de charge parallèle note la réponse SMTP de chaque rejet, de sorte que la limitation frontale propre à Pepsi ([pepsi-ingress] CONN_RATE_PER_SECOND / CONN_RATE_BURST) apparaît sous forme de 421 et est signalée avec le réglage à augmenter.

Pour le chemin sortant (scénario C), qui touche des MTA que Pepsi ne contrôle pas, le script lit la vision qu’a Pepsi du saut suivant — codes et texte SMTP de state.bounce, pepsi.tls_session, le journal du relais — et, s’il voit des différés 4xx / « rate »/« too many »/« try again »/« throttle », signale une limitation de débit externe avec des corrections concrètes :

  • Sur le MTA distant (Postfix) : augmenter smtpd_client_connection_rate_limit, smtpd_client_message_rate_limit, les *_connection_count_limit, élargir anvil_rate_time_unit, ou exempter l’IP de l’hôte Pepsi via smtpd_client_event_limit_exceptions / un transport dédié.

  • Du côté Pepsi (pour rester sous une limite que vous ne pouvez pas changer) : abaisser [stage-internet] PARALLELISM et cadencer les réessais via RETRY_INITIAL / RETRY_MAX_INTERVAL / RETRY_FACTOR.

26.4. Réglages utiles

script

réglages

tous

BENCH_STATS_INTERVAL, BENCH_POLL_INTERVAL, BENCH_LOG_LEVEL, MSGS_PER_CONN, LOAD_PROCS, BENCH_WHITELIST_NAME, BENCH_CONFIRM_GRACE ; et, pour les runs qui appellent bench_set_admission, 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 ; et pour la moitié isolée 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 ; et pour la moitié rafale BURST_MSGS, BURST_MSGS_REMOTE, BURST_PAR, BURST_TIMEOUT, BURST_SETTLE