
Der Bericht behandelt praktische Fragen zur Entwicklung von Operatoren in Kubernetes, zur Gestaltung ihrer Architektur und zu den grundlegenden Funktionsprinzipien.
Im ersten Teil des Berichts werden wir erörtern:
- was ein Operator in Kubernetes ist und wozu er benötigt wird;
- wie der Operator das Management komplexer Systeme vereinfacht;
- was der Operator kann und was nicht.
Dann wenden wir uns der Diskussion über die interne Struktur des Operators zu. Wir betrachten die Architektur und die Funktionsweise des Operators Schritt für Schritt. Wir werden detailliert untersuchen:
- die Interaktion zwischen dem Operator und Kubernetes;
- welche Funktionen der Operator übernimmt und was er an Kubernetes delegiert.
Wir werden das Management von Shards und Replikaten von Datenbanken in Kubernetes betrachten.
Anschließend werden wir die Fragen zur Datenspeicherung diskutieren:
- wie man persistenten Speicher aus der Sicht des Operators nutzt;
- die Fallstricke bei der Verwendung von lokalem Speicher.
Im abschließenden Teil des Berichts werden wir praktische Anwendungsbeispiele betrachten mit Amazon oder Google Cloud Service. Der Bericht stützt sich auf die Entwicklung und Erfahrungen mit dem Operator für ClickHouse.
Video:

Ich bin Wladislaw Klimenko. Heute möchte ich über unsere Erfahrungen in der Entwicklung und dem Betrieb des Operators sprechen, und zwar über einen spezialisierten Operator zur Verwaltung von Datenbank-Clustern. Am Beispiel von zur Verwaltung des ClickHouse-Clusters.

Warum sind wir in der Lage, über den Operator und ClickHouse zu sprechen?
- Wir kümmern uns um die Unterstützung und Weiterentwicklung von ClickHouse.
- Momentan versuchen wir, schrittweise unseren Teil zur Entwicklung von ClickHouse beizutragen. Wir sind nach Yandex die zweitgrößte Quelle für Änderungen an ClickHouse.
- Wir bemühen uns, zusätzliche Projekte für das Ökosystem von ClickHouse zu realisieren.
Über eines dieser Projekte möchte ich sprechen. Es handelt sich um den ClickHouse-Operator für Kubernetes.
In meinem Bericht möchte ich zwei Themen ansprechen:
- Das erste Thema ist, wie unser Operator zur Verwaltung der ClickHouse-Datenbanken in Kubernetes funktioniert.
- Das zweite Thema ist, wie jeder Operator funktioniert, d. h. wie er mit Kubernetes interagiert.
Diese beiden Fragen werden sich durch meinen gesamten Bericht ziehen.

Wer interessiert sich dafür, was ich zu erzählen versuche?
- Besonders interessant wird es für diejenigen sein, die Operatoren betreiben.
- Oder für diejenigen, die ihren eigenen Operator entwickeln möchten, um zu verstehen, wie er intern arbeitet, wie der Operator mit Kubernetes interagiert und welche Fallstricke auftreten können.

Um das, was wir heute besprechen werden, besser zu verstehen, wäre es hilfreich zu wissen, wie Kubernetes funktioniert und über Grundkenntnisse in Cloud-Technologien zu verfügen.

Was ist ClickHouse? Es ist eine spaltenbasierte Datenbank, die speziell für die Online-Verarbeitung analytischer Abfragen ausgelegt ist. Und sie ist vollständig Open Source.
Und wir müssen nur zwei Dinge wissen. Erstens, dass es sich um eine Datenbank handelt, sodass das, was ich erzählen werde, praktisch auf jede Datenbank zutrifft. Und zweitens, dass das DBMS ClickHouse sehr gut skalierbar ist und praktisch eine lineare Skalierbarkeit bietet. Daher ist der Zustand des Clusters für ClickHouse ein natürlicher Zustand. Und wir sind besonders daran interessiert, zu diskutieren, wie man einen ClickHouse-Cluster in Kubernetes verwaltet.

Warum ist es dort notwendig? Warum können wir es nicht selbstständig betreiben? Die Antworten sind teilweise technischer und teilweise organisatorischer Natur.
- In der Praxis begegnen wir immer häufiger der Situation, dass in großen Unternehmen fast alle Komponenten bereits in Kubernetes laufen. Die Datenbanken sind jedoch außen vor.
- Und immer öfter wird die Frage gestellt: „Kann man das integrieren?“. Daher versuchen große Unternehmen, das Management so weit wie möglich zu standardisieren, um schnell ihre Datenspeicher verwalten zu können.
- Und das hilft besonders, wenn es maximale Möglichkeiten zur Wiederholung am neuen Standort erforderlich sind, d.h. maximale Portabilität.

Wie einfach oder schwierig ist das? Es ist natürlich möglich, es manuell zu tun. Aber es ist nicht so einfach, weil wir es mit der Komplexität der Verwaltung von Kubernetes zu tun haben, die sich mit den spezifischen Anforderungen von ClickHouse verbindet. Und es entsteht eine Aggregation.
Und all dies zusammen ergibt einen recht großen Technologiebereich, dessen Verwaltung bereits recht kompliziert wird, da Kubernetes seine alltäglichen Betriebsfragen mitbringt und ClickHouse seine eigenen Anforderungen an den täglichen Betrieb hat. Insbesondere, wenn wir mehrere ClickHouse-Datenbanken haben und ständig mit ihnen etwas tun müssen.

ClickHouse hat bei dynamischer Konfiguration eine Vielzahl von Fragen, die eine ständige Belastung für DevOps darstellen:
- Wenn wir etwas in ClickHouse ändern wollen, zum Beispiel einen Replikat oder Shard hinzufügen, müssen wir das Konfigurationsmanagement durchführen.
- Danach das Datenbankschema ändern, denn ClickHouse hat einen spezifischen Sharding-Mechanismus. Dort muss das Datenbankschema aufgeteilt und die Konfigurationen verteilt werden.
- Es muss ein Monitoring eingerichtet werden.
- Sammlung von Logs für neue Shards, für neue Replikate.
- Sich um die Wiederherstellung kümmern.
- Und um den Neustart.
Das sind alltägliche Arbeiten, deren Handhabung in der Praxis man sich wünscht, dass sie erleichtert wird.

Kubernetes hilft gut bei der Betriebshandhabung, aber nur bei grundlegenden Systemfragen.
Kubernetes erleichtert und automatisiert solche Dinge wie:
- Wiederherstellung.
- Neustart.
- Verwaltung des Speichersystems.
Das ist gut, das ist der richtige Ansatz, aber es hat kein vollständiges Verständnis dafür, wie man einen Datenbankcluster betreibt.
Man wünscht sich mehr, man möchte, dass unsere gesamte Datenbank in Kubernetes läuft.

Man möchte etwas schaffen, das wie ein großer, magischer roter Knopf ist, auf den man drückt und der einen Cluster mit alltäglichen Aufgaben bereitstellt und über den gesamten Lebenszyklus aufrechterhält. Ein ClickHouse-Cluster in Kubernetes.
Und wir haben uns bemüht, eine Lösung zu entwickeln, die die Arbeit erleichtert. Das ist der ClickHouse-Operator für Kubernetes von Altinity.

Ein Operator ist ein Programm, dessen Hauptaufgabe es ist, andere Programme zu verwalten, d. h. es ist ein Verwalter.
Und er enthält Verhaltensmuster. Man kann das als kodifiziertes Wissen über das Fachgebiet bezeichnen.
Sein Hauptziel ist es, das Leben der DevOps zu erleichtern und das Mikromanagement zu verringern, damit er (DevOps) in höheren Begriffen denkt, d. h. damit er (DevOps) sich nicht mit Mikromanagement beschäftigt und nicht alle Details manuell einstellen muss.
Und genau das ist der Operator – ein Hilfsroboter, der sich um Mikrounter Aufgaben kümmert und DevOps unterstützt.

Warum braucht man einen Operator? Er zeigt sich besonders in zwei Aspekten als effektiv:
- Wenn der Spezialist, der sich mit ClickHouse beschäftigt, nicht genügend Erfahrung hat, aber ClickHouse bereits betrieben werden muss, erleichtert der Operator den Betrieb und ermöglicht die Verwaltung eines ClickHouse-Clusters mit einer ziemlich komplexen Konfiguration, ohne sich allzu sehr mit den inneren Abläufen zu befassen. Man gibt ihm einfach Aufgaben auf hohem Niveau, und das funktioniert.
- Und die zweite Aufgabe, in der er sich am besten beweist, ist, wenn es darum geht, eine große Anzahl standardisierter Aufgaben zu automatisieren. Er nimmt die Mikroaufgaben von den Systemadministratoren ab.

Das ist am meisten nötig für diejenigen, die gerade erst ihren Weg anfangen, oder für diejenigen, die viel mit Automatisierung zu tun haben müssen.

Was unterscheidet den auf Operatoren basierenden Ansatz von anderen Systemen? Es gibt doch Helm. Das hilft auch, ClickHouse zu installieren, man kann Helm-Charts erstellen, die sogar einen gesamten ClickHouse-Cluster aufsetzen. Was ist also der Unterschied zwischen einem Operator und zum Beispiel Helm?
Der grundlegende fundamentale Unterschied ist, dass Helm ein Paketmanagement-Tool ist, während der Operator weiter geht. Es geht um die Begleitung des gesamten Lebenszyklus. Es ist nicht nur die Installation, sondern tägliche Aufgaben, die Skalierung, Sharding usw. umfassen, das heißt, all das, was im Verlauf des Lebenszyklus erledigt werden muss (wenn nötig, auch die Deinstallation) – all das löst der Operator. Er versucht, den gesamten Lebenszyklus der Software zu automatisieren und zu unterstützen. Das ist der fundamentale Unterschied zu anderen Lösungen, die es gibt.

Das war der einführende Teil, lassen Sie uns weitermachen.
Wie bauen wir unseren Operator? Wir versuchen, das Cluster von ClickHouse als eine Ressource zu verwalten.
Hier auf der linken Seite des Bildes haben wir die Eingabedaten. Das ist YAML mit der Spezifikation für den Cluster, das auf die klassische Weise über kubectl an Kubernetes übergeben wird. Dort ergreift der Operator das, macht seine Magie und am Ende haben wir so ein Schema. Das ist die Implementierung von ClickHouse in Kubernetes.
Und dann werden wir langsam schauen, wie der Operator funktioniert, welche typischen Aufgaben gelöst werden können. Wir werden nur typische Aufgaben behandeln, da wir nur begrenzte Zeit haben. Es wird nicht alles besprochen, was der Operator leisten kann.

Lassen Sie uns von der Praxis ausgehen. Unser Projekt ist vollständig Open Source, daher kann man sich auf GitHub ansehen, wie es funktioniert. Und man kann mit dem Quick Start Guide einfach anfangen, wenn man gleich starten möchte.
Wenn man detailliert einsteigen möchte, bemühen wir uns, die Dokumentation in einem mehr oder weniger ansprechenden Zustand zu halten.

Lass uns mit einer praktischen Aufgabe beginnen. Die erste Aufgabe, mit der wir alle anfangen wollen, ist das Starten des ersten Beispiels, egal wie. Wie startet man ClickHouse mit dem Operator, ohne genau zu wissen, wie er funktioniert? Wir schreiben ein Manifest, da die gesamte Kommunikation mit k8s über Manifeste erfolgt.

Hier ist ein komplexes Manifest. Das, was wir rot hervorgehoben haben, ist das, worauf man achten sollte. Wir bitten den Operator, ein Cluster namens demo zu erstellen.
Bis jetzt sind das grundlegende Beispiele. Storage wird noch nicht beschrieben, aber wir werden später darauf zurückkommen. Momentan beobachten wir die Entwicklung des Clusters in Echtzeit.
Wir haben dieses Manifest erstellt. Wir füttern es unserem Operator. Er hat gearbeitet, seine Magie vollbracht.

Wir schauen in die Konsole. Drei Komponenten sind von Interesse – das sind Pod, zwei Services und ein StatefulSet.
Der Operator hat das erledigt, und wir können sehen, was genau er erstellt hat.

Er erstellt ungefähr folgendes Schema. Wir haben ein StatefulSet, einen Pod, ConfigMap für jede Replik und eine ConfigMap für das gesamte Cluster. Unbedingt Dienste als Einstiegspunkte in das Cluster.
Die Dienste sind der zentrale Load Balancer Service und zusätzlich kann es für jede Replik, für jedes Shard einen weiteren geben.
So sieht unser Basis-Cluster ungefähr aus. Er besteht aus genau einem einzigen Node.

Lass uns weitermachen und es komplizierter machen. Wir müssen das Cluster sharden.

Unsere Aufgaben wachsen, die Dynamik beginnt. Wir möchten ein Shard hinzufügen. Wir beobachten die Entwicklung. Wir ändern unsere Spezifikation und geben an, dass wir zwei Shards wollen.
Das ist dieselbe Datei, die sich dynamisch mit dem Wachstum des Systems entwickelt. Storage gibt es nicht, das wird später behandelt, es ist ein separates Thema.
Wir füttern den YAML-Operator und schauen, was herauskommt.

Der Operator hat nachgedacht und die folgenden Entitäten erstellt. Wir haben bereits zwei Pods, drei Services und plötzlich 2 StatefulSets. Warum 2 StatefulSets?

Auf dem Diagramm sah es so aus – das ist unser ursprünglicher Zustand, als wir einen Pod hatten.

Es sieht jetzt so aus. Bis jetzt ist alles einfach, es wurde dupliziert.

Und warum wurden die StatefulSets zwei? Hier muss man ablenken und darüber diskutieren, wie in Kubernetes die Pods verwaltet werden.
Es gibt ein Objekt namens StatefulSet, das es ermöglicht, eine Menge von Pods aus einer Vorlage zu erstellen. Der Schlüssel dazu ist das Template. Und man kann in einem StatefulSet viele Pods aus einem einzigen Template starten. Der entscheidende Satz hier ist: „viele Pods aus einem einzigen Template“.
Es gab einen großen Anreiz, das gesamte Cluster in einem einzigen StatefulSet zu erstellen. Das würde funktionieren, da gibt es keine Probleme. Aber es gibt einen Punkt. Wenn wir ein heterogenes Cluster zusammenstellen wollen, d. h. aus mehreren Versionen von ClickHouse, dann stellt sich die Frage. Ja, StatefulSet kann ein Rolling Update durchführen, ja, dort kann eine neue Version eingespielt werden, und es wird erklärt, dass man nicht mehr als so und so viele Nodes gleichzeitig ausprobieren sollte.
Aber wenn wir das Problem extrapolieren und sagen, dass wir ein vollständig heterogenes Cluster erstellen möchten und nicht von einer alten Version auf eine neue mittels Rolling Update wechseln möchten, sondern einfach ein heterogenes Cluster mit verschiedenen Versionen von ClickHouse und unterschiedlichen Speichern aufbauen wollen. Wir möchten zum Beispiel einige Replikate auf separaten, langsameren Festplatten erstellen, insgesamt ein vollständig heterogenes Cluster aufbauen. Und da StatefulSet eine standardisierte Lösung aus einer Vorlage erstellt, ist das nicht möglich.
Nach einigem Überlegen wurde entschieden, dass wir es so machen. Jede Replikate befindet sich in ihrem eigenen StatefulSet. Es gibt einige Nachteile bei dieser Lösung, aber in der Praxis fasst der Operator alles vollständig ein. Es gibt viele Vorteile. Wir können ein vollständig heterogenes Cluster nach unseren Wünschen aufbauen. Deshalb haben wir in einem Cluster, in dem wir zwei Shards mit einer Replikate haben, 2 StatefulSets und 2 Pods, genau weil wir diesen Ansatz aus den oben genannten Gründen gewählt haben, um ein heterogenes Cluster aufbauen zu können.

Zurück zu den praktischen Aufgaben. In unserem Cluster müssen wir Benutzer konfigurieren, d. h. wir müssen eine Art Konfiguration von ClickHouse in Kubernetes vornehmen. Der Operator bietet dafür alle Möglichkeiten.

Wir können direkt im YAML schreiben, was wir möchten. Alle Konfigurationsoptionen werden direkt aus diesem YAML in die ClickHouse-Konfigurationen gemappt, die dann im gesamten Cluster verteilt werden.
Das kann man auch so schreiben. Das ist nur ein Beispiel. Das Passwort kann verschlüsselt werden. Alle Konfigurationsoptionen von ClickHouse werden vollständig unterstützt. Dies ist nur ein Beispiel.
Die Konfiguration wird als ConfigMap im Cluster verteilt. In der Praxis erfolgt das Update der ConfigMap nicht sofort, deshalb kann es in einem großen Cluster einige Zeit in Anspruch nehmen, die Konfiguration zu propagieren. Aber das ist alles sehr praktisch im Betrieb.

Wir erschweren die Aufgabe. Der Cluster entwickelt sich weiter. Wir möchten Daten replizieren. Das heißt, wir haben bereits zwei Shards, jeweils mit einer Replik, und es sind Benutzer eingerichtet. Wir wachsen und wollen uns mit der Replikation beschäftigen.

Was benötigen wir für die Replikation?
Wir benötigen ZooKeeper. In ClickHouse basiert die Replikation auf ZooKeeper. ZooKeeper wird benötigt, damit verschiedene Replikate von ClickHouse einen Konsens darüber haben, welche Datenblöcke auf welchem ClickHouse vorhanden sind.
Jeden ZooKeeper können wir verwenden. Wenn das Unternehmen einen externen ZooKeeper hat, kann er verwendet werden. Wenn nicht, kann man ihn aus unserem Repository installieren. Es gibt einen Installer, der das ganze Verfahren erleichtert.

Und das Schema für die Interaktion des gesamten Systems sieht so aus. Wir haben Kubernetes als Plattform. Darauf läuft der ClickHouse-Operator. ZooKeeper habe ich hier dargestellt. Der Operator interagiert sowohl mit ClickHouse als auch mit ZooKeeper. Das heißt, es findet eine Interaktion statt.
Und all das ist notwendig, damit ClickHouse Daten erfolgreich in k8s replizieren kann.

Lassen Sie uns nun die eigentliche Aufgabe betrachten, nämlich wie das Manifest für die Replikation aussehen wird.
Wir fügen unserem Manifest zwei Abschnitte hinzu. Der erste – woher wir ZooKeeper beziehen, der sowohl innerhalb von Kubernetes als auch extern sein kann. Das ist einfach eine Beschreibung. Und wir bestellen Replikate. Das heißt, wir möchten zwei Replikate. Insgesamt sollten wir 4 Pods erhalten. An den Speicher denken wir, er wird später zurückkommen. Speicher ist ein separates Thema.

Es war so.

Es wird so. Es werden Replikate hinzugefügt. Die 4. passte nicht, wir glauben, dass es dort viele geben kann. Und daneben wird ZooKeeper hinzugefügt. Die Schemas werden komplizierter.

Und es ist Zeit, die nächste Aufgabe hinzuzufügen. Wir werden einen Persistent Storage hinzufügen.
Für Persistent Storage haben wir verschiedene Ausführungsvarianten.
Falls wir in einem Cloud-Anbieter aktiv sind, etwa bei Amazon oder Google, besteht die große Versuchung, Cloud-Speicher zu nutzen. Das ist sehr bequem und gut.
Und es gibt eine zweite Variante. Das ist für den lokalen Speicher, wenn wir lokale Festplatten auf jeder Node haben. Diese Variante ist viel schwieriger umzusetzen, aber dafür leistungsfähiger.

Lassen Sie uns ansehen, was wir im Hinblick auf Cloud-Speicher haben.
Es gibt Vorteile. Es ist sehr einfach zu konfigurieren. Wir bestellen einfach beim Cloud-Anbieter, bitte gib uns einen Speicher dieser Kapazität und dieser Klasse. Die Klassen sind von den Anbietern selbst beschrieben.
Und es gibt einen Nachteil. Für manche ist das kein kritischer Nachteil. Natürlich wird es einige Einbußen bei der Leistung geben. Es ist sehr praktisch in der Anwendung, zuverlässig, aber es gibt einige potenzielle Leistungseinbußen.

Da ClickHouse speziell auf Leistung ausgerichtet ist, kann man sogar sagen, dass es alles herauskitzelt, was möglich ist, weshalb viele Kunden versuchen, die maximale Leistung herauszuholen.

Um das Maximum herauszuholen, benötigen wir lokalen Speicher.
Kubernetes bietet drei Abstraktionen zur Nutzung von lokalem Speicher in Kubernetes. Diese sind:
- EmptyDir
- HostPath.
- Local
Lassen Sie uns betrachten, wie sie sich unterscheiden und ähnlich sind.
Zunächst haben wir in allen drei Ansätzen den Speicher – das sind lokale Festplatten, die sich auf demselben physischen k8s-Knoten befinden. Aber es gibt einige Unterschiede.

Beginnen wir mit dem einfachsten, nämlich emptyDir. Was ist das in der Praxis? Wir bitten unser Spezifikationssystem (meistens ist das Docker), uns Zugriff auf einen Ordner auf der lokalen Festplatte zu gewähren.
In der Praxis erstellt Docker irgendwo auf seinen eigenen Pfaden einen temporären Ordner, nennt ihn mit einem langen Hash und gewährt Zugang zu ihm.
Wie wird das in Bezug auf die Leistung funktionieren? Es wird mit der Geschwindigkeit der lokalen Festplatte arbeiten, d.h. es ist ein vollständiger Zugriff auf das eigene Laufwerk.
Aber diese Methode hat ihren Nachteil. Persistent in diesem Fall ist ziemlich fraglich. Bei der ersten Bewegung von Docker mit Containern geht die Persistenz verloren. Wenn Kubernetes aus irgendeinem Grund diesen Pod auf eine andere Festplatte verschieben möchte, gehen die Daten verloren.
Dieser Ansatz eignet sich gut für Tests, weil er bereits eine angemessene Geschwindigkeit zeigt, aber für etwas Ernsthaftes ist diese Option nicht geeignet.

Deshalb gibt es den zweiten Ansatz. Das ist hostPath. Wenn man sich die vorherige Folie und diese ansieht, sieht man nur einen Unterschied. Unser Ordner ist aus Docker direkt auf den Kubernetes-Knoten gewandert. Hier ist es etwas einfacher. Wir geben den Pfad im lokalen Dateisystem an, an dem wir unsere Daten speichern möchten.
Die Vorteile dieser Methode sind vorhanden. Es handelt sich bereits um echte Persistenz, und zwar eine klassische. Unsere Daten werden an einer bestimmten Adresse auf der Festplatte gespeichert.
Es gibt auch Nachteile. Dazu gehört die Komplexität des Managements. Unser Kubernetes könnte sich entscheiden, einen Pod auf einen anderen physischen Knoten zu verschieben. Und hier kommt DevOps ins Spiel. Er muss der gesamten Architektur richtig erklären, dass diese Pods nur auf solche Knoten verschoben werden dürfen, auf denen etwas unter diesen Pfaden montiert ist, und niemals mehr als einen Knoten auf einmal. Das ist ziemlich kompliziert.
Speziell für diese Zwecke haben wir in unserem Operator Vorlagen entwickelt, um all diese Komplexität zu verbergen. Und man könnte einfach sagen: „Ich möchte, dass ich einen ClickHouse-Instanz auf jedem physischen Knoten und auf diesem bestimmten Pfad habe.“

Aber dieses Bedürfnis besteht nicht nur für uns, daher verstehen auch die Herren vom Kubernetes, dass die Menschen Zugang zu physischen Festplatten haben möchten, weshalb sie eine dritte Ebene bereitstellen.
Sie wird local genannt. Der Unterschied zum vorherigen Slide ist praktisch nicht vorhanden. Früher musste man manuell dafür sorgen, dass wir diese Pods nicht von Knoten zu Knoten verschieben konnten, weil sie an einem bestimmten Pfad an die lokale physische Festplatte angeschlossen sein mussten. Jetzt werden all diese Kenntnisse in Kubernetes selbst gekapselt. Es wird deutlich einfacher zu konfigurieren.

Kehren wir zu unserer praktischen Aufgabe zurück. Wir wenden uns der YAML-Vorlage zu. Hier haben wir echten Speicher. Wir sind zurückgekehrt zu diesem Punkt. Wir definieren eine klassische VolumeClaim-Vorlage wie in k8s. Und wir beschreiben, welchen Speicher wir möchten.
Danach wird k8s Speicher anfordern. Er wird uns diesen im StatefulSet zuweisen. Und letztendlich wird dies ClickHouse zur Verfügung stehen.

Wir hatten ein solches Schema. Unser Persistent Storage war rot, was andeutet, dass wir ihn erstellen sollten.

Und er wird grün. Jetzt ist das Schema des ClickHouse-Clusters auf k8s vollständig finalisiert. Wir haben Shards, Replikate, ZooKeeper, und es gibt einen echten Persistent Storage, der auf die eine oder andere Weise realisiert ist. Das Schema ist bereits vollständig funktionsfähig.

Wir machen weiter. Unser Cluster entwickelt sich weiter. Und Alexey gibt sich Mühe und veröffentlicht eine neue Version von ClickHouse.
Es entsteht die praktische Aufgabe, die neue Version von ClickHouse in unserem Cluster zu testen. Und natürlich möchte man nicht alles darauf zu aktualisieren, sondern lieber in einer abgelegenen Ecke eine Replik mit der neuen Version betreiben, oder vielleicht sogar nicht nur eine neue Version, sondern gleich zwei, weil sie ziemlich häufig erscheinen.
Was können wir dazu sagen?

Hier haben wir genau die Möglichkeit. Das sind die Vorlagen von Pods. Man kann festlegen, unser Operator erlaubt es vollständig, einen heterogenen Cluster aufzubauen. D.h. konfiguriert von allen Replikaten in einer Gruppe bis hin zu jeder einzelnen Replik, welche Version von ClickHouse und welche Version von Storage wir haben möchten. Wir können den Cluster in der benötigten Konfiguration komplett einrichten.

Wir werden jetzt etwas tiefer eintauchen. Zuvor haben wir darüber gesprochen, wie der ClickHouse-Operator im Hinblick auf die spezifischen Eigenschaften von ClickHouse funktioniert.
Jetzt möchte ich ein paar Worte darüber sagen, wie jeder Operator im Allgemeinen funktioniert und wie er mit K8s interagiert.

Lassen Sie uns zuerst die Interaktion mit K8s betrachten. Was passiert, wenn wir kubectl apply ausführen? Durch die API erscheinen unsere Objekte in etcd.

Beispielsweise die grundlegenden Kubernetes-Objekte: Pod, StatefulSet, Service und so weiter.
Dabei passiert physisch noch nichts. Diese Objekte müssen im Cluster materialisiert werden.

Dafür kommt ein Controller ins Spiel. Der Controller ist eine spezielle K8s-Komponente, die diese Beschreibungen materialisieren kann. Er weiß, was physisch zu tun ist. Er weiß, wie man Container startet und was eingestellt werden muss, damit der Server funktioniert.

Und er materialisiert unsere Objekte in K8s.
Aber wir möchten nicht nur mit Pods und StatefulSets arbeiten, wir möchten eine ClickHouseInstallation erstellen, d.h. ein Objekt vom Typ ClickHouse, um damit als Einheit zu arbeiten. Bisher gibt es diese Möglichkeit nicht.

Aber K8s hat eine weitere angenehme Eigenschaft. Wir möchten, dass irgendwo eine so komplexe Entität entsteht, in der unser Cluster aus Pods und StatefulSets zusammengefasst ist.

Was muss also dafür getan werden? Zuerst tritt die Custom Resource Definition auf den Plan. Was ist das? Es ist eine Beschreibung für K8s, dass es einen weiteren Datentyp geben wird, dass wir zu Pod, StatefulSet eine benutzerdefinierte Ressource hinzufügen möchten, die komplex ist. Dies ist eine Beschreibung der Datenstruktur.

Wir senden es ebenfalls über kubectl apply dorthin. Kubernetes hat es erfreut aufgenommen.
Und jetzt haben wir im Speicher, beim Objekt in etcd die Möglichkeit, eine benutzerdefinierte Ressource mit dem Namen ClickHouseInstallation zu registrieren.
Aber bisher wird nichts weiter passieren. D. h., wenn wir jetzt die YAML-Datei erstellen, die wir mit der Beschreibung der Shards, Replikate und sagen „kubectl apply“ betrachtet haben, wird Kubernetes sie akzeptieren, in etcd ablegen und sagen: „Super, aber was damit zu tun ist, weiß ich nicht. Wie man ClickHouseInstallation verwaltet, weiß ich nicht.“

Das heißt, wir brauchen jemanden, der Kubernetes hilft, den neuen Datentyp zu verwalten. Links haben wir einen nativen Kubernetes-Controller, der mit den nativen Datentypen arbeitet. Rechts sollte ein benutzerdefinierter Controller erscheinen, der mit benutzerdefinierten Datentypen arbeiten kann.
Und er wird anders als Operator bezeichnet. Ich habe ihn hier absichtlich außerhalb von Kubernetes platziert, weil er auch außerhalb von K8s ausgeführt werden kann. Die meisten Operatoren laufen natürlich in Kubernetes, aber nichts hindert ihn daran, außen zu stehen, deshalb ist er hier speziell nach außen verschoben.

Der benutzerdefinierte Controller, auch Operator genannt, interagiert über die API mit Kubernetes. Er kann bereits mit der API interagieren. Und er weiß bereits, wie man aus einer benutzerdefinierten Ressource ein komplexes Schema materialisiert, das wir erstellen möchten. Genau das tut der Operator.

Wie funktioniert der Operator? Lassen Sie uns in den rechten Teil schauen, um zu erfahren, wie er das macht. Wir erfahren, wie der Operator all dies materialisiert und wie die Interaktion mit K8s weitergeht.

Der Operator ist ein Programm. Es ist ereignisorientiert. Der Operator meldet sich mit Hilfe der Kubernetes API auf Ereignisse an. In der Kubernetes API gibt es Einstiegspunkte, an denen man sich auf Ereignisse anmelden kann. Und wenn sich etwas in K8s ändert, sendet Kubernetes Ereignisse an alle Interessierten, d. h. wer sich an diesen API-Punkt angemeldet hat, erhält die Benachrichtigungen.
Der Operator meldet sich auf Ereignisse, und er muss darauf reagieren. Seine Aufgabe ist es, auf die auftretenden Ereignisse zu reagieren.

Ereignisse werden durch bestimmte Updates erzeugt. Unsere YAML-Datei mit der Beschreibung der ClickHouseInstallation kommt an. Sie geht über kubectl apply nach etcd. Dort trat ein Ereignis ein, das letztendlich dieses Ereignis an den ClickHouse-Operator gesendet hat. Der Operator hat diese Beschreibung erhalten. Und er muss etwas tun. Wenn ein Update des Objekts ClickHouseInstallation eintrifft, muss der Cluster aktualisiert werden. Und die Aufgabe des Operators besteht darin, den Cluster zu aktualisieren.

Was macht er? Zunächst einmal müssen wir einen Aktionsplan erstellen, was wir mit diesem Update machen werden. Updates können sehr klein sein, also klein in der YAML-Implementierung, können aber sehr große Änderungen im Cluster nach sich ziehen. Daher erstellt der Operator einen Plan und hält sich dann daran.

Er beginnt gemäß diesem Plan, diese Struktur intern zu erstellen, um Pods und Dienste zu materialisieren, also das zu tun, was seine Hauptaufgabe ist. Es ist, als würde man ein ClickHouse-Cluster in Kubernetes aufbauen.

Jetzt lassen Sie uns ein interessantes Thema ansprechen. Das ist die Aufteilung der Verantwortlichkeiten zwischen Kubernetes und dem Operator, d.h. was Kubernetes tut, was der Operator tut und wie sie untereinander interagieren.
Kubernetes ist für systematische Dinge zuständig, also für eine grundlegende Sammlung von Objekten, die man als systemscope interpretieren kann. Kubernetes weiß, wie man Pods startet, wie man Container neu startet, wie man Volumes anbindet, wie man mit ConfigMaps arbeitet, also alles, was man als System bezeichnen kann.
Operatoren betreiben in Fachbereichen. Jeder Operator ist für seinen eigenen Fachbereich konzipiert. Wir haben einen für ClickHouse erstellt.
Der Operator interagiert genau in den Begriffen des Fachbereichs, wie beispielsweise eine Replikation hinzufügen, ein Schema erstellen, Monitoring einrichten. So ergibt sich eine solche Teilung.

Lassen Sie uns am praktischen Beispiel betrachten, wie diese Aufteilung der Verantwortlichkeiten erfolgt, wenn wir die Aktion "Replik hinzufügen" durchführen.
Im Operator kommt die Aufgabe – eine Replik hinzuzufügen. Was macht der Operator? Der Operator berechnet, dass ein neuer StatefulSet erstellt werden muss, in dem bestimmte Vorlagen und Volume-Claims beschrieben werden müssen.

Er hat alles vorbereitet und übergibt es weiter an K8s. Er sagt, dass er eine ConfigMap, einen StatefulSet, ein Volume benötigt. Kubernetes bearbeitet es. Es materialisiert die grundlegenden Einheiten, mit denen es arbeitet.

Und dann tritt erneut der ClickHouse-Operator in Aktion. Er hat bereits einen physischen Pod, auf dem er etwas tun kann. Und der ClickHouse-Operator arbeitet erneut in den Begriffen des Fachbereichs. Das heißt, um eine Replik in das Cluster einzufügen, muss man zunächst das Datenschema, das in diesem Cluster existiert, einrichten. Und zweitens muss diese Replik in das Monitoring aufgenommen werden, damit sie gut nachverfolgt werden kann. Dies richtet der Operator ein.

Und erst dann kommt ClickHouse selbst ins Spiel, d.h. eine weitere, höherstufige Entität. Das ist bereits eine Datenbank. Sie hat ihre eigene Instanz, eine konfigurierte Replik, die bereit ist, einem Cluster beizutreten.
Es ergibt sich eine recht lange Kette von Ausführungen und der Verteilung der Verantwortlichkeiten bei der Hinzufügung einer Replik.

Behalten wir unsere praktischen Aufgaben im Auge. Wenn ein Cluster bereits existiert, kann die Konfiguration migriert werden.

Wir haben es so gemacht, dass in die bestehende XML, die ClickHouse versteht, hindurchgeschoben werden kann.

Man kann ClickHouse fein abstimmen. Gerade zonierte Bereitstellungen sind das, was ich bei der Erklärung von hostPath und lokalem Speicher erläutert habe. So wird eine zonierte Bereitstellung korrekt gemacht.

Die nächste praktische Aufgabe ist das Monitoring.

Wenn sich unser Cluster ändert, müssen wir das Monitoring regelmäßig einstellen.
Lassen Sie uns das Schema betrachten. Die grünen Pfeile haben wir bereits betrachtet. Jetzt schauen wir uns die roten Pfeile an. So wollen wir unser Cluster überwachen. Wie die Metriken aus dem ClickHouse-Cluster in Prometheus gelangen und dann in Grafana.

Und was ist die Schwierigkeit beim Monitoring? Warum wird das als eine Art Errungenschaft angesehen? Die Schwierigkeit liegt genau in der Dynamik. Wenn wir einen Cluster haben und dieser statisch ist, kann das Monitoring einmalig eingerichtet werden und dann kümmert man sich nicht mehr darum.
Aber wenn wir viele Cluster haben oder sich ständig etwas ändert, dann ist der Prozess dynamisch. Und ständig das Monitoring umzustellen, ist ein Ressourcen- und Zeitaufwand, d.h. auch einfach Faulheit. Das muss automatisiert werden. Die Schwierigkeit liegt genau in der Dynamik des Prozesses. Und der Operator automatisiert das sehr gut.

Wie hat sich unser Cluster entwickelt? Am Anfang war er so.

Dann war er so.

Schließlich wurde er so.
Und das Monitoring wird automatisch vom Operator durchgeführt. Ein einziger Zugangspunkt.

Und wir schauen nur am Ausgang in das Grafana-Dashboard, wie das Leben in unserem Cluster pulsiert.
Übrigens wird das Grafana-Dashboard auch zusammen mit unserem Operator direkt im Quellcode bereitgestellt. Es kann angeschlossen und genutzt werden. Dieses Screenshot haben mir unsere DevOps gegeben.

Wohin möchten wir als nächstes gehen? Das ist:
- Die Automatisierung von Tests weiterentwickeln. Die Hauptaufgabe besteht in der automatisierten Testung neuer Versionen.
- Wir möchten auch die Integration mit ZooKeeper automatisieren. Wir planen, uns mit dem ZooKeeper-Operator zu integrieren. Das bedeutet, dass ein Operator für ZooKeeper geschrieben wurde, und es ist logisch, dass zwei Operatoren beginnen, sich zu integrieren, um eine benutzerfreundlichere Lösung zu entwickeln.
- Wir wollen komplexere Gesundheitsprüfungen durchführen.
- Ich habe in Grün hervorgehoben, dass wir mit der Vererbung von Templates – DONE – kurz davor stehen, d. h. mit der nächsten Version des Operators wird die Vererbung von Vorlagen verfügbar sein. Das ist ein leistungsstarkes Werkzeug, das es ermöglicht, komplexe Konfigurationen aus Einzelteilen zu erstellen.
- Und wir wollen die Automatisierung komplexer Aufgaben. Die Hauptaufgabe ist das Re-Sharding.

Lassen Sie uns ein Zwischenfazit ziehen.

Was erhalten wir am Ende? Und sollten wir uns damit beschäftigen oder nicht? Ist es notwendig, die Datenbank in Kubernetes zu integrieren und den Operator im Allgemeinen sowie den Alitnity-Operator im Besonderen einzusetzen?
Am Ende erhalten wir:
- Eine wesentliche Vereinfachung und Automatisierung der Konfiguration, Bereitstellung sowie Wartung.
- Sofort integrierte Überwachung.
- Und einsatzbereite, kodifizierte Vorlagen für komplexe Situationen. Aktionen wie das Hinzufügen eines Replikats müssen nicht mehr manuell durchgeführt werden. Das übernimmt der Operator.

Es bleibt nur noch eine letzte Frage. Wir haben bereits die Datenbank in Kubernetes und Virtualisierung. Wie steht es um die Leistung einer solchen Lösung, insbesondere im Hinblick darauf, dass ClickHouse für Leistung optimiert ist?
Die Antwort lautet: alles in Ordnung! Ich werde das nicht im Detail ausführen, das wäre das Thema eines separaten Vortrags.

Es gibt jedoch ein Projekt namens TSBS. Was ist dessen Hauptaufgabe? Es ist ein Leistungstest für Datenbanken. Es ist ein Versuch, ähnliches zu vergleichen.
Wie funktioniert es? Ein Datensatz wird generiert. Dann wird dieser Datensatz auf einem identischen Testset in verschiedenen Datenbanken getestet. Und jede Datenbank löst die gleiche Aufgabe auf ihre Weise. Anschließend können die Ergebnisse verglichen werden.
Es werden bereits viele Datenbanken unterstützt. Ich habe drei Hauptdatenbanken hervorgehoben. Das sind:
- TimescaleDB.
- InfluxDB.
- ClickHouse.

Es wurde auch ein Vergleich mit einer ähnlichen Lösung durchgeführt. Ein Vergleich mit RedShift. Dieser Vergleich fand auf Amazon statt. ClickHouse übertrifft in diesem Bereich ebenfalls alle.

Welche Schlussfolgerungen können aus dem, was ich erzählt habe, gezogen werden?
- DB in Kubernetes ist möglich. Wahrscheinlich kann man alles Mögliche verwenden, aber insgesamt sieht es so aus, als ob es funktioniert. ClickHouse in Kubernetes kann auf jeden Fall mit Hilfe unseres Operators betrieben werden.
- Der Operator hilft, Prozesse zu automatisieren und erleichtert das Leben wirklich.
- Die Leistung ist normal.
- Und wir glauben, dass es möglich ist und genutzt werden sollte.
Open Source – schließen Sie sich uns an!
Wie ich bereits gesagt habe, ist der Operator ein vollständig Open Source Produkt, daher wäre es sehr gut, wenn möglichst viele Menschen ihn verwenden. Schließen Sie sich uns an! Wir freuen uns auf euch alle!
Vielen Dank an alle!
Fragen

Danke für den Vortrag! Mein Name ist Anton. Ich bin von der Firma SEMrush. Mich interessiert das Thema Logging. Über das Monitoring hört man viel, aber über das Logging weiß man nichts, wenn man über den gesamten Cluster spricht. Bei uns beispielsweise läuft der Cluster auf Hardware. Wir nutzen zentrales Logging und sammeln mit den standardmäßigen Mitteln in einem gemeinsamen Pool. Und dann holen wir dort die für uns interessanten Daten heraus.
Gute Frage, das heißt, Logging steht auf der To-Do-Liste. Unser Operator automatisiert das derzeit noch nicht. Er entwickelt sich noch, das Projekt ist noch recht jung. Wir verstehen die Notwendigkeit des Loggings. Es ist auch ein sehr wichtiges Thema. Und es ist wahrscheinlich nicht weniger wichtig als das Monitoring. Aber das Monitoring stand zuerst auf der Liste zur Umsetzung. Logging wird es geben. Wir bemühen uns natürlich, alle Aspekte des Clusterlebens zu automatisieren. Daher die Antwort – momentan kann der Operator das leider noch nicht, aber es ist in den Plänen, wir werden das umsetzen. Wenn jemand Lust hat mitzumachen, dann bitte Pull Request.
Hallo! Vielen Dank für den Vortrag! Ich habe eine Standardfrage zu den Persistent Volumes. Wenn wir mit diesem Operator eine Konfiguration erstellen, wie bestimmt der Operator, auf welchem Knoten eine Festplatte oder ein Ordner gemountet ist? Müssen wir ihm vorher erklären, dass er bitte unser ClickHouse genau auf diesen Knoten platzieren soll, auf denen eine Festplatte vorhanden ist?
Soweit ich das verstehe, ist diese Frage eine Fortsetzung des lokalen Speichers, insbesondere des Teils über den hostPath. Es geht darum, der gesamten Systemarchitektur zu erklären, dass der Pod genau auf diesem Knoten gestartet werden muss, an dem wir eine physisch angeschlossene Festplatte haben, die an einem bestimmten Pfad gemountet ist. Das ist ein ganz großer Abschnitt, den ich hier nur sehr oberflächlich angesprochen habe, da die Antwort ziemlich umfangreich ist.
Kurz gesagt sieht es so aus. Wir müssen natürlich das Provisioning dieser Volumes durchführen. Momentan gibt es im lokalen Speicher kein dynamisches Provisioning, weshalb die DevOps-Teams die Festplatten selbst erstellen müssen, also diese Volumes. Sie müssen Kubernetes das Provisioning erklären, dass es Persistent Volumes dieser Klasse gibt, die auf bestimmten Nodes liegen. Dann muss kubernetes erklärt werden, dass Pods, die einen bestimmten Typ von local storage benötigen, nur auf bestimmten Nodes anhand von Labels geplant werden sollen. Zu diesem Zweck gibt es im Operator die Möglichkeit, einen bestimmten Label und einen pro Host-Instanz zuzuweisen. Das führt dazu, dass die Pods von Kubernetes ausschließlich auf Nodes gestartet werden, die den Anforderungen entsprechen, vereinfacht gesagt. Die Administratoren vergeben Labels und führen das Provisioning der Festplatten manuell durch. Und dann skaliert es.
Und genau die dritte Variante von local hilft, dies ein wenig zu vereinfachen. Wie ich bereits betont habe, ist es eine mühsame Arbeit, die bei der Umsetzung hilft, die maximale Leistung zu erzielen.
Ich habe eine zweite Frage, die damit zusammenhängt. Kubernetes wurde so konzipiert, dass es uns egal ist, ob wir einen Node verlieren oder nicht. Was sollen wir in diesem Fall tun, wenn wir den Node verloren haben, an dem unsere Shard hängt?
Ja, Kubernetes wurde ursprünglich so positioniert, dass unsere Beziehung zu unseren Pods wie zu Vieh ist, und hier wird jede Festplatte zu einer Art Haustier. Es gibt dieses Problem, dass wir sie nicht einfach wegwerfen können. Die Entwicklung von Kubernetes geht in die Richtung, dass man nicht vollständig solidarisch mit diesem philosophisch umgehen kann, wie mit vollständig wegwerfbaren Ressourcen.
Jetzt zur praktischen Frage. Was tun, wenn Sie einen Node verloren haben, auf dem die Festplatte war? Hier wird das Problem auf einer höheren Ebene gelöst. Im Falle von ClickHouse haben wir Replikate, die auf einer höheren Ebene arbeiten, d.h. auf der ClickHouse-Ebene.
Was ergibt sich daraus? Für den Verlust von Daten ist DevOps verantwortlich. Er muss die Replikation richtig konfigurieren und sicherstellen, dass die Replikation durchgeführt wird. In der Replik auf der ClickHouse-Ebene müssen die Daten dupliziert sein. Das ist nicht die Aufgabe, die der Operator löst. Und das ist nicht die Aufgabe, die Kubernetes selbst löst. Das liegt auf der ClickHouse-Ebene.
Was tun, wenn Ihnen ein Eisenknoten abgerissen ist? Man muss also einen zweiten setzen, richtig den Disk provisionieren, Labels anbringen. Und danach wird sie die Anforderungen erfüllen, damit Kubernetes darauf ein Pod-Instance starten kann. Kubernetes wird es starten. Ihnen fehlen ja die Anzahl an Pods bis zur festgelegten Anzahl. Es wird durch den Zyklus gehen, den ich gezeigt habe. Und auf der obersten Ebene wird ClickHouse verstehen, dass uns eine Replica eingegangen ist, sie ist noch leer und man muss anfangen, Daten dorthin zu übertragen. Das heißt, dieser Prozess ist noch schlecht automatisiert.
Danke für den Vortrag! Wenn verschiedene Probleme auftreten, der Operator abstürzt und neu gestartet wird, während in diesem Moment Ereignisse eintreffen, verarbeiten Sie das irgendwie?
Was passiert, wenn der Operator abgestürzt ist und neu gestartet wurde, ja?
Ja. Und in diesem Moment sind Ereignisse eingegangen.
Die Aufgabe, was in diesem Fall zu tun ist, teilt sich teilweise zwischen dem Operator und Kubernetes auf. Kubernetes hat die Möglichkeit, das eingetretene Ereignis zu wiederholen. Er spielt es nach. Die Aufgabe des Operators besteht darin, sicherzustellen, dass, wenn ihm die Ereignisprotokolle erneut abgespielt werden, diese Ereignisse idempotent sind. Und dass das wiederholte Eintreffen desselben Ereignisses unser System nicht kaputt macht. Und unser Operator bewältigt diese Aufgabe.
Hallo! Danke für den Vortrag! Dmitrij Sawjalow, Unternehmen Smedova. Ist die Möglichkeit, die Konfiguration mit Haproxy in den Operator aufzunehmen, geplant? Interessiert mich ein anderer Load Balancer als der Standard, damit er intelligent ist und versteht, dass dort wirklich ClickHouse ist.
Sprechen Sie von Ingress?
Ja, Ingress durch Haproxy ersetzen. In Haproxy kann man die Cluster-Topologie angeben, wo seine Replikas sind.
Bisher haben wir noch nicht darüber nachgedacht. Wenn Sie das brauchen und erklären können, warum das notwendig ist, könnte es umgesetzt werden, insbesondere, wenn Sie teilnehmen möchten. Wir betrachten gerne die Option. Kurze Antwort – nein, wir haben momentan diese Funktionalität nicht. Danke für den Hinweis, wir werden uns das ansehen. Und wenn Sie uns noch den Anwendungsfall erklären und warum es in der Praxis notwendig ist, zum Beispiel, wenn Sie Issues auf GitHub erstellen, wäre das wundervoll.
Gibt es bereits.
Gut. Wir sind offen für alle Vorschläge. Und Haproxy steht auf der To-Do-Liste. Die To-Do-Liste wächst zurzeit, sie verringert sich nicht. Aber das ist gut, das heißt, dass das Produkt gefragt ist.
Quelle: habr.com
