Der Betrieb von Produktionsdiensten erfordert Kenntnis darüber, was in ihnen geschieht, doch das Hinzufügen von Metriken ist nicht kostenlos. In einem von InfoQ veröffentlichten Vortrag erklärt Brian Martin, Mitgründer von IOP Systems, dass der Unterschied zwischen Implementierungen ein Zähler-Update von einer Operation mit Kosten von etwa 5 Nanosekunden in eine Operation verwandeln kann, die mehr als eine Mikrosekunde benötigt. Bei Histogrammen kann sich der Unterschied bei konkurrierenden Threads von etwa 7 Nanosekunden auf mehrere Dutzend Mikrosekunden vergrößern.
Die zentrale Aussage des Vortrags besteht nicht darin, eine bestimmte Rust-Bibliothek auszuwählen, sondern die Messung als Teil des Performance-Designs zu behandeln. Eine Metrik innerhalb eines Pfads, der millionenfach aufgerufen wird, verstärkt jede noch so kleine Kostenquelle, während das Fehlen von Messungen die Diagnose von Verlangsamungen, Produktionsvorfällen und Performance-Problemen erschwert.
Beginnen Sie damit, Datentyp und Kosten ihrer Aktualisierung zu verstehen
Martin unterscheidet drei Haupttypen von Metriken: den Zähler, der normalerweise nicht sinkt, etwa die Anzahl der Anfragen; die Momentanmetrik, die einen aktuellen Wert darstellt, etwa die Warteschlangentiefe; und das Histogramm, das die Verteilung von Werten beschreibt, etwa Antwortzeiten. Diese Unterscheidung ist wichtig, weil jeder Typ andere Operationen erfordert und Histogramme Informationen liefern, die ein einzelner Gesamtzähler nicht bereitstellt.
In den einfachsten Fällen eignet sich ein atomic fetch_add für ganzzahlige Zähler. Vergleich-und-Tausch-Schleifen oder CAS müssen dagegen normalerweise erneut versuchen, wenn mehrere Threads um dieselbe Stelle konkurrieren. Ein großer Teil der Kosten entsteht durch die Synchronisierung von Cache-Zeilen zwischen den Kernen. Der Vortrag führt Messungen auf einer AWS-Graviton-Maschine mit 32 virtuellen Prozessoren an: Der theoretische Durchsatz lag bei etwa 119 Millionen Anfragen pro Sekunde mit einem kostengünstigen atomaren Update, gegenüber etwa 23 Millionen bei einer kostenintensiveren Implementierung mit Prometheus.
Reduzieren Sie Konkurrenz durch Aufteilung nach Prozessoren
Wenn sich alle Threads einen einzigen Zähler teilen, wird die Cache-Zeile ständig zwischen den Kernen übertragen. Martin schlägt stattdessen einen eigenen Zähler pro Prozessor vor, sodass Schreibvorgänge nahezu ohne Konkurrenz erfolgen, und die Werte erst beim Lesen zu summieren. Dieser Ansatz erhöht die Lesekosten geringfügig, schützt jedoch den Hot Path des Schreibens, der bei jeder Anfrage durchlaufen wird.
Hier ist auf das Phänomen des false sharing zu achten. Logisch unabhängige Zähler reichen nicht aus, wenn sie in derselben Cache-Zeile liegen; eine Cache-Zeile ist 64 Byte groß, sodass acht Zähler des Typs 64-bit nebeneinander darin Platz finden können. Daher empfiehlt der Vortrag, die Zähler zu gruppieren und mit Padding zu versehen, sodass sie getrennte Cache-Zeilen belegen. Den präsentierten Zahlen zufolge kann sich die theoretische Leistung von etwa 119 Millionen Anfragen pro Sekunde mit einem atomaren Zähler auf etwa 6,4 Milliarden Anfragen pro Sekunde erhöhen, wenn eine geeignete Aufteilung verwendet wird.
Entwerfen Sie Histogramme für den Aktualisierungspfad
Die Kosten eines Histogramms beginnen mit der Bestimmung des Buckets, zu dem ein Wert gehört. Eine lineare Suche innerhalb einer Bucket-Liste ist die einfachste und langsamste Option, während eine binäre Suche die Zahl der Vergleiche reduziert, aber weiterhin von der Anzahl der Buckets abhängt. Die schnellere Alternative ist die direkte Indizierung, bei der die Bucket-Nummer aus dem Wert berechnet wird, anstatt danach zu suchen.
Bei dieser Indizierung bestehen Zielkonflikte. Die Aufteilung in lineare Bereiche ist schnell, kann bei kleinen Werten jedoch einen relativ großen Fehler erzeugen. Eine logarithmische Indizierung hält den relativen Fehler besser konstant, doch die Berechnung des Logarithmus selbst ist teuer. Martin stellt die Verwendung äußerer, auf Log2 basierender Bereiche mit Unter-Buckets zur Feinabstimmung der Genauigkeit vor, wie bei HDR Histogram und H2Histogram. In einem nichtatomaren Test dauerten die Bestimmung des Buckets und die Aktualisierung bei HDR Histogram etwa 2,65 Nanosekunden und bei H2Histogram etwa 2,15 Nanosekunden.
Wann ist approximative Konsistenz akzeptabel?
Die Kosten eines Histogramms werden nicht allein durch die Indizierungsmethode bestimmt. Manche Anwendungen aktualisieren pro Vorgang mehrere Atomics, verwenden CAS für Summenwerte oder erzwingen für einen konsistenten Snapshot eine Sperre. Der Vortrag weist darauf hin, dass einige Implementierungen mehr als zwei Mikrosekunden und bei 32 Kernen sogar mehrere Dutzend Mikrosekunden erreichten, während eine Implementierung mit direkter Indizierung und einem einzigen atomaren Update näher an den Kosten eines Zählers lag.
Die Alternative ist approximative Konsistenz: Einige Buckets können sich während des Lesens des Histogramms ändern, doch der Unterschied zwischen zwei aufeinanderfolgenden Messungen bleibt nützlich, wenn die Metriken ohnehin nur Näherungswerte sind. Dies ist keine allgemeingültige Regel; Systeme, die einen vollständig konsistenten Snapshot benötigen, können trotz der Kosten eine Synchronisierung bevorzugen.
Die redaktionelle Einschätzung von certi.news
Praktisch ändert sich dadurch, dass die Entscheidung für das Hinzufügen von Metriken auch die Struktur der Aktualisierung einbeziehen muss, nicht nur die Namen der Metriken. Atomare Zähler, die Aufteilung nach Prozessoren und direkte Indizierung können Messungen innerhalb sensibler Pfade nutzbar machen, während synchronisierte oder dynamisch erweiterbare Histogramme unter hoher Last erhebliche Kosten verursachen können. Der Vortrag bietet kein einheitliches Rezept für jede Bibliothek oder jeden Dienst; er zeigt vielmehr, dass Flexibilität, die Verwendbarkeit der Bibliothek in anderen Projekten, Konsistenz und Leistung teilweise miteinander konkurrierende Ziele sind. Daher sollte die tatsächliche Implementierung unter den vorgesehenen Konkurrenzniveaus und Verarbeitungsmengen getestet werden, statt sich auf den Namen der Bibliothek oder das Ergebnis eines nicht konkurrierenden Zustands zu verlassen.
Martin stellt außerdem die Verwendung von eBPF über das Projekt Rezolus vor, um präzise Metriken aus dem Linux-Kernel zu gewinnen, darunter den Scheduler, Systemaufrufpfade und den TCP-Stack, ohne den Kernel-Code zu ändern. Offen bleibt, wie viel Genauigkeit und Konsistenz in den jeweiligen Fällen erforderlich sind und ob die Kosten des Lesens oder des Zusammenführens der Teilwerte bei einer Skalierung des Systems weiterhin akzeptabel bleiben.