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-dispatchnote chaque exécution d’étape danspepsi.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(etPOLL_INTERVAL) pour que les compteurs soient vidés rapidement, et règlent aussi[pepsi] LOGà un niveau discret (warnpar défaut, redéfinissable viaBENCH_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-dispatchetpepsi-ingresssont 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 uniquepepsi.conf.bench.baksur l’hôte ADMIN, rejouée par untrap … EXIT).Le débit utile est le delta du compteur global
pepsi.dispatch_stats.messages_processedsur chaque fenêtre de 5 secondes. Il est écrit tous lesSTATS_INTERVAL, il retarde donc, à haut débit, d’au plus un intervalle sur la vérité ; la colonnedeliv/sà côté — les fichiers apparaissant dans leMaildir/newde 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/newcible.Le générateur de charge est plusieurs processus, pas un seul.
load_burstrépartit une rafale surLOAD_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¶
``08-perstage-bench.sh`` — coût par étape en fonction de la taille du message. Affiche une unique matrice
stage × sizede µ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-localCAROL→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-relayCAROL→ALICE(entrant → relais ultérieur)srs,dkim-sign-relay,smarthost(le destinataire non local est détaché parlocalvers cette queue)outboundALICE→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)bounceCAROL→dave-broken(la remise locale échoue)bounce(le helper maildir échoue → unSideClonede rebond est acheminé vers l’étape de rebond →dkim-sign)bounce-languageCAROL→DAVE, corps en français (langue sur liste noire)bounce-language(block-languagefait rebondir un messagefr)payment(sur consentement explicite)CAROL(pas en liste blanche) →DAVE(barrière de péage à l’envoi)bounce-payment(raccourcitPAYMENT_DEADLINEpour l’exécution)delay(sur consentement explicite)ALICE→blackhole.invalid,ENVID=PEPSIDELAY(DSN DELAY)delaytest(relay-to-smarthost vers un MTA non routable)Les
SCENARIOSpar défaut sont les cinq rapides ; ajoutezpaymentet/oudelaypour 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énariospayment/delayet 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, que01-deploy.shprovisionne tous. RéglerSCENARIOS=""n’exécute que la moitié isolée.Les étapes isolées.
pepsi-ingressadmet 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 SQLpepsi.workqueue_inject, ce qui est exactement ce que fait une étape qui émet du courrier (StageContext::enqueue_new).workqueue_injectnotifie le canalworkqueue, 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 danspepsi.stage_statscomme n’importe quel autre. Injecter plutôt qu’envoyer sort aussipepsi-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-discardpepsi-stage-discardDISPOSITION = success,BOUNCE = no— le puits partagé, de sorte que sa propre ligne est aussi le plancher nu par message du pipelinebench-aliasespepsi-stage-aliasesune table
ALIASESà trois entrées (une expansion vers deux cibles, un saut transitif, un attrape-tout@domain) et un destinataire qui se développebench-routepepsi-stage-routeune exception
ROUTESexplicite plusMANAGED_STAGE; le destinataire est à un domaine géré, la branche gérée est donc prisebench-vacationpepsi-stage-vacationdes
VACATION_RANGEScouvrant aujourd’hui (dans le fuseau horaire du serveur) etSUPPRESS_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-learnpepsi-stage-autocrypt-learnles 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èsbench-vks-confirmpepsi-stage-vks-confirmun
VKS_HOSTque 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-paypepsi-stage-auto-payle 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/13pilotent cela.bench-secure-linkpepsi-stage-secure-linkles valeurs par défaut ; terminale. Le
[pepsi-secure-link] NOTIFY_STAGEglobal 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-lmtppepsi-stage-relay-to-lmtpla 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ésentesbench-milterpepsi-stage-milterignoré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-signet-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.``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
Nmessages 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.``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
ALICEautorisée → relay-to-internet → MX deCAROL). Chacun est mesuré deux fois :un vidage par rafale de taille fixe (
run_burst) offreBURST_MSGSmessages à 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%utildu disque et l’utilisation du lien sur exactement cette fenêtre ;une rampe (
run_ramp) double la charge offerte tous lesSTEP_SECSjusqu’à ce que la file dépasseSAT_QUEUEet 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 deBURST_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 uneRATE_UNITfixe. 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 rapporteMAX_STEPScomme 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 facteurRAMP_FACTORprès. Le chiffre soutenu n’en est pas affecté : c’est le taux d’achèvement mesuré.RAMP_FACTOR=1donne une rampe linéaire par pas deRATE_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_QUEUEet croît encore pendantSTALL_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
%utildu disque portantPGDATAet 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%utildu 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 commePARALLELISMdans 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 unmax_connectionssorti 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Σ PARALLELISMplus les pools du dispatcher et des serveurs face aumax_connectionsde 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, élargiranvil_rate_time_unit, ou exempter l’IP de l’hôte Pepsi viasmtpd_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] PARALLELISMet cadencer les réessais viaRETRY_INITIAL/RETRY_MAX_INTERVAL/RETRY_FACTOR.
26.4. Réglages utiles¶
script |
réglages |
|---|---|
tous |
|
08 |
|
09 |
|
10 |
|