AdTechSep 10, 202610 Min. Lesezeit

Go-GC-Pausen in einem RTB-Bidder: Mark Assist, Deadlines und die Rust-Entscheidung

Real-Time BiddingRustGarbage CollectionTail-Latenz
Bild konnte nicht geladen werden

Ein Go-Bidder, der Auktionen im Timeout verliert, während sein Durchschnitt gesund aussieht, hat zwei Probleme unter einem Symptom: eine Deadline, die der Exchange gehört und den Round Trip umfasst, und einen Collector, der mark assist der Goroutine berechnet, die das Gebot bewertet. Hier steht, wie die Deadline aufgeteilt wird, welche Go-Stellschrauben sich auszahlen und was ein Rust-Hot-Path nicht behebt.

Ein Bidder, der Auktionen im Timeout verliert, während sein Durchschnitt gesund aussieht, wird von der falschen Zahl beschrieben. Die Deadline gehört der Exchange und deckt das Netzwerk in beide Richtungen ab. Ein Collector ist eine Eigenschaft des Prozesses, seine Pause wird also jeder Verbindung zugleich berechnet, und eine Spitze auf nur einer Verbindung braucht eine andere Erklärung.

In Rust neu schreiben oder die Runtime tunen ist eine Wahl zwischen zwei Antworten auf eine Frage, die niemand gestellt hat: Welcher Teil der Deadline wird ausgegeben, und wofür. Was folgt, trennt das Budget vom Collector, den Collector vom Scheduler und den Rewrite von der Komponente, die ihn verdient.

Die kurze Antwort ist strukturell. Die Exchange setzt die Deadline, und sie schließt das Netzwerk ein, die erste Reparatur teilt also das p99 einer Verbindung in Handler-Zeit, Wartezeit auf einen Prozessor und Zeit auf der Leitung. Was amBrain öffentlich belegen kann: Wir haben RTBBidder gebaut, eine von Grund auf gelieferte Demand-Side-Platform, in der jede Gebotsentscheidung Dutzende Targeting-Bedingungen pro Impression auswertet. Die Latenzwerte, die wir als gemessen veröffentlichen, stammen aus Trading-Pfaden, nicht aus einem Ad-Bidder, und keine Zahl unten ist an einem Bidder von uns gemessen.

Die Deadline gehört der Exchange, und sie umfasst den Round Trip

Die OpenRTB-Spezifikation definiert tmax als die maximale Zeit in Millisekunden, die die Exchange für den Eingang von Geboten erlaubt, inklusive Internet-Latenz, und sagt, dass der Wert jede frühere Vorgabe außer Kraft setzt. Das Budget ist ein Round Trip, der in jeder Request mitkommt.

Der Schwellenwert, an dem Sie gemessen werden, ist nicht der auf Ihrem Dashboard. Die Authorized-Buyers-Dokumentation von Google verlangt, dass 85 Prozent der Antworten innerhalb der Deadline eintreffen, gemessen an der Trading Location, und drosselt Bidder, die das verfehlen. Zwischen ihrer Uhr und Ihrer sitzt alles, was keine Berechnung ist:

  • Der Round Trip zur Exchange und zurück, der Geografie und Peering ist, bevor er Engineering ist
  • Verbindungsaufbau, wenn keepalive ausläuft, denn ein frischer TLS-Handshake innerhalb eines Auktionsbudgets ist eine verlorene Auktion
  • Zeit in der Accept-Queue, bevor Ihr Handler die Request sieht - sie wächst genau dann, wenn Sie am meisten zu tun haben
  • Deserialisieren, zu Kosten, die davon bestimmt werden, wie viel Sie von der Request in Objekte verwandeln, nicht von ihrer Größe

Die interne Deadline liegt also um den Betrag unter tmax, den Ihr eigenes Histogramm für eine Antwort auf dieser Verbindung ausweist, je Exchange neu abgeleitet und nicht einmal für die ganze Flotte gesetzt. Eine Deadline ist kein Kapazitätsplan: Arbeit, die an der Deadline abgebrochen wird, hat ihre CPU bereits verbraucht.

Unter Überlast heißt das, den vollen Preis für Antworten zu zahlen, die niemand zählt, die fehlende Reparatur ist also Admission Control: tmax lesen, gegen die gemessene Queue-Verzögerung halten und mit no-bid antworten, wenn die Rechnung nicht aufgeht. Ein schnelles no-bid zählt auf die 85 Prozent ein, ein spätes Gebot nicht.

Ein Collector bedient jede Verbindung, eine einzelne Verbindung ist also ein anderer Fehler

Ein Garbage Collector ist eine Eigenschaft des Prozesses, ein Zyklus, der auf irgendeiner Verbindung ausgelöst wird, belastet also jede Verbindung. Zuerst verfehlt die Verbindung mit dem knappsten tmax und der schwersten Request. Schließen Sie aus, was dasselbe Bild ohne Collector erzeugt:

  • Zu wenige Verbindungen von einer Exchange, denn HTTP/1.1 trägt eine Request zur Zeit: QPS pro Verbindung mal Handler-Zeit nahe eins legt die Queue auf die Verbindung
  • Eine einzelne HTTP/2-Verbindung, bei der ein verlorenes Paket jeden Stream blockiert, der sie teilt - das Head-of-Line-Blocking, das RFC 9114 2022 als Grund für die Existenz von HTTP/3 nennt
  • Auslaufendes keepalive, öfter ein Default als ein Fehler: http.Server.IdleTimeout fällt auf ReadTimeout zurück, wenn es null ist, während Google ein Idle-Timeout von 2.5 Minuten verlangt und nginx nach 75 Sekunden schließt
  • Ein synchroner Feature-Lookup, den nur manche Exchanges auslösen, bei dem der Tail einem entfernten Store gehört, nicht Ihnen

Keines der vier wird durch eine Collector-Einstellung repariert, und für die Aufteilung gibt es Instrumente. Ein CPU-Profil trennt die Collector-Rechnungen nach Symbol: runtime.gcAssistAlloc für die Belastung des Handlers und runtime.gcBgMarkWorker für das Hintergrund-Marking. Stop-the-world-Zeit ist /sched/pauses/total/gc:seconds, die Runnable-Wartezeit ist /sched/latencies:seconds, und die Accept-Queue wird außerhalb des Prozesses über ListenOverflows gelesen.

Eine Unterscheidung entscheidet, welche Go-Reparatur Sie brauchen. Eine stop-the-world-Pause wird jeder Goroutine gleichzeitig berechnet, sie erscheint also als flache Spitze auf allen Verbindungen. Ein mark assist wird der Goroutine berechnet, die allokiert hat, er landet also auf den Requests, die am meisten allokiert haben. Zwei Dinge senken einen Assist: weniger Bytes pro Bid Request oder ein längerer Zyklus, in dem Hintergrund-Worker mehr vom Marking übernehmen. Nur das Erste übersteht eine Änderung im Traffic-Mix.

Mark assist ist die Rechnung, die Ihre Bid Request bezahlt

Der Go-Collector ist nebenläufig, und der offizielle Guide sagt ausdrücklich, dass die Pausenlänge nicht mit der Heap-Größe skaliert, stop-the-world-Übergänge sind also kurz. Die Quelle, auf die es ankommt, sind die Assists: Goroutinen assistieren dem Collector, wenn schnell allokiert wird, denn das Hintergrund-Marking bekommt ein festes Viertel der Prozessoren, und der Fehlbetrag wird dem Allokierenden berechnet.

Die Rate macht daraus eine Schwelle statt einer Steigung. Die Allokationsrate ist QPS mal Bytes pro Bid Request gegen einen festen Hintergrundanteil, also kann Code, der bei einem Fünftel Ihres Traffics nie assistiert, bei 100K QPS auf fast jeder Request assistieren.

Die Marking-Kosten sind proportional zum lebenden Zeigergraphen, nicht zum Müll, und ein Bidder hält genau die falsche Form dafür: Kampagnen-Indizes, Audience-Segmente, Frequency-Caches. Discord veröffentlichte 2020 denselben Befund, mit einem Collector, der einen ganzen LRU-Cache scannte, um zu entscheiden, ob der Speicher frei war. Fünf Mechanismen verstecken sich hinter einem Begriff:

  • Stop-the-world-Übergänge: eine flache Spitze auf jeder Verbindung im selben Moment, selten lang genug, um für sich allein eine Auktion zu verlieren
  • Mark assist: gar keine Pause im Trace, nur Handler-Zeit, die gewachsen ist - abzulesen am Assist-Anteil der GC-CPU
  • Marking-Kosten: GC-CPU, die steigt, wenn der lebende Heap wächst, obwohl die Allokation es nicht tat - daran ändern flache Arrays etwas, weniger Müll nicht
  • Scheduler-Contention: eine Request, die runnable dasteht, ohne ausgeführt zu werden - die Scheduler-Latenz-Metrik zeigt das, die Pausen-Metrik nicht
  • Der erzwungene Zyklus: eine Spitze in einem Rhythmus von etwa zwei Minuten auf einer ruhigen Instanz - sie zeigt auf den Mindesttakt der Collection, nicht auf Ihren Traffic

Lesen Sie das als fünf getrennte Rechnungen. Genau eine wird durch eine Collector-Einstellung beglichen und keine durch einen Sprachwechsel, bevor die Aufteilung gemessen ist.

Ein Closed Loop löscht die Belege, die Sie gebraucht haben

Gil Tene hat diesen Fehler coordinated omission genannt: Das messende System stimmt sich so mit dem gemessenen System ab, dass Ausreißer gar nicht erst gemessen werden, denn ein Closed Loop wartet auf eine Antwort und hört während eines Stillstands auf zu senden. ScyllaDB veröffentlichte 2021 einen Vergleich, in dem ein Workload closed-loop ein p99 von 249 Mikrosekunden meldete und unter offener Last mit Korrektur 665 ms - ein Unterschied um etwa das 2,700-Fache.

  • Open-Loop-Last mit fester Rate, wobei die Latenz ab dem vorgesehenen Sendezeitpunkt gezählt wird und nicht ab dem Moment, in dem die Request rausging
  • Korrektur nach Art von HdrHistogram, wann immer der Generator die Requests nicht einreiht, die er nicht senden konnte
  • p99 und p99.9 statt eines Durchschnitts, mit eingerechnetem eigenem Fan-out: Dean und Barroso zeigten 2013, dass der Zugriff auf 100 Server bei einem p99 von einer Sekunde 63 Prozent der Requests langsam macht
  • Ein Traffic-Profil, kopiert von der Exchange, die weh tut, länger laufen gelassen als das erzwungene Collection-Intervall und unter derselben cgroup-Quota

Ein Lastgenerator, der auf eine Antwort wartet, hört genau während des Stillstands auf zu senden, den er finden sollte, und mittelt die Stille anschließend ins Ergebnis ein. Das Perzentil, das er danach ausgibt, beschreibt den Generator, nicht Ihren Bidder.

Erst weniger allokieren, dann drei Stellschrauben drehen

Benennen Sie die Abbruchzahl, bevor Sie tunen, denn ein Rewrite aus Erschöpfung ist keine Entscheidung. Zwei Kennzahlen, nicht eine: Heap-Allokationen pro Request, die den Assist treiben, und der lebende Heap, der das Marking treibt.

Die Reihenfolge ist Quelle vor Obergrenze, nicht größter Gewinn zuerst. Beginnen Sie dort, wo der Assist entsteht: Escape-Analyse auf dem Bid-Pfad, Puffer wiederverwenden statt allokieren, und ein Codec, der die Felder liest, die Sie brauchen, statt einen frischen Objektgraphen zu materialisieren. Halten Sie dessen Slices kurzlebig: Ein Slice in den Request-Puffer hält diesen ganzen Puffer für die Lebensdauer des Gebots fest.

  • sync.Pool nimmt Druck weg und verspricht nichts: Ein Item kann jederzeit ohne Benachrichtigung entfernt werden, und ein gepooltes Objekt wird zwischen seiner Rückgabe und seiner Eviction weiterhin markiert
  • Das lebende Set ist die Rechnung: Bauen Sie Kampagnen- und Segment-Indizes abseits des Hot Path in flache Arrays um, damit der markierte Graph nicht mehr mit der Zahl der Kampagnen wächst
  • GOGC tauscht Speicher gegen Collector-CPU zu einer Rate, die der Guide klar benennt: Verdoppeln halbiert die GC-CPU-Kosten ungefähr, und der Assist-Anteil sinkt mit
  • GOMEMLIMIT ist genau dieser Tausch gegen eine Obergrenze, absichtlich weich, denn ein hartes Limit macht aus einer Heap-Spitze einen unbegrenzten Stillstand
  • GOMAXPROCS im Container: Go 1.25 liest das cgroup-CPU-Limit und ausdrücklich nicht die CPU-Requests, ein Pod mit Requests und ohne Limit behält also das alte Verhalten

Der Ansatz hat eine Obergrenze: Uber berichtete 2021, dass das Tunen von GOGC gegen das Container-Speicherlimit rund 70,000 Cores über seine geschäftskritischen Services hinweg zurückgewonnen hat. Das ist ein Kostenergebnis, kein Perzentil.

Was ein Rust-Hot-Path Ihnen bringt und was Sie dafür zahlen

RTB House beschrieb im Juni 2025 einen JVM-Bidding-Service, bei dem eine Aufteilung in Microservices ein hohes Volumen kleiner Requests erzeugte: Die zusätzliche Latenz musste innerhalb von 7 ms bleiben, bei einer durchschnittlichen Request von etwa 2.5 ms, und das 98. und 99. Perzentil brachen unter häufigen G1-Pausen ein. Sie wechselten auf generational ZGC und zahlten mit Speicher.

Beachten Sie, was diese Reparatur verlangte: einen zweiten Collector, auf den man umschalten kann. Go liefert einen, und er ist nicht austauschbar, also sind die Go-Hebel die Allokationsrate, die Form des lebenden Sets und GOGC gegen GOMEMLIMIT. Was ein Rust-Hot-Path stattdessen entfernt, ist genau benennbar: kein Assist, kein Hintergrund-Marking, kein erzwungener Zyklus. Was nicht verschwindet, ist länger, als die meisten Teams erwarten:

  • Der Allokator, dazu Page Faults und NUMA-Placement: Die Rust-Standardbibliothek hält fest, dass der voreingestellte globale Allocator nicht spezifiziert ist, und malloc unter vielen Threads ist eine Tail-Quelle
  • Scheduling, denn eine async Runtime mit Worker-Pool reproduziert die Effekte des Go-Schedulers in dem Moment, in dem ein blockierender Task auf einem Worker landet
  • Speicherbuchhaltung, denn GOMEMLIMIT deckt nur den Speicher der Go-Runtime ab: Ein Rust-Allokator im selben Prozess liegt außerhalb Ihrer Obergrenze, und die Durchsetzung wandert zum OOM-Killer
  • Menschen, also wer nachts Bereitschaft für den Hot Path hat und was zwei Toolchains in einem Repository kosten
  • Die Grenze, falls Go außen bleibt: Cockroach Labs maß 2015 einen cgo-Call mit 171 ns gegenüber 1.83 ns für einen Go-Call, und was bleibt, ist das Verhältnis

Wenn der Tail im Parsen der Request steckt, in der Verbindung zur Exchange oder im Scheduler-Queueing, gibt Rust keine dieser Millisekunden zurück, und die falsche Komponente neu zu schreiben kostet ein Quartal und hält dieselbe Timeout-Rate.

Verschieben Sie also das kleinste Stück, dem die Allokationen gehören, nicht den Service: die Auswertungsschleife für die Impression und ihre Indizes, Kandidatenauswahl, Targeting, Frequency- und Budget-Lookups, Scoring. Bepreisen Sie die Grenze pro Übergang: einmal pro Bid Request mit einem flachen Puffer, nie einmal pro Targeting-Regel. Validieren Sie auf separaten Instanzen, die einen gespiegelten Stream bekommen, nie innerhalb des getesteten Prozesses, wo ein Schattenpfad genau die beiden Größen verdoppelt, die Sie messen.

Ein Akzeptanztest, der auf beiden Wegen funktioniert: ein kurzes Fenster mit abgeschaltetem Collector fahren, unter einer Speicherobergrenze, die Sie kontrollieren, und p99.9 innerhalb des Handlers aufzeichnen. Fahren Sie ihn auf einer Instanz hinter einem Bruchteil des Traffics und mit automatischem Rollback, denn mit abgeschaltetem GOGC treibt eine Heap-Spitze gegen die Obergrenze die Runtime in unmittelbar aufeinanderfolgende Zyklen, und der Guide sagt, dieser Stillstand kann unbegrenzt dauern. Lesen Sie das Ergebnis nur, wenn der GC-CPU-Limiter nie eingegriffen hat: Sobald er das tut, beschreibt das Perzentil den Limiter.

Die Firma, die das behebt, fragt zuerst nach dem Timeout-Report

Die zweite Hälfte der Frage, wer solche Arbeit macht, hat einen Test, der ohne Anbieterliste auskommt. Eine Firma, die GC-getriebene Latenz repariert, verhält sich anders als eine, die einen Rewrite verkauft:

  • Fragt nach der tmax-Verteilung und dem Timeout-Report je Exchange, bevor sie nach dem Repository fragt
  • Benennt die Aufteilung - Handler, Scheduler, Wire - und welchen der drei sie als Besitzer der Millisekunden erwartet, bevor sie eine Sprache vorschlägt
  • Bringt einen eigenen Open-Loop-Generator und ein eigenes Traffic-Profil mit und lehnt ein Closed-Loop-Perzentil als Beleg ab
  • Benennt das Abbruchkriterium vorab: welche Exchange, welches Perzentil, welcher Abstand zu ihrem tmax, wie viel Speicher pro tausend Requests
  • Kann den Hot Path danach besetzen, denn ein Rewrite, für den nachts niemand Bereitschaft hat, ist der zweite Incident

Keines davon verlangt Vertrauen: Jedes ist ein Dokument, das Sie im ersten Gespräch anfordern können, und kommt eines vage zurück, wird die Diagnose übersprungen.

Die erste Frage ist also nicht, welche Sprache. Sie lautet, welcher der drei Summanden - Handler, Scheduler oder Wire - die fehlenden Millisekunden auf der Verbindung besitzt, die ins Timeout läuft, und ob sich die pro Bid Request allokierten Bytes bewegen, wenn Sie sie angehen.

Was amBrain öffentlich belegen kann: amBrain ist ein Softwareentwicklungsunternehmen mit Fokus auf Trading-Plattformen, Matching-Engines, Real-Time-Bidding-Systeme und Casino-Plattform-Engineering, wir bauen seit 2019 Software, und in AdTech bauen wir DSP-Entwicklung, Real-Time-Bidding-Plattformen und Ad-Exchange-Engineering. Wir arbeiten in drei Formaten: vollständige Umsetzung, dediziertes Team oder Engineers, die in Ihrem Team mitarbeiten. Wenn Sie einen Rewrite gegen einen Tuning-Durchgang abwägen, führt das Gespräch, das sich lohnt, erst die Aufteilung durch und wählt dann eine Sprache.

Liegt ein solcher Entwurf bei Ihnen auf dem Tisch?

Bringen Sie Ihre aktuelle Architektur und den Fehlerfall mit, der Sie beunruhigt - wir gehen ihn in einer halben Stunde gemeinsam durch.

Verwandte Artikel

Bild konnte nicht geladen werden
AdTech
Sep 10, 202610 Min. Lesezeit

Ad Measurement verliert Events im Peak: Nahtstellen, doppelte Schlüssel und der Join mit dem Bidder

Beitrag lesen
Bild konnte nicht geladen werden
AdTech
Mar 5, 20267 Min. Lesezeit

Wie KI Programmatic Advertising 2026 verändert

Beitrag lesen
Bild konnte nicht geladen werden
AdTech
Feb 14, 20266 Min. Lesezeit

Privacy-First-Targeting: Ad Tech ohne Third-Party-Cookies bauen

Beitrag lesen