Also, Sie sammeln Metriken. So wie wir. Auch wir sammeln Metriken. Natürlich solche, die für das Geschäft notwendig sind. Heute werden wir über das erste Glied in unserem Überwachungssystem sprechen – einen statsd-kompatiblen Aggregationsserver. , warum wir ihn geschrieben haben und warum wir auf brubeck verzichtet haben.

Aus unseren vorherigen Artikeln (, ) können Sie erfahren, dass wir bis zu einem bestimmten Zeitpunkt die Metriken mit Hilfe von gesammelt haben. Er ist in C geschrieben. In Bezug auf den Code – so einfach wie ein Korken (das ist wichtig, wenn Sie beitragen möchten) und vor allem, dass er ohne größere Probleme mit unserem Volumen von 2 Millionen Metriken pro Sekunde (MPS) in der Spitze zurechtkommt. Die Dokumentation behauptet eine Unterstützung von 4 Millionen MPS mit Sternchen. Das bedeutet, dass Sie die angegebene Zahl erreichen, wenn Sie das Netzwerk auf Linux korrekt konfigurieren. (Wie viele MPS man erreichen kann, wenn man das Netzwerk so lässt, wie es ist, wissen wir nicht). Trotz dieser Vorteile hatten wir einige ernsthafte Beschwerden über brubeck.
Beschwerde 1. Github – der Entwickler des Projekts – hat die Unterstützung eingestellt: keine Veröffentlichung von Patches und Fixes, keine Annahme unserer und (nicht nur unserer) PR. In den letzten Monaten (irgendwo seit Februar-März 2018) hat sich die Aktivität wiederbelebt, aber vorher gab es fast 2 Jahre vollständige Stille. Außerdem wird das Projekt , was ein ernsthaftes Hindernis für die Einführung neuer Funktionen sein kann.
Beschwerde 2. Die Genauigkeit der Berechnungen. Brubeck sammelt für die Aggregation nur 65536 Werte. In unserem Fall können während der Aggregation (30 Sekunden) bei einigen Metriken deutlich mehr Werte (1.527.392 in der Spitze) ankommen. Infolgedessen sind die Werte der Maxima und Minima völlig nutzlos. Zum Beispiel so:

Wie zuvor

Wie es sein sollte
Aus demselben Grund werden die Summen überhaupt falsch berechnet. Fügen Sie hier einen Fehler mit der Überlauf von 32-Bit-Float hinzu, der den Server bei Erhalt einer scheinbar harmlosen Metrik in einen Segfault versetzt, und es wird wirklich großartig. Der Fehler wurde übrigens bis heute nicht behoben.
Und schließlich, Beschwerde X. Zum Zeitpunkt des Verfassens dieses Artikels sind wir bereit, ihn allen 14 mehr oder weniger funktionierenden Implementierungen von statsd zu präsentieren, die wir finden konnten. Stellen wir uns vor, dass eine bestimmte Infrastruktur so gewachsen ist, dass die Verarbeitung von 4 Millionen MPS nicht mehr ausreicht. Oder sie ist vielleicht noch nicht gewachsen, aber die Metriken sind bereits so wichtig für Sie, dass selbst kurze, 2-3-minütige Ausfälle in den Grafiken kritisch werden können und bei den Managern anhaltende Depressionen auslösen. Da die Behandlung von Depressionen eine danklose Aufgabe ist, sind technische Lösungen erforderlich.
Erstens, Fehlertoleranz, damit ein plötzliches Problem auf dem Server im Büro keinen psychiatrischen Zombie-Apokalyptischen Anfall auslöst. Zweitens, Skalierung, um die Möglichkeit zu erhalten, mehr als 4 Millionen MPS zu verarbeiten, ohne tief im Netzwerk-Stack von Linux graben zu müssen, und um in die gewünschten Größen „breit“ wachsen zu können.
Da wir beim Thema Skalierung über Spielraum verfügten, entschieden wir uns, zunächst mit der Fehlertoleranz zu beginnen. „Oh! Fehlertoleranz! Das ist einfach, das können wir!“, dachten wir und starteten 2 Server, auf jedem eine Kopie von brubeck. Dazu mussten wir den Datenverkehr mit den Metriken auf beide Server kopieren und sogar dafür . Dieses Problem der Fehlertoleranz haben wir damit gelöst, aber… nicht sehr gut. Zunächst schien alles super zu funktionieren: Jeder brubeck sammelt seine eigene Variante der Aggregation, schreibt die Daten alle 30 Sekunden in Graphite und überschreibt den alten Intervall (das wird auf Seiten von Graphite gemacht). Wenn plötzlich ein Server ausfällt, haben wir immer einen zweiten mit einer eigenen Kopie der aggregierten Daten. Aber hier ist das Problem: Wenn der Server ausfällt, tritt in den Grafiken eine „Säge“ auf. Das hängt damit zusammen, dass die 30-Sekunden-Intervalle von brubeck nicht synchronisiert sind, und im Moment des Ausfalls wird einer von ihnen nicht überschrieben. Beim Starten des zweiten Servers passiert das Gleiche. Ziemlich erträglich, aber wir möchten es besser! Das Problem der Skalierbarkeit ist ebenfalls nicht verschwunden. Alle Metriken fliegen immer noch auf einen einzelnen Server, und daher sind wir durch die gleichen 2-4 Millionen MPS, abhängig von der Netzwerkverbindung, begrenzt.
Wenn man ein wenig über das Problem nachdenkt und gleichzeitig Schnee mit einer Schaufel schaufelt, kann einem eine offensichtliche Idee kommen: Wir brauchen ein StatsD, das im verteilten Modus funktioniert. Das heißt, ein System, in dem die Synchronisation zwischen den Knoten nach Zeit und Metriken realisiert ist. "Natürlich gibt es eine solche Lösung schon", sagten wir und fingen an zu googeln... und fanden nichts. Bei der Durchsicht der Dokumentation zu verschiedenen StatsD ( Stand 11.12.2017) fanden wir absolut nichts. Anscheinend sind weder die Entwickler noch die Nutzer dieser Lösungen mit SO VIELEN Metriken konfrontiert gewesen, sonst hätten sie mit Sicherheit etwas erfunden.
Und dann erinnerten wir uns an das "Spielzeug" StatsD - bioyino, das wir beim Hackathon just for fun geschrieben hatten (der Projektname wurde von einem Skript vor Beginn des Hackathons generiert) und erkannten, dass wir dringend unser eigenes StatsD benötigten. Warum?
- weil es in der Welt viel zu wenige Klone von StatsD gibt,
- weil wir die gewünschte oder annähernd gewünschte Fehlertoleranz und Skalierbarkeit gewährleisten können (einschließlich der Synchronisation aggregierter Metriken zwischen Servern und der Lösung von Konflikten beim Senden),
- weil wir Metriken genauer zählen können als es Brubeck tut,
- weil wir selbst detailliertere Statistiken sammeln können, die uns Brubeck praktisch nicht zur Verfügung stellte,
- weil sich die Gelegenheit bot, unsere eigene hyperperformante verteilte Skalierlab-Anwendung zu programmieren, die nicht die Architektur einer anderen solchen hyperperformanten.... nundíš azept.
Worauf schreiben? Natürlich auf Rust. Warum?
- weil bereits ein Prototyp der Lösung vorlag,
- weil der Autor des Artikels zu diesem Zeitpunkt bereits Rust kannte und brannte darauf, etwas für die Produktion zu schreiben, das in Open Source veröffentlicht werden kann,
- weil uns Sprachen mit GC aufgrund der Natur des erzeugten Traffics (praktisch in Echtzeit) nicht passen und GC-Pausen praktisch unzulässig sind,
- weil maximale Leistung erforderlich ist, die mit C vergleichbar ist,
- weil Rust uns furchtlose Nebenläufigkeit bietet, und wenn wir begännen, dies in C/C++ zu schreiben, würden wir noch mehr Sicherheitsanfälligkeiten, Buffer Overflows, Race Conditions und andere schreckliche Wörter als bei Brubeck erleiden.
Es gab auch ein Argument gegen Rust. Das Unternehmen hatte keine Erfahrung in der Erstellung von Projekten mit Rust und plant derzeit auch nicht, es im Hauptprojekt zu verwenden. Deshalb gab es ernsthafte Bedenken, dass es nicht klappen würde, aber wir entschieden uns, das Risiko einzugehen und es auszuprobieren.
Es verging Zeit…
Endlich, nach mehreren gescheiterten Versuchen, war die erste funktionierende Version bereit. Was ist dabei herausgekommen? Es ist so geworden.

Jede Node erhält ihr eigenes Set an Metriken und speichert diese, ohne die Metriken für die Typen zu aggregieren, für die eine vollständige Sammlung für die finale Aggregation erforderlich ist. Die Nodes sind durch ein Protokoll der verteilten Sperre (distributed lock) miteinander verbunden, das es ermöglicht, unter ihnen die eine einzige auszuwählen (hier weinten wir), die berechtigt ist, die Metriken an den Großen zu senden. Momentan wird dieses Problem mit Mitteln gelöst , aber in Zukunft erstrecken sich die Ambitionen des Autors bis zu Raft, wobei die besagte würdige Node natürlich die Konsensus-Führungs-Node sein wird. Neben dem Konsens senden Nodes ihren Nachbarn relativ häufig (standardmäßig einmal pro Sekunde) die Teile der voraggregierten Metriken zu, die sie in dieser Sekunde gesammelt haben. Daher bleibt die Skalierbarkeit und Fehlertoleranz erhalten – jede der Nodes behält nach wie vor ihr vollständiges Set an Metriken, aber die Metriken werden nun aggregiert, über TCP und in einem binären Protokoll codiert, wodurch die Ausgaben für Duplikate im Vergleich zu UDP deutlich reduziert werden. Trotz der relativ großen Anzahl an eingehenden Metriken benötigt die Speicherung nur sehr wenig Speicher und noch weniger CPU. Für unsere gut komprimierbaren Metriken sind das nur einige Dutzend Megabyte Daten. Als zusätzlichen Bonus erhalten wir das Fehlen überflüssiger Datenüberschreibungen in Graphite, wie es im Fall von burbeck war.
UDP-Pakete mit Metriken sind auf den Netzwerkgeräten über einfaches Rundlaufverfahren zwischen den Knoten verteilt. Natürlich versteht die Netzwerkhardware den Inhalt der Pakete nicht und kann daher viel mehr als 4 Millionen Pakete pro Sekunde verarbeiten, ganz zu schweigen von Metriken, über die sie überhaupt nichts weiß. Angesichts der Tatsache, dass Metriken nicht einzeln in jedem Paket kommen, erwarten wir an dieser Stelle keine Leistungsprobleme. Im Falle eines Serverausfalls erkennt das Netzwerkgerät schnell (innerhalb von 1-2 Sekunden) diesen Umstand und entfernt den ausgefallenen Server aus der Rotation. Infolgedessen können passive (d. h. nicht führende) Knoten ein- und ausgeschaltet werden, ohne dass signifikante Rückgänge in den Grafiken zu verzeichnen sind. Das Maximum, was wir verlieren, ist ein Teil der Metriken, die in der letzten Sekunde gesendet wurden. Ein plötzlicher Verlust/Ausfall/Umstieg des Leiters wird immer noch eine kleine Anomalie aufzeichnen (der 30-Sekunden-Intervall ist weiterhin unsynchronisiert), aber bei bestehender Verbindung zwischen den Knoten können auch diese Probleme minimiert werden, beispielsweise durch das Versenden synchronisierender Pakete.
Ein wenig über die interne Struktur. Die Anwendung ist natürlich mehrthreadig, aber die Architektur der Threads unterscheidet sich von der, die in Brubeck verwendet wird. Die Threads in Brubeck sind identisch – jeder von ihnen ist sowohl für das Sammeln von Informationen als auch für die Aggregation verantwortlich. In Bioyino sind die Arbeitsthreads (Worker) in zwei Gruppen unterteilt: eine für das Netzwerk und eine für die Aggregation. Diese Trennung ermöglicht eine flexiblere Verwaltung der Anwendung abhängig von der Art der Metriken: Wo intensive Aggregation erforderlich ist, kann die Anzahl der Aggregatoren erhöht werden, dort wo es viel Netzwerkverkehr gibt – kann die Anzahl der Netzwerkthreads erhöht werden. Derzeit arbeiten wir auf unseren Servern mit 8 Netzwerk- und 4 Aggregationsthreads.
Der zählende (für die Aggregation verantwortliche) Teil ist ziemlich langweilig. Befüllte Netzwerk-Threads werden auf die zählenden Threads verteilt, wo sie dann geparst und aggregiert werden. Auf Anfrage werden die Metriken zur Versendung an andere Knoten bereitgestellt. All dies, einschließlich der Übertragung von Daten zwischen den Knoten und der Arbeit mit Consul, erfolgt asynchron, basiert auf dem Framework .
Die Netzwerkkomponente, die für das Empfangen von Metriken verantwortlich ist, stellte bei der Entwicklung weitaus größere Probleme dar. Die Hauptaufgabe bei der Trennung der Netzwerkströme in separate Entitäten war der Versuch, die Zeit zu reduzieren, die der Stream für das Lesen von Daten aus dem Socket benötigt. nicht Die Möglichkeiten mit asynchronem UDP und dem herkömmlichen recvmsg fielen schnell weg: Erstere verbraucht zu viel CPU im User-Space zur Verarbeitung von Ereignissen, letztere verursacht zu viele Kontextwechsel. Daher wird jetzt verwendet mit großen Puffern (und die Puffer, liebe Offiziere, sind nicht einfach irgendetwas!). Die Unterstützung des normalen UDP wurde für weniger belastete Fälle beibehalten, in denen recvmmsg nicht notwendig ist. Im Multimessage-Modus gelingt es, das Hauptziel zu erreichen: Die überwiegende Mehrheit der Zeit verarbeitet der Netzwerkstream die Warteschlange des Betriebssystems — er liest Daten aus dem Socket und legt sie in den Userspace-Puffer, wobei er nur selten umschaltet, um den gefüllten Puffer an die Aggregatoren zurückzugeben. Die Warteschlange im Socket wächst praktisch nicht an, die Anzahl der verworfenen Pakete nimmt kaum zu.
Hinweis
In den Standardeinstellungen ist die Puffegröße ausreichend groß eingestellt. Wenn Sie zufällig entscheiden, den Server selbst auszuprobieren, stellen Sie möglicherweise fest, dass nach dem Senden einer geringen Anzahl von Metriken diese nicht in Graphite ankommen und im Puffer des Netzwerkstreams verbleiben. Um mit einer kleinen Anzahl von Metriken zu arbeiten, müssen die Werte für bufsize und task-queue-size in der Konfiguration kleiner eingestellt werden.
Zum Schluss — einige Grafiken für die Liebhaber von Grafiken.
Statistik der eingehenden Metriken pro Server: über 2 Millionen MPS.

Deaktivierung eines der Knoten und Neuzuteilung der eingehenden Metriken.

Statistik der ausgehenden Metriken: immer nur ein Knoten sendet — der Raidsboss.

Statistik über die Arbeiten jedes Knotens unter Berücksichtigung der Fehler in verschiedenen Modulen des Systems.

Detailansicht der eingehenden Metriken (Metriknamen sind verborgen).

Was haben wir in der Zukunft mit all dem geplant? Natürlich, Code schreiben, bl…! Das Projekt wurde ursprünglich als Open Source geplant und wird dies sein Leben lang bleiben. In naher Zukunft steht der Übergang zu einer eigenen Version von Raft, der Wechsel des Peer-Protokolls zu einem portableren, die Einführung zusätzlicher interner Statistiken, neuer Metriktypen, die Behebung von Fehlern und weitere Verbesserungen auf der Agenda.
Selbstverständlich sind alle, die an der Weiterentwicklung des Projekts mitarbeiten möchten, herzlich willkommen: erstellt PR, Issues, wir werden nach Möglichkeit antworten und weiterarbeiten usw.
Damit sind wir am Ende, wie man so schön sagt, das war's, Leute, kauft unsere Elefanten!

Quelle: habr.com
