27. Performance¶
Ce à quoi le pipeline Pepsi consacre son temps, comment lire les chiffres que produisent les scripts de benchmark sous tests/ (08-perstage-bench.sh, 09-latency-bench.sh et 10-goodput-bench.sh), et quels réglages les font bouger. La façon dont les scripts obtiennent leurs chiffres est décrite dans le chapitre Suite de benchmarks.
Note
À part une exécution datée sous Mesures de référence, ce chapitre ne cite aucun chiffre. Les nombres absolus dépendent de l’hôte, des réglages de la base de données et du pipeline configuré bien plus que de quoi que ce soit dans Pepsi ; exécutez donc la suite sur votre propre matériel. Ce qui est transposable, c’est la forme des résultats — quelles étapes sont peu coûteuses, lesquelles croissent avec la taille du message, et de quelle nature se révèle être le plafond de débit — et c’est ce que décrit ce chapitre.
Exécutez les benchmarks à un niveau de journalisation discret : les scripts règlent [pepsi] LOG (chaque composant l’honore — le dispatcher et les workers d’étape qu’il lance) à warn pour leur durée, afin que la journalisation par message ne gonfle pas les chiffres.
27.1. Comment le pipeline passe son temps¶
Cinq propriétés de l’architecture fixent les chiffres. Les trois dernières découlent d’un seul principe : à la cadence du courrier, le coût du pipeline est dominé par la fréquence à laquelle il fait coordonner la base de données plutôt que par le travail d’une étape ; il ne coordonne donc que là où l’information se trouve réellement.
Chaque étape paie un petit coût fixe de base de données par message. Un worker d’étape charge sa ligne en un seul
SELECT(la ligne et les paramètres par adresse qui s’y appliquent, en un seul aller-retour) et valide son issue en un seulUPDATE/DELETEterminal (ou une seule fonction stockée à fan-out). Cette paire d’allers-retours, plus l’ordonnancement du worker, est le plancher que chaque message paie à chaque saut, indépendamment de la taille du message.Plusieurs mécanismes suppriment ou amortissent ces allers-retours. Le dispatcher revendique le travail de toutes les étapes en un seul appel (par étape, la revendication équitable lit au plus 64 expéditeurs en attente par un parcours d’index lâche et, pour chacun, au plus autant de ses lignes les plus anciennes que la capacité de l’étape, de sorte qu’un arriéré profond ne la ralentit pas ; avec
FAIR_SCHEDULING = no, unLATERAL … LIMITarrête le parcours d’index de chaque étape à sa capacité) et pipeline jusqu’àQUEUE_LIMITmessages vers chaque worker, de sorte que terminer un message ne demande aucun aller-retour préalable avec le coordinateur. Le chemin chaud de chargement-et-avancement s’exécute enREAD COMMITTEDplutôt qu’enSERIALIZABLE— sûr, car au plus un écrivain touche jamais une ligne donnée (le dispatcher est l’unique revendicateur). Le worker d’une étape sans corps charge et valide un lot entier de messages pipelinés en unSELECT … = ANYet unUPDATEpar tableaux. Et la fusion d’étapes ([pepsi] ALLOW_FUSION, activée par défaut) exécute un successeur rapide sans corps dontFUSIONest activé — par défautif,srs,list,check-whitelist,auto-whitelist,block-language,discardetedit-settings(ce dernier pour les seuls messages qui ne sont pas des messages de contrôle, ce qu’il peut décider à partir des métadonnées) — à l’intérieur du processus worker de son prédécesseur sur la ligne déjà chargée, ramenant une suite de n telles étapes de n chargements + n avancées + n passages de main à un seul chargement, le travail des étapes, et une seule écriture terminale. La fusion ne change aucune issue : une étape qui ne peut pas être fusionnée (elle a besoin du corps, elle n’est pas pliée dans le binaire unifié, ou sonFUSIONest désactivé) retombe simplement sur un saut dispatché normal. La fusion exige le binaire unifiépepsi; elle est donc inerte dans une construction de développementmultibin.Les passages de main entre étapes sont délibérément silencieux. Le dispatcher apprend qu’il y a du travail par un
LISTENsur le canalworkqueue, mais une étape qui fait avancer un message ne notifie pas. Elle n’en a pas besoin : le worker rapporte chaque message terminé sur sa sortie standard, et le coordinateur répond à ce rapport par la même revendication complète, bornée par la capacité, qu’une notification aurait déclenchée. Une notification sur le chemin d’avancement apprendrait au dispatcher une chose qu’il est sur le point d’apprendre de toute façon, par un canal moins coûteux.Les notifications sont loin d’être gratuites : PostgreSQL détient un verrou
AccessExclusiveà l’échelle de la base depuis l’instant où une transaction en met une en file jusqu’à sa validation, de sorte que les transactions notifiantes ne peuvent pas être validées par groupes et que chaque saut sérialiserait le chemin de validation de la base entière. Une notification n’est donc émise que là où l’information est réellement nouvelle — par les écrivains hors de la boucle du dispatcher : l’admission des messages (coalescée, une fois par rafale plutôt qu’une fois par message),workqueue_inject,workqueue_resume, pepsi-keydisc(1) libérant du courrier garé, pepsi-failure-bouncer(1) et les commandes de réparation de pepsi-queue(1).POLL_INTERVALreste le filet de sécurité derrière tout cela : un réveil qui n’est jamais envoyé, ou qui est perdu, coûte de la latence et jamais un message.Les transitions d’étape n’attendent pas le disque. La connexion d’un worker d’étape s’exécute avec
synchronous_commitdésactivé ([pepsi-postgres] WORKER_SYNCHRONOUS_COMMIT, désactivé par défaut), de sorte que les écritures du pipeline sont validées par groupes au lieu que chacune paie son propre vidage. L”admission des messages n’est pas concernée — l’ingress valide toujours de façon synchrone, donc un250signifie toujours que le message a atteint le disque. L’exposition ainsi ajoutée est qu’un plantage machine peut perdre les dernières centaines de millisecondes de transitions d’étape, après quoi le message est retrouvé à l’étape précédente et la traverse de nouveau — ce qui est exactement ce qui se produit déjà quand un worker est tué entre l’exécution de son travail et sa validation.La comptabilité a exactement un écrivain. Les compteurs par étape et globaux sont accumulés dans la mémoire du dispatcher et écrits en une seule transaction — à chaque
STATS_INTERVAL, quand le pipeline devient inactif, et à l’arrêt. Aucun worker ne les écrit :stage_statscontient une ligne par étape, de sorte qu’un compteur écrit par message mettrait chaque worker d’une étape en file derrière la ligne unique de cette étape — la comptabilité entrant en contention là où le courrier lui-même ne le fait jamais, puisque exactement un écrivain touche un message donné. Une étape fusionnée dans la passe d’une autre étape est donc rapportée au dispatcher sur la ligne de statut que cette passe écrit déjà, et intégrée aux compteurs qu’il tient déjà, de sorte que la comptabilité par étape reste exacte sans aucune écriture. Le coût est celui, habituel, des statistiques : les deltas non encore vidés sont perdus si le dispatcher meurt.
27.2. Lire la matrice par étape¶
Le dispatcher chronomètre chaque exécution d’étape : 08-perstage-bench.sh mesure donc le coût d’une étape directement, sans instrumenter le code de l’étape : le temps d’horloge qu’un seul message passe à l’intérieur de chaque étape (le worker lit la ligne, exécute la logique de l’étape, valide le résultat), en millisecondes par message, pour chaque taille de message balayée. Pour que chaque étape soit observable comme son propre worker distribué, ce benchmark tourne avec [pepsi] ALLOW_FUSION = no ; les benchmarks de latence et de débit utile laissent la fusion active et mesurent le système tel qu’il est déployé.
La matrice a deux moitiés (voir le chapitre Suite de benchmarks pour les deux listes) : les étapes chronométrées in situ, en envoyant un message sur chaque chemin du pipeline déployé, et les programmes d’étape par lesquels le déploiement n’achemine aucun message, chronométrés en isolation en injectant un message directement dans une section réservée au benchmark. Les chiffres isolés ne comportent aucun coût de pepsi-ingress ni de SMTP.
Trois classes de lignes se dégagent, ainsi qu’un artefact de mesure qu’il vaut la peine de nommer.
Les étapes de métadonnées sont plates et peu coûteuses.
if(les branchements),srs,list,aliases,routeetdiscarddéclarentLoad::Metadata— elles ne touchent jamais le corps du message — et coûtent la même chose à toutes les tailles.check-whitelist,block-languageetvacationse comportent de même en pratique : elles déclarentLoad::Headers, tirent donc le bloc d’en-têtes mais jamais le corps, et le bloc d’en-têtes ne croît pas avec le message.Le chiffre isolé d’une telle étape — une étape, un worker chaud, rien d’autre dans le pipeline — est le coût du saut lui-même : l’unique
SELECTde chargement, l’uniqueUPDATEterminal et la comptabilité propre du worker. Son chiffre in situ est le même saut avec les workers du reste de la chaîne vivants autour de lui, et il est plus élevé. « Le plancher par saut » n’est donc pas un seul nombre : c’est le travail propre du saut plus le fait d’être l’un parmi beaucoup. C’est ce que la fusion d’étapes supprime, et c’est pourquoi la latence de bout en bout mesurée avec la fusion active est bien inférieure à la somme de la colonne de la matrice.Les étapes qui lisent le corps croissent avec la taille du message.
detect-languageest l’opération unitaire la plus coûteuse du pipeline et essentiellement linéaire en fonction du corps, suivie dedecryptet desecure-link; uniffondé sur la taille placé devantdetect-languageest le changement qui économise le plus sur les gros messages.dkim-sign,local— l’écrivain Maildir setuid traitant le message entier — etinit(vérification ARC, hachage et scellement) montent bien plus doucement.Les étapes qui ne font qu”*inspecter* se tiennent au plancher.
autocrypt-learn,vks-confirmetauto-paydéclarent toutesLoad::Full, mais pour du courrier ordinaire elles ne font que regarder : leur coût ne monte donc qu’avec le coût du chargement d’un corps plus grand. Les ajouter à un pipeline coûte environ un saut de plus, pas un balayage de plus.Les lignes de relais ne sont comparables à rien de tout cela.
smarthost,internet,delaytestetbench-lmtpmaintiennent une transaction SMTP ou LMTP sortante ouverte : leur chiffre est donc l’aller-retour d’un autre hôte et le traitement propre de cet hôte. Il n’a même pas besoin de croître de façon monotone avec la taille, et il ne dit rien de l’hôte Pepsi — voir Chiffres qui appartiennent à un autre hôte.L’artefact : les workers froids. Le balayage passe d’une taille à l’autre plus lentement que
[pepsi-dispatch] WORKER_IDLE_TIMEOUT(5 s par défaut), si bien que le worker d’une étape réservée au benchmark peut être recyclé entre les tailles et que la cellule suivante paie pour en faireexec’er un. On le reconnaît à un saut des mêmes quelques millisecondes quelle que soit la taille du message, alternant le long d’une ligne avec la valeur à chaud. La valeur à chaud est le plancher ; celle à froid est le plancher plus un démarrage de processus.
anti-spam apparaît dans la matrice levé : les benchmarks mettent l’expéditeur en liste blanche, de sorte qu’il court-circuite sur state.spam et ne regarde jamais le corps. Son coût lorsqu’il filtre est la latence du backend marchand Taler, qui est une propriété du marchand et non de Pepsi. C’est la liste blanche qui le garde hors du chemin courant.
27.3. Chiffres qui appartiennent à un autre hôte¶
Certains chiffres qu’affiche un run de benchmark sont des mesures de l’hôte Pepsi dialoguant avec une autre machine, et sont bornés par cette machine :
Le coût par étape des étapes de relais, comme ci-dessus : un aller-retour distant plus un traitement distant, et non ce qu’il en coûte à Pepsi de relayer un message.
Le débit utile sortant (scénario C du benchmark de débit utile). Le relais est limité par la vitesse à laquelle le MX destinataire accepte. Si cet hôte est lui-même un déploiement Pepsi, ses
[pepsi-ingress] CONN_RATE_PER_SECOND/CONN_RATE_BURSTvalent par défaut une nouvelle connexion par seconde et par source, et un relais qui ouvre une connexion par message reçoit421et diffère — rien n’est perdu, mais rien n’est mesuré non plus. Relevez-les d’abord du côté destinataire. Une fois relevés, ce qui reste est la vitesse propre de l’hôte destinataire ; un petit pair accepte une rafale rapidement puis passe longtemps à la remettre.Cellules ``n/a`` aux grandes tailles sur le chemin du smarthost. Le
message_size_limitpar défaut de Postfix est de 10 240 000 octets, et un smarthost Postfix refuse les messages plus gros avec552. Le balayage consigne le refus et saute les tailles supérieures sur ce chemin plutôt que de le reproduire.
27.3.1. Extrapoler au-delà d’un goulot d’étranglement distant¶
Le récapitulatif du débit utile affiche une colonne extrapolée précisément pour ce cas.
Avertissement
Une extrapolation est de l’arithmétique appliquée à des mesures, non une mesure, et c’est une borne supérieure : elle suppose que la seule ressource mise à l’échelle reste la contrainte, ce qui cesse précisément d’être vrai quand on s’en approche. Citez les chiffres mesurés.
La méthode est la plus évidente : si un run a utilisé une fraction u d’une ressource locale et que c’est autre chose qui limitait, alors supprimer cette autre chose laisserait le débit monter d’au plus 1/u. C’est la ressource choisie qui décide si la réponse a un sens :
Le ``%util`` du disque ne doit pas être utilisé. Le
%utild”iostatest la part du temps d’horloge où la file de requêtes n’était pas vide ; sur un périphérique NVMe qui sert des dizaines de requêtes en parallèle, cela dit « quelque chose était en cours », non « le périphérique est saturé », et il ne croît pas avec le débit d’une manière qui permettrait de s’en servir comme facteur d’échelle.Le réseau est rapporté parce qu’il serait le premier à devenir contraignant sur un lien plus chargé, mais il en est rarement proche.
Le CPU suit la charge, et c’est lui que la projection utilise.
Et la projection est plafonnée au meilleur débit qu’un chemin entièrement local (sans hôte distant) a atteint dans le même run : on ne peut pas relayer des messages plus vite qu’on ne peut les traiter. Ce que l’extrapolation autorise, c’est la forme de l’affirmation — le chemin sortant est limité par le pair, d’à peu près ce facteur — et non le chiffre. La manière honnête de combler l’écart est une seconde machine de la même classe à l’autre bout.
27.4. Latence de bout en bout (au repos)¶
Le système étant par ailleurs au repos, 09-latency-bench.sh injecte des messages courts un par un et sonde l’arrivée de chacun dans le Maildir de destination, chronométrant tout l’aller-retour injection→remise à travers le chemin entrant, et rapporte min/moyenne/médiane/p95/max. Lisez la médiane : un seul échantillon lent — un worker en cours de fork, un checkpoint — suffit à tirer la moyenne au-dessus du p95.
Le script affiche aussi la somme des temps de traitement moyens par étape sur le même run. Sur un système au repos, la latence de bout en bout est proche de cette somme : l’ordonnancement du dispatcher et le passage entre étapes ajoutent peu par saut, et le chemin d’avancement est piloté par le rapport d’état du worker plutôt que par une notification, de sorte que POLL_INTERVAL ne le conditionne pas.
Deux propriétés du banc d’essai gardent le chiffre honnête. L’échantillonneur lit chaque fichier du Maildir/new de destination une fois par taille à laquelle il est vu, plutôt que chaque fichier à chaque sondage, de sorte qu’une boîte laissée pleine par un run antérieur ne transforme pas la mesure en une mesure du propre glob du banc d’essai. Et il ouvre une connexion par échantillon, ce qui, à faible latence, est plus rapide que ce qu’autorise un CONN_RATE_PER_SECOND déployé : il relève donc les plafonds d’admission pour le run et rapporte un refus à la soumission séparément d’un message accepté qui n’est jamais arrivé.
La latence au repos est le travail par message qu’un message unique paie sur une file vide. Ce n’est pas l’inverse du débit à saturation ci-dessous — les deux sont limités par des choses différentes.
27.5. Goodput à saturation¶
10-goodput-bench.sh mesure chacun des trois chemins de charge et de remise deux fois, et les deux instruments ne se valent pas.
Une rampe multiplie la charge offerte tous les STEP_SECS jusqu’à ce que la file dépasse SAT_QUEUE et continue de croître. Un vidage par rafale de taille fixe offre BURST_MSGS messages à plein régime et chronomètre le vidage de la file. Tous deux relèvent pour la durée les plafonds d’admission ([pepsi-ingress] CONN_RATE_PER_SECOND/CONN_RATE_BURST/ MAX_CONNECTIONS et son DB_POOL_SIZE) et le PARALLELISM de chaque section [stage-*], et tous deux restaurent la configuration déployée en sortant. Tous deux échantillonnent le CPU de l’hôte, le %util du disque sous PGDATA et l’utilisation du lien réseau.
Citez la rafale. 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 de la fenêtre quand cet hôte est distant, et plus large encore quand il est petit —, de sorte qu’une rampe affiche bien moins que la rafale pour le même chemin. La rampe vaut la peine d’être lue pour la forme du coude — où la file commence à croître, ce que fait l’hôte à ce moment — mais pas pour un débit.
Les chemins, et ce qui limite chacun :
chemin |
ce qui le limite |
|---|---|
A injection locale → |
souvent son propre générateur de charge, qui tourne sur l’hôte testé ; utilisez-le pour confirmer que le réseau n’ajoute rien, non comme un chiffre à citer |
B expéditeur étranger → |
cet hôte. Le chiffre entrant à citer |
C origine locale → relais vers un MX étranger |
l’hôte destinataire (voir Chiffres qui appartiennent à un autre hôte) |
pipeline seul, vidage alimenté en SQL (voir Reproduire) |
cet hôte, l’admission SMTP entièrement retirée du chemin |
Lus ensemble :
A face à B isole le réseau. Les deux ne diffèrent que par l’emplacement de l’expéditeur ; des coûts par étape qui concordent à la dispersion près signifient que l’authentification entrante du courrier étranger n’est pas un coût par message qui mérite d’être vu.
Le vidage alimenté en SQL face à B isole l’admission. La différence, c’est
pepsi-ingressacceptant des connexions et validant chaque admission de façon synchrone — un250signifie que le message a atteint le disque — et c’est le prix de cette garantie, non un défaut.Vérifiez que rien à l’intérieur de la base de données n’entre en contention. Échantillonnez
pg_stat_activityetpg_lockspendant un vidage. Des backends enClient/ClientReadsont PostgreSQL attendant les workers, et non l’inverse ; un verrou non accordé ou des attentesIO/WALSyncdisent que la base de données est la limite. Les colonnesfail/setserr/sde la rampe doivent indiquer zéro à chaque niveau de charge : un message valide ne doit jamais échouer et la file à revendicateur unique ne doit jamais sérialiser, de sorte qu’une valeur non nulle est un défaut de correction plutôt qu’une limite de capacité.Au-delà du coude, le débit peut chuter au lieu de s’aplatir. Un Pepsi soumis à plus de charge qu’il ne peut en prendre ne perd rien, mais il ne se dégrade pas nécessairement gracieusement vers son débit maximal ; ce sont les plafonds d’admission qui empêchent une rafale de provoquer cela, et c’est vers eux qu’il faut se tourner.
Note
Une file saturée ne rapporte jamais que sa contrainte la plus externe ; un chiffre de débit à lui seul ne dit donc rien de ce qui le limite. Les contrôles qui tranchent sont pg_locks et pg_stat_activity sous charge, le CPU/disque/réseau de l’hôte lui-même et — comme le montre le chemin C — ce que fait le saut suivant. Citez un chiffre accompagné de ces éléments, et refaites les mêmes contrôles après un changement.
Un message coûte environ deux transactions de base de données par saut dispatché — le chargement et l’écriture terminale — plus l’admission, quantité dont il est vraiment question dans les propriétés de coordination ci-dessus, et celle à surveiller lorsqu’on ajoute une étape. La marge restante est là : moins d’étapes sur le chemin, et la fusion d’étapes, qui réduit une suite d’étapes fusionnables sans corps à un chargement, une écriture terminale et une ronde de comptabilité au lieu de n.
27.6. Notes d’ajustement¶
Parallélisme et budget de connexions. Chaque worker d’étape est un processus qui détient une connexion de base de données, de sorte que le nombre de connexions actives est approximativement
Σ PARALLELISMsur les étapes occupées, plus le pool du dispatcher et les pools depepsi-ingress/pepsi-httpd. Cette somme doit rester sous lemax_connectionsde PostgreSQL. LePARALLELISMpar défaut est 4 et leDB_POOL_SIZEpar défaut est 1 pourpepsi-ingressetpepsi-httpd(le pool propre au dispatcher vaut 8 par défaut, celui depepsi-telemetry2) précisément pour qu’une installation par défaut tienne sur un serveur par défaut. Un pipeline comportant de nombreuses sections d’étape peut malgré tout dépasser lemax_connections = 100par défaut de Debian si toutes les étapes sont occupées à la fois. ReleverPARALLELISMpour pousser le débit, ouDB_POOL_SIZEpour admettre plus de sessions concurrentes, oblige à revérifier ce budget. S’il est dépassé, les workers qui ne peuvent pas obtenir de connexion rapportent le statut d’engorgement de la base de données et le dispatcher remet le message en file et freine l’étape la plus occupée plutôt que de perdre du courrier — voir pepsi-dispatch (Contre-pression en cas d’engorgement de la base de données). Cela ne produit aucune erreur, seulement un débit plus faible, et c’est pourquoi10-goodput-bench.shaffiche la somme en regard demax_connectionsavant de mesurer quoi que ce soit. Le remède à une pression soutenue est de relevermax_connectionsou d’abaisserPARALLELISM, pas de compter sur le frein.Moins de sauts aide toujours ; plus de workers n’aide que lorsque c’est le nombre de workers qui fait obstacle. Un saut distribué coûte une paire d’allers-retours — le
SELECTde chargement du worker et sonUPDATEterminal — plus l’ordonnancement autour, de sorte que le plafond de débit utile évolue avec la longueur du chemin quoi qu’il en soit par ailleurs. (Ce n’est pas une écriture de statistiques : aucun worker n’écritstage_stats, voir La comptabilité a exactement un rédacteur ci-dessus.) Raccourcir le chemin, ou fusionner le saut, est donc le changement qui paie toujours.PARALLELISMest celui qui dépend de la machine. Sur un petit hôte, le coude est le CPU ou le journal d’écriture anticipée, et plus de workers signifie seulement plus d’attente. Sur un gros hôte, quelques workers par étape, chacun bloqué sur son propre aller-retour avec la base de données, peuvent laisser la machine presque inactive sans la moindre contention de verrous, et releverPARALLELISMest alors toute l’histoire. La règle est le diagnostic plutôt qu’un nombre : si l’hôte est inactif etpg_lockspropre au coude, le nombre de workers est le plafond et l’augmenter marchera ; si l’hôte est occupé, ou si les attentes sont desIO/WALSync, cela ne marchera pas. Chaque worker est une connexion à la base : jusqu’où cela peut aller est donc une propriété demax_connections. (Le%utildu disque ne tranche pas la question — voir Extrapoler au-delà d’un goulot d’étranglement distant.)Laissez la fusion d’étapes activée.
[pepsi] ALLOW_FUSION(activée par défaut) exécute un successeur fusionnable sans corps dans le worker de son prédécesseur sur la ligne déjà chargée, de sorte qu’une suite de n telles étapes coûte un chargement, une écriture terminale et une passe de comptabilité au lieu de n. La seule raison de la désactiver est de mesurer les étapes séparément, comme le fait le benchmark par étape. PositionnerFUSION = yessur une étape bon marché à métadonnées seules que le registre de fusion prend en charge vaut plus que n’importe quel réglage de pool.Profondeur de pipeline avant parallélisme. Pour la même raison, privilégiez le
QUEUE_LIMITd’une étape (messages pipelinés vers un worker) avant sonPARALLELISM.QUEUE_LIMITporte la capacité en vol d’une étape àQUEUE_LIMIT × PARALLELISMet supprime un aller-retour de coordinateur par message sans ajouter de processus workers ni de connexions de base de données, de sorte qu’il n’entre pas dans le budget ci-dessus ; et le worker d’une étape sans corps valide un lot pipeliné entier en un seulUPDATEpar tableaux, ce qui fait un aller-retour pour tout le lot. Il vaut 4 par défaut — le pipelining est activé d’emblée — de sorte que le réglage consiste souvent à l”abaisser : mettezQUEUE_LIMIT = 1pour les étapes dont le travail par message est long et inégal, principalement les relais réseau, où un message pipeliné attendrait sinon derrière un frère lent.Supprimez le travail dont vous n’avez pas besoin. L’étape la moins coûteuse est celle qui n’est pas dans la chaîne — la moins coûteuse doublement, puisqu’elle ne coûte ni son propre travail ni sa part de coordination pour un saut.
detect-languageen particulier mérite d’être omise, ou gardée par une branche de taille, sauf si le filtrage de langue est réellement utilisé.Laissez ``WORKER_SYNCHRONOUS_COMMIT`` désactivé sauf si une étape de votre pipeline a un effet de bord externe qui ne doit pas être répété, même au travers d’une coupure de courant. L’activer coûte environ un vidage du journal d’écriture anticipée par saut d’étape. Ce que cela vaut dépend entièrement du disque — considérable sur un SSD SATA, faible sur un périphérique NVMe rapide — ; mesurez-le donc chez vous avant d’y renoncer.
Les plafonds frontaux sont distincts du débit du pipeline, et ils s’appliquent d’abord.
[pepsi-ingress] CONN_RATE_PER_SECOND/CONN_RATE_BURST/MAX_CONNECTIONSconstituent une politique d’admission par source, et les valeurs par défaut sont délibérément strictes —CONN_RATE_PER_SECONDetCONN_RATE_BURSTvalent tous deux 1 par défaut, une nouvelle connexion par seconde depuis une adresse donnée. C’est le bon défaut pour un hôte exposé à l’internet ouvert et complètement inadapté à un hôte par lequel une autre de vos propres machines relaie, et c’est la première chose à vérifier lorsqu’un voisin à vous semble lent à accepter du courrier. Relevez-les du côté récepteur, par source si vous le pouvez. Les benchmarks les relèvent sur l’hôte testé pour la durée du run précisément pour mesurer le pipeline et non ceci.PostgreSQL lui-même. Les benchmarks n’exigent aucun réglage de la base de données au-delà de
max_connections, et la suite laisse délibérément le reste aux valeurs par défaut de la distribution ; un chiffre mesuré ainsi est un plancher qu’une base réglée (shared_buffersen particulier) relèverait, et non un plafond.
27.7. Mesures de référence¶
Une exécution des trois bancs d’essai, réalisée le 2026-09-26 sur le commit 0c6a40ea avec le pipeline que déploie tests/01-deploy.sh et chaque réglage à la valeur par défaut qu’indique le chapitre Suite de bancs d’essai. Ces chiffres ne décrivent que cet hôte et ce commit ; lisez-les pour la forme exposée dans les sections ci-dessus, non comme une valeur à attendre ailleurs.
élément |
valeur |
|---|---|
hôte testé |
l’hôte |
CPU |
2 x AMD EPYC 9555, 128 cœurs / 256 threads |
mémoire |
256 Gio |
stockage |
|
système d’exploitation |
Debian GNU/Linux 13 (trixie), noyau 6.12 |
base de données |
PostgreSQL 17.11 sur le même hôte, valeurs par défaut de la distribution ( |
fixé par la suite pour l’exécution |
|
hôte de |
une machine virtuelle à 2 vCPU et 4 Gio exécutant Postfix ; le smarthost du chemin de relais entrant et l’expéditeur du chemin C |
hôte de |
un Core i3-4005U à 4 threads avec 16 Gio exécutant lui-même Pepsi ; l’expéditeur étranger des scénarios entrants et du chemin B, et le MX destinataire de l’étape |
Coût par étape (08-perstage-bench.sh), en millisecondes par message, moyenne de trois messages par cellule. Les lignes bench-* sont la moitié isolée, toutes les autres ont été chronométrées in situ :
étape
1K
10K
100K
1M
5M
10M
20M
init17,9
17.6
18,0
19.4
27,0
35,4
63,6
route6.8
6.5
7.0
6.5
7.1
6.6
11.3
decrypt7.1
7.1
8,4
21,8
76,2
144
286
detect-language11,9
16.0
23,9
57,0
208
385
741
block-language4.9
5.0
5.0
5.1
5.4
5.3
9,4
check-whitelist5.6
5.4
5.9
4,7
4,7
5.3
8,5
anti-spam4.8
5.0
4.8
6.0
7.0
9.7
19,0
list6.1
6.2
6.1
6.5
6.4
5,7
10,3
forward4,3
6.0
6.1
7,6
26,2
11.3
20,2
local4.6
10.9
11.6
13.4
21,1
27.5
48,9
srs5.8
6.3
5.8
6.3
14,5
n/a
n/a
dkim-sign-relay13,0
13.9
13,3
16,6
49,3
n/a
n/a
smarthost294
267
258
217
347
n/a
n/a
list-out11,9
6.6
5.8
6.9
10.7
15.1
14.3
edit-settings9.7
4.8
5.6
5,7
12,8
18,8
25,1
auto-whitelist10,5
7.0
6.4
6.8
10,3
15.6
15.2
delay-route10.7
6.1
5.4
5.5
9,2
13.4
15.1
encrypt10.1
6.9
6.4
11,1
30,8
54,4
91,4
dkim-sign14,0
13,8
13,0
17.1
40,1
66,9
98,5
internet245
112
132
256
628
1145
2121
bounce5.3
5.1
5.0
5.3
5.1
5.2
5.5
bounce-language5.1
4.8
4.6
5.5
5.2
5.0
5.0
bench-discard6.5
1.8
1.7
1.7
1.9
6.5
6.6
bench-aliases7.2
2.0
1.9
1.8
1.9
6.2
6.9
bench-route6.8
2.0
1.8
1.8
1.7
6.3
6.9
bench-vacation6.3
1.4
1.6
1.2
1.1
5.9
6.2
bench-autocrypt-learn5.5
0.7
0.9
1.8
4.5
11,2
17.5
bench-vks-confirm5.0
0.8
0.7
1.4
3,8
11,0
16.2
bench-auto-pay5.2
0.8
0.8
1.5
3,6
10.8
16.0
bench-secure-link24.9
19,3
20,3
27.5
55,6
95,7
175
n/a est le 552 du smarthost pour un message de 10 Mo, voir Chiffres qui appartiennent à un autre hôte. Les lignes smarthost et internet sont l’aller-retour vers l’hôte ALICE/BOB et l’hôte CAROL respectivement et le traitement sur ces hôtes, non un coût de cet hôte. Les lignes isolées montrent l’artefact des workers froids à 1K, 10M et 20M : leur valeur à chaud, environ 2 ms pour une étape ne lisant que les métadonnées, est le plancher.
Latence au repos (09-latency-bench.sh), 200 messages de 1 Kio du port SMTP local vers un Maildir local, interrogé toutes les 2 ms :
distribués |
min |
médiane |
moyenne |
p95 |
max |
somme des temps moyens par étape |
|---|---|---|---|---|---|---|
200 sur 200 |
26 ms |
28 ms |
32 ms |
29 ms |
448 ms |
15,8 ms (10 étapes) |
Goodput à saturation (10-goodput-bench.sh), messages de 2 Kio. La rafale est le chiffre ; la colonne de rampe est le dernier palier qui a suivi, et les taux d’utilisation sont ceux de cet hôte pendant la rafale :
chemin |
rafale (msg/s) |
rampe (msg/s) |
CPU |
disque |
lien |
|---|---|---|---|---|---|
A injection locale -> |
1162 |
256 |
8 % |
63 % |
0,0 % |
B hôte |
1210 |
334 |
7 % |
65 % |
2,8 % |
C hôte |
88 |
109 |
1 % |
11 % |
1,8 % |
Aucune exécution n’a enregistré de message en échec, d’erreur de sérialisation ni de refus SMTP. Le chemin B est le chiffre entrant à citer. Il a tourné avec le CPU de l’hôte occupé à 7 %, la situation que décrivent les Notes d’ajustement sous PARALLELISM ; cette exécution n’a pas établi quelle limite était contraignante. Le chemin C est la vitesse de l’hôte CAROL : quand la file s’est vidée ici, il avait accepté les 2 000 messages et en avait distribué 296.
27.8. Reproduire¶
Les benchmarks ne font pas partie de make integrationtests (ils sont lents et chargent délibérément le MTA). Exécutez-les contre un pipeline déployé avec
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
Chaque script abaisse les STATS_INTERVAL/POLL_INTERVAL du dispatcher pour la durée de l’exécution afin que les compteurs soient vidés rapidement, et restaure la configuration déployée à la sortie (y compris les plafonds d’admission que l’exécution de goodput relève). Voir le chapitre Suite de benchmarks pour la méthodologie complète et les paramètres ajustables disponibles (tailles de message, nombres d’échantillons, paramètres de montée, redéfinitions d’admission).
Le vidage alimenté par SQL n’est pas l’un des scripts, car il contourne délibérément la partie de Pepsi par laquelle les scripts passent. Avec le même rythme et le même PARALLELISM que fixe le run de débit utile, insérez l’arriéré directement et chronométrez le vidage de la file
INSERT INTO pepsi.workqueue
(token, mail_from, rcpt_to, from_header, subject, headers, body,
stage, status, state)
SELECT 'bulk-' || g, 'sender@example.org', ARRAY['dave@<your-host>'],
'sender@example.org', 'bulk ' || g,
convert_to('From: sender@example.org' || E'\r\n' || '…' || E'\r\n', 'UTF8'),
convert_to(repeat('filler' || E'\r\n', 300), 'UTF8'),
'init', 'pending', '{"local_origin": false, "spam": false}'::jsonb
FROM generate_series(1, 50000) g;
Une seule transaction, de sorte que le dispatcher voit tout l’arriéré d’un coup ; puis sondez SELECT count(*) FROM pepsi.workqueue jusqu’à ce qu’il atteigne zéro. Rien ne notifie le dispatcher : cela exerce donc aussi le battement de cœur POLL_INTERVAL.