Diese Geschichte handelt davon, wie wir Container in der Produktivumgebung nutzen, insbesondere unter Kubernetes. Der Artikel widmet sich der Sammlung von Metriken und Logs aus Containern sowie dem Erstellen von Images.

Wir sind ein Fintech-Unternehmen namens Exness, das Dienstleistungen für den Online-Handel und Fintech-Produkte für B2B und B2C entwickelt. In unserer F&E gibt es viele verschiedene Teams, in der Entwicklungsabteilung arbeiten über 100 Mitarbeiter.
Wir repräsentieren ein Team, das für die Plattform zuständig ist, über die unsere Entwickler Code bereitstellen und ausführen. Insbesondere sind wir verantwortlich für die Sammlung, Speicherung und Bereitstellung von Metriken, Logs und Ereignissen aus Anwendungen. Derzeit betreiben wir etwa dreitausend Docker-Container in der Produktivumgebung, unterstützen unser Big Data-Speicher mit 50 TB und bieten architektonische Lösungen an, die auf unserer Infrastruktur basieren: Kubernetes, Rancher und verschiedene öffentliche Cloud-Anbieter.
Unsere Motivation
Was brennt? Niemand kann das sagen. Wo ist der Brandherd? Schwer zu verstehen. Wann gab es das Feuer? Man kann es herausfinden, aber nicht sofort.

Warum stehen einige Container still, während andere abstürzen? Welcher Container war der Grund dafür? Denn äußerlich sind die Container identisch, doch jeder hat seinen eigenen Neo im Inneren.

Unsere Entwickler sind versierte Leute. Sie erstellen gute Dienste, die dem Unternehmen Gewinn bringen. Doch manchmal gibt es Pannen, wenn die Container mit den Anwendungen durcheinander geraten. Ein Container verbraucht zu viel CPU, ein anderer das Netzwerk, ein dritter führt Ein- und Ausgaben aus, und der vierte — man versteht überhaupt nicht, was er mit den Sockets macht. All das fällt aus, und das Schiff sinkt.
Agenten
Um zu verstehen, was drinnen passiert, haben wir beschlossen, Agenten direkt in die Container zu setzen.

Diese Agenten sind Schutzprogramme, die sicherstellen, dass die Container in einem Zustand bleiben, in dem sie sich nicht gegenseitig beschädigen. Die Agenten sind standardisiert, was es ermöglicht, den Ansatz zur Wartung der Container zu standardisieren.
In unserem Fall müssen die Agenten Logs im Standardformat bereitstellen, getaggt und mit Throttling. Außerdem müssen sie uns standardisierte Metriken zur Verfügung stellen, die in Bezug auf Geschäftsanwendungen erweiterbar sind.
Unter Agenten versteht man auch Tools für Betrieb und Wartung, die in verschiedenen Orchestrierungssystemen arbeiten können und unterschiedliche Images unterstützen (Debian, Alpine, Centos usw.).
Letztendlich müssen die Agenten einen einfachen CI/CD-Pipeline unterstützen, die Docker-Dateien beinhaltet. Andernfalls wird das Schiff auseinanderbrechen, da die Container über "schiefe" Schienen geliefert werden.
Der Build-Prozess und die Erstellung des Ziel-Images
Um alles zu standardisieren und verwaltbar zu machen, ist es notwendig, einen standardisierten Build-Prozess einzuhalten. Daher haben wir uns entschieden, Container mit Containern zu bauen – eine Art Rekursion.

Hier werden die Container durch durchgehende Konturen dargestellt. Wir haben auch entschieden, Distributive darin zu platzieren, damit das „Leben nicht wie ein Himbeerbaum erscheint“. Warum dies gemacht wurde, werden wir weiter unten erläutern.
Das Ergebnis ist ein Werkzeug für den Build – ein Container einer bestimmten Version, der auf bestimmte Versionen von Distributiven und Skripten verweist.
Wie verwenden wir es? Wir haben ein Docker Hub, das den Container enthält. Wir spiegeln ihn in unser System, um externe Abhängigkeiten loszuwerden. So entstand ein Container, der gelb markiert ist. Wir erstellen eine Vorlage, um alle benötigten Distributive und Skripte in den Container zu installieren. Danach erstellen wir ein betriebsbereites Image: Die Entwickler legen ihren Code und einige spezielle Abhängigkeiten hinein.
Was ist gut an diesem Ansatz?
- Erstens ermöglicht die vollständige Versionskontrolle der Build-Tools – der Build-Container, die Versionen der Skripte und der Distributiven.
- Zweitens haben wir Standardisierung erreicht: Wir erstellen Vorlagen, Zwischen- und betriebsbereite Images auf die gleiche Weise.
- Drittens bieten die Container Portabilität. Heute nutzen wir Gitlab, und morgen wechseln wir zu TeamCity oder Jenkins und können unsere Container genau so ausführen.
- Viertens gibt es eine Minimierung der Abhängigkeiten. Wir haben nicht zufällig Distributive in den Container gelegt, denn dies ermöglicht es, sie nicht jedes Mal aus dem Internet herunterzuladen.
- Fünftens hat sich die Geschwindigkeit des Builds erhöht – das Vorhandensein lokaler Kopien der Images erlaubt es, keine Zeit für das Herunterladen zu verschwenden, da es ein lokales Image gibt.
Mit anderen Worten, wir haben einen kontrollierten und flexiblen Build-Prozess erreicht. Wir verwenden die gleichen Mittel, um beliebige Container mit vollständiger Versionierung zu bauen.
Wie funktioniert unser Build-Verfahren?

Der Build wird mit einem einzigen Befehl gestartet, der Prozess wird im Image ausgeführt (rot markiert). Der Entwickler hat eine Docker-Datei (gelb markiert), die wir rendern, indem wir Platzhalter durch Werte ersetzen. Gleichzeitig fügen wir Header und Footer hinzu – das sind unsere Agenten.
Der Header fügt Distributionen aus den entsprechenden Images hinzu. Der Footer installiert unsere Dienste, konfiguriert den Start der Arbeitslast, das Logging und andere Agenten, ersetzt den entrypoint usw.

Wir haben lange darüber nachgedacht, ob wir einen Supervisor einsetzen sollen. Letztendlich haben wir beschlossen, dass wir ihn brauchen. Wir haben uns für S6 entschieden. Der Supervisor ermöglicht die Verwaltung des Containers: Er erlaubt den Zugriff im Falle eines Absturzes des Hauptprozesses und bietet eine manuelle Steuerung des Containers, ohne ihn neu erstellen zu müssen. Logs und Metriken sind Prozesse, die innerhalb des Containers ausgeführt werden. Auch sie müssen überwacht werden, und das machen wir mit dem Supervisor. Schließlich kümmert sich S6 um das housekeeping, die Signalverarbeitung und andere Aufgaben.
Da wir unterschiedliche Orchestrierungssysteme verwenden, muss der Container nach dem Build und Start verstehen, in welcher Umgebung er sich befindet, und entsprechend handeln. Zum Beispiel:
Das ermöglicht es uns, ein einzelnes Image zu erstellen und es in verschiedenen Orchestrierungssystemen zu starten, wobei der Start die Besonderheiten dieses Orchestrierungssystems berücksichtigt.

Für denselben Container erhalten wir unterschiedliche Prozessbäume in Docker und Kubernetes:

Die Nutzlast wird unter dem Supervisor S6 ausgeführt. Beachten Sie den Collector und die Events – das sind unsere Agenten, die für Logs und Metriken verantwortlich sind. In Kubernetes gibt es diese nicht, in Docker schon. Warum?
Wenn wir die Spezifikation eines Pods (hier und im Folgenden – Kubernetes Pod) betrachten, sehen wir, dass der Container Events in einem Pod ausgeführt wird, in dem sich ein separater Container Collector befindet, der die Funktion des Sammelns von Metriken und Logs übernimmt. Wir können die Möglichkeiten von Kubernetes nutzen: Container in einem Pod zu starten, im selben Prozess- und/oder Netzwerkraum. Tatsächlich können wir eigene Agenten implementieren und bestimmte Funktionen ausführen. Und wenn dieser Container in Docker gestartet wird, erhält er genau die gleichen Möglichkeiten, das heißt, er kann Logs und Metriken liefern, da die Agenten innerhalb des Containers ausgeführt werden.
Metriken und Logs
Die Lieferung von Metriken und Logs ist eine komplexe Aufgabe. Mit ihrer Lösung sind mehrere Aspekte verbunden.
Die Infrastruktur wird zum Ausführen von Workloads und nicht zur massenhaften Verteilung von Protokollen geschaffen. Das bedeutet, dass dieser Prozess mit minimalen Anforderungen an die Ressourcen der Container durchgeführt werden sollte. Wir streben danach, unseren Entwicklern zu helfen: „Nehmt den Docker Hub-Container, startet ihn, und wir können die Protokolle bereitstellen.“
Der zweite Aspekt ist die Begrenzung des Protokollvolumens. Wenn in mehreren Containern eine Situation mit einem Anstieg des Protokollvolumens auftritt (eine Anwendung gibt im Loop einen Stack-Trace aus), steigt die Belastung der CPU, der Kommunikationskanäle und des Protokollverarbeitungssystems, was sich negativ auf den Gesamteinsatz des Hosts und auf andere Container auf dem Host auswirken kann, und manchmal führt dies zu einem "Absturz" des Hosts.
Der dritte Aspekt ist, dass aus der Box heraus möglichst viele Methoden zur Metrik-Sammlung unterstützt werden müssen. Vom Lesen von Dateien und dem Abfragen von Prometheus-Endpunkten bis hin zur Verwendung spezifischer Anwendungsprotokolle.
Und der letzte Aspekt ist, dass der Ressourcenverbrauch minimiert werden muss.
Wir haben eine Open-Source-Lösung in Go namens Telegraf ausgewählt. Dies ist ein universeller Connector, der mehr als 140 Arten von Eingangsquellen (Input-Plugins) und 30 Arten von Ausgängen (Output-Plugins) unterstützt. Wir haben sie weiterentwickelt und erzählen jetzt, wie sie bei uns am Beispiel von Kubernetes verwendet wird.

Nehmen wir an, ein Entwickler deployt eine Last, und Kubernetes erhält die Anfrage zur Erstellung eines Pods. Zu diesem Zeitpunkt wird für jeden Pod automatisch ein Container mit dem Namen Collector erstellt (wir verwenden einen Mutation Webhook). Collector ist unser Agent. Bei der Initialisierung konfiguriert sich dieser Container, um mit Prometheus und dem Protokollsammlungssystem zu arbeiten.
- Dazu verwendet er die Pod-Anmerkungen, und je nach deren Inhalt wird beispielsweise ein Endpunkt (Prometheus-Endpunkt) erstellt;
- Basierend auf der Pod-Spezifikation und spezifischen Containereinstellungen wird entschieden, wie die Protokolle bereitgestellt werden.
Protokolle sammeln wir über die Docker API: Entwicklern genügt es, diese in stdout oder stderr zu schreiben, der Collector kümmert sich darum. Die Protokolle werden in Chunks mit einer gewissen Verzögerung gesammelt, um mögliche Überlastungen des Hosts zu verhindern.
Metriken werden pro Instanz der Arbeitslast (Prozesse) in Containern gesammelt. Alles wird mit Tags versehen: Namespace, Pod und so weiter, und wird dann in das Prometheus-Format konvertiert – und steht zur Sammlung bereit (außer den Protokollen). Außerdem senden wir Protokolle, Metriken und Ereignisse an Kafka und weiter:
- Protokolle sind in Graylog verfügbar (für die visuelle Analyse);
- Logs, Metriken und Ereignisse werden in Clickhouse für eine langfristige Speicherung gesendet.
Genau so funktioniert es in AWS, wir ersetzen einfach Graylog mit Kafka durch Cloudwatch. Wir senden die Logs dorthin, und es ist sehr praktisch: sofort ersichtlich, zu welchem Cluster und Container sie gehören. Das gleiche gilt für Google Stackdriver. Das heißt, unser Schema funktioniert sowohl lokal mit Kafka als auch in der Cloud.
Wenn wir jedoch kein Kubernetes mit Pods haben, wird das Schema etwas komplizierter, funktioniert aber nach denselben Prinzipien.

Innerhalb des Containers laufen die gleichen Prozesse, die mit Hilfe von S6 orchestriert werden. Alle diese Prozesse sind in einem einzigen Container gestartet.
Insgesamt
Wir haben eine umfassende Lösung zur Erstellung und Bereitstellung von Images entwickelt, mit Optionen für das Sammeln und Zustellen von Logs und Metriken:
- Wir haben einen standardisierten Ansatz zur Erstellung von Images entwickelt und darauf basierende CI-Vorlagen erstellt;
- Die Agenten für die Datensammlung sind unsere Erweiterungen von Telegraf. Wir haben sie gut in der Produktion getestet;
- Wir verwenden Webhooks zur Mutation für die Implementierung von Containern mit Agenten in Pods;
- Wir haben uns in das Ökosystem Kubernetes/Rancher integriert;
- Wir können identische Container in verschiedenen Orchestrierungssystemen ausführen und das gewünschte Ergebnis erzielen;
- Wir haben eine vollständig dynamische Konfiguration zur Verwaltung von Containern erstellt.
Mitsautor: Ilja Prudnikov
Quelle: habr.com
