Un bidder Go qui perd des enchères sur timeout alors que sa moyenne paraît saine a deux problèmes derrière un seul symptôme : une échéance qui appartient à l'ad exchange et couvre l'aller-retour, et un collecteur qui impute du mark assist à la goroutine qui score le bid. Voici comment l'échéance se décompose, quels leviers Go rapportent, et ce qu'un hot path en Rust ne répare pas.
Un bidder qui perd des enchères sur timeout alors que sa moyenne paraît saine est décrit par le mauvais chiffre. L'échéance appartient à l'ad exchange et couvre le réseau dans les deux sens. Un collecteur est une propriété du processus : sa pause est donc imputée à toutes les connexions à la fois, et un pic sur une seule connexion demande une autre explication.
Réécrire en Rust ou régler le runtime, c'est un choix entre deux réponses à une question que personne n'a posée : quelle part de l'échéance est dépensée, et à quoi. Ce qui suit sépare le budget du collecteur, le collecteur du scheduler, et la réécriture du composant qui la mérite.
La réponse courte est structurelle. L'ad exchange fixe l'échéance et elle inclut le réseau : la première réparation décompose donc le p99 d'une connexion en temps de handler, temps d'attente d'un processeur et temps sur le réseau. Ce qu'amBrain peut étayer publiquement : nous avons construit RTBBidder, une demand-side platform livrée de zéro, où chaque décision de bid évalue des dizaines de conditions de ciblage par impression. Les chiffres de latence que nous publions comme mesurés viennent de chemins de trading, pas d'un bidder publicitaire, et aucun chiffre ci-dessous n'est mesuré sur un bidder à nous.
La spécification OpenRTB définit tmax comme le temps maximum en millisecondes que l'ad exchange accorde pour recevoir les bids, latence Internet incluse, et précise que cette valeur prime sur toute recommandation antérieure. Le budget est un aller-retour qui arrive à l'intérieur de chaque requête.
Le seuil sur lequel vous êtes jugé n'est pas celui de votre dashboard. La documentation Authorized Buyers de Google exige que 85 percent des réponses arrivent dans l'échéance, telle qu'elle est mesurée au trading location, et bride les bidders qui n'y parviennent pas. Entre son horloge et la vôtre se trouve tout ce qui n'est pas du calcul :
L'échéance interne se place donc sous le tmax, de ce que votre propre histogramme dit qu'une réponse coûte sur cette connexion, re-dérivée par ad exchange plutôt que fixée une fois pour toute la flotte. Une échéance n'est pas un plan de capacité : le travail annulé à l'échéance a déjà dépensé son CPU.
En surcharge, cela revient à payer le prix fort pour des réponses que personne ne compte : la réparation manquante est le contrôle d'admission. Lisez tmax, comparez-le au délai de file que vous mesurez, et répondez no-bid quand l'arithmétique ne tombe pas juste. Un no-bid rapide compte dans les 85 percent ; un bid tardif, non.
Un garbage collector est une propriété du processus : un cycle déclenché sur n'importe quelle connexion est donc facturé à toutes les connexions. Celle qui rate en premier est celle qui a le tmax le plus serré et la requête la plus lourde. Écartez ce qui produit la même image sans collecteur :
Aucun des quatre ne se répare par un réglage du collecteur, et la décomposition dispose d'instruments. Un profil CPU sépare les factures du collecteur par symbole, runtime.gcAssistAlloc pour les charges du handler et runtime.gcBgMarkWorker pour le marquage en arrière-plan. Le temps stop-the-world est /sched/pauses/total/gc:seconds, l'attente en état runnable est /sched/latencies:seconds, et la file d'acceptation se lit hors du processus via ListenOverflows.
Une seule distinction décide de la réparation Go dont vous avez besoin. Une pause stop-the-world est imputée à toutes les goroutines à la fois : elle apparaît donc comme un pic uniforme sur toutes les connexions. Un mark assist est imputé à la goroutine qui a alloué : il tombe donc sur les requêtes qui ont le plus alloué. Deux choses font baisser un assist : moins d'octets par bid request, ou un cycle plus long dans lequel les workers d'arrière-plan couvrent davantage de marquage. Seule la première survit à un changement de mix de trafic.
Le collecteur Go est concurrent, et le guide officiel est explicite : la durée des pauses ne croît pas avec la taille du heap, donc les transitions stop-the-world sont brèves. Ce sont les assists qui comptent : les goroutines assistent le collecteur quand l'allocation est rapide, parce que le marquage en arrière-plan reçoit un quart fixe des processeurs et que le manque est imputé à celui qui alloue.
Le débit fait de cela un seuil plutôt qu'une pente. Le taux d'allocation, c'est le QPS multiplié par les octets par bid request, face à une part d'arrière-plan fixe : du code qui n'assiste jamais à un cinquième de votre trafic peut donc assister sur presque chaque requête à 100K QPS.
Le coût du marquage est proportionnel au graphe de pointeurs vivants, pas aux déchets, et un bidder détient exactement la mauvaise forme pour cela : index de campagnes, segments d'audience, caches de fréquence. Discord a publié le même constat en 2020, avec un collecteur qui parcourait tout un cache LRU pour décider si la mémoire était libre. Cinq mécanismes se cachent derrière une seule expression :
Lisez cela comme cinq factures distinctes. Exactement une se règle par un réglage du collecteur, et aucune en changeant de langage avant d'avoir mesuré la décomposition.
Gil Tene a nommé cette défaillance coordinated omission : le système de mesure se coordonne avec le système testé d'une manière qui évite de mesurer les valeurs extrêmes, parce qu'une boucle fermée attend une réponse et cesse d'émettre pendant un blocage. ScyllaDB a publié en 2021 une comparaison où une charge de travail rapportait un p99 de 249 microseconds en boucle fermée et de 665 ms sous charge ouverte avec correction, soit un facteur d'écart d'environ 2,700.
Un générateur de charge qui attend une réponse cesse d'émettre pendant précisément le blocage qu'il devait trouver, puis fait entrer ce silence dans la moyenne du résultat. Le percentile qu'il affiche ensuite décrit le générateur, pas votre bidder.
Nommez le chiffre d'arrêt avant de régler quoi que ce soit, car une réécriture décidée par épuisement n'est pas une décision. Deux chiffres, pas un : les allocations de heap par requête, qui pilotent l'assist, et le heap vivant, qui pilote le marquage.
L'ordre est la source avant le plafond, pas le plus gros gain d'abord. Commencez là où l'assist est généré : escape analysis sur le chemin de bid, des buffers réutilisés au lieu d'être alloués, et un codec qui lit les champs dont vous avez besoin au lieu de matérialiser un nouveau graphe d'objets. Gardez ses slices à durée de vie courte : une slice pointant dans le buffer de requête retient tout ce buffer pendant toute la vie du bid.
L'approche a un plafond : Uber a rapporté en 2021 que le réglage de GOGC face à la limite mémoire du conteneur avait récupéré environ 70,000 cœurs sur l'ensemble de ses services critiques. C'est un résultat de coût, pas un percentile.
RTB House a décrit en juin 2025 un service de bidding en JVM où un découpage en microservices produisait un volume élevé de petites requêtes : la latence ajoutée devait rester sous 7 ms face à une requête moyenne d'environ 2.5 ms, et les percentiles 98 et 99 cassaient sous des pauses G1 fréquentes. Ils sont passés à generational ZGC et l'ont payé en mémoire.
Notez ce qu'a exigé cette réparation : un second collecteur vers lequel basculer. Go n'en livre qu'un et il n'est pas pluggable, donc les leviers Go sont le taux d'allocation, la forme du live set, et GOGC face à GOMEMLIMIT. Ce qu'un hot path en Rust supprime est en revanche précis : pas d'assist, pas de marquage en arrière-plan, pas de cycle forcé. Ce qui ne disparaît pas est plus long que ce que la plupart des équipes imaginent :
Si la queue de latence se loge dans le parsing de la requête, dans la connexion à l'ad exchange ou dans la mise en file du scheduler, Rust ne rend aucune de ces millisecondes, et réécrire le mauvais composant dépense un trimestre pour conserver le même taux de timeout.
Déplacez donc le plus petit morceau qui détient les allocations, pas le service : la boucle d'évaluation d'impression et ses index, la sélection des candidats, le ciblage, les lookups de fréquence et de budget, le scoring. Chiffrez la frontière au franchissement : une fois par bid request avec un buffer plat, jamais une fois par règle de ciblage. Validez sur des instances séparées alimentées par un flux miroir, jamais à l'intérieur du processus testé, où un chemin shadow double les deux quantités que vous mesurez.
Un test d'acceptation qui vaut pour les deux chemins : exécutez une courte fenêtre avec le collecteur désactivé, sous un plafond mémoire que vous contrôlez, et enregistrez le p99.9 à l'intérieur du handler. Faites-le sur une seule instance derrière une fraction du trafic, avec revert automatique, car avec GOGC désactivé un pic de heap contre le plafond met le runtime en cycles à la chaîne, et le guide indique que ce blocage peut être indéfini. Ne lisez le résultat que si le limiteur de CPU GC ne s'est jamais enclenché : dès qu'il s'enclenche, le percentile décrit le limiteur.
La seconde moitié de la question, qui fait ce genre de travail, a un test qui ne demande aucune liste de fournisseurs. Un prestataire qui répare une latence pilotée par le GC se comporte autrement qu'un prestataire qui vend une réécriture :
Aucun d'eux ne demande de faire confiance : chacun est un document que vous pouvez réclamer dès la première conversation, et celui qui revient vague dit que le diagnostic est en train d'être sauté.
La première question n'est donc pas quel langage. C'est lequel des trois postes - handler, scheduler ou réseau - détient les millisecondes manquantes sur la connexion qui expire, et si les octets alloués par bid request bougent quand vous vous y attaquez.
Ce qu'amBrain peut étayer publiquement : amBrain est une société de développement logiciel spécialisée dans les plateformes de trading, les matching engines, les systèmes de real-time bidding et l'ingénierie de plateformes de casino ; nous construisons des logiciels depuis 2019, et en AdTech ce que nous construisons, c'est du développement de DSP, des plateformes d'enchères en temps réel et de l'ingénierie d'ad exchange. Nous travaillons en trois formats : livraison complète, équipe dédiée ou ingénieurs intégrés à votre équipe. Si vous pesez une réécriture contre une passe de réglage, la conversation qui vaut la peine fait la décomposition avant de choisir un langage.
Apportez votre architecture actuelle et le mode de défaillance qui vous inquiète : nous les passerons en revue ensemble en une demi-heure.