Cassandra. Wie man nicht stirbt, wenn man nur Oracle kennt

Hallo, Habr.

Ich heiße Misha Butrimov und möchte ein wenig über Cassandra erzählen. Mein Vortrag wird für diejenigen nützlich sein, die noch nie mit NoSQL-Datenbanken in Berührung gekommen sind, da es viele Besonderheiten bei der Implementierung und Fallstricke gibt, die man kennen sollte. Und wenn Sie außer Oracle oder einer anderen relationalen Datenbank nichts gesehen haben, werden Ihnen diese Dinge das Leben retten.

Was macht Cassandra so gut? Es ist eine NoSQL-Datenbank, die ohne einen einzigen Ausfallpunkt entworfen wurde und sich gut skalieren lässt. Wenn Sie ein paar Terabyte für eine Datenbank hinzufügen müssen, fügen Sie einfach Knoten zum Ring hinzu. Möchten Sie sie auf ein weiteres Rechenzentrum erweitern? Fügen Sie Knoten zum Cluster hinzu. Möchten Sie die bearbeiteten RPS erhöhen? Fügen Sie Knoten zum Cluster hinzu. Es funktioniert auch in die andere Richtung.

Cassandra. Wie man nicht stirbt, wenn man nur Oracle kennt

Was ist noch gut daran? Dass sie viele Anfragen verarbeiten kann. Aber wie viele? 10.000, 20.000, 30.000, 40.000 Anfragen pro Sekunde – das ist nicht viel. 100.000 Anfragen pro Sekunde für Schreibvorgänge – auch nicht. Es gibt Unternehmen, die sagen, dass sie 2 Millionen Anfragen pro Sekunde verarbeiten. Deren Aussage müssen wir wohl glauben.

Im Grunde hat Cassandra eine große Abweichung von relationalen Daten – sie ist überhaupt nicht ähnlich. Und das ist sehr wichtig zu beachten.

Nicht alles, was gleich aussieht, funktioniert gleich.

Eines Tages kam ein Kollege zu mir und fragte: „Hier ist das SQL Cassandra Query Language, und es gibt ein SELECT-Statement, es gibt ein WHERE und es gibt ein AND. Ich schreibe die Buchstaben, und es funktioniert nicht. Warum?“. Wenn man Cassandra wie eine relationale Datenbank behandelt, ist das der ideale Weg, um ein schlimmes Selbstmordurteil zu fällen. Und ich propagiere das nicht, es ist in Russland verboten. Sie werden einfach etwas falsch konzipieren.

Zum Beispiel kommt ein Kunde zu uns und sagt: „Lass uns eine Datenbank für Serien bauen oder eine Datenbank für ein Rezeptregister. Wir haben da Gerichte mit Zutaten oder eine Liste von Serien und den Schauspielern darin“. Wir sagen fröhlich: „Lass uns machen!“. Es sind nur zwei Bytes zu übertragen, ein paar Tabellen und alles ist bereit, es wird sehr schnell und zuverlässig funktionieren. Und alles ist großartig, bis die Kunden kommen und sagen, dass Hausfrauen auch die Umkehrfrage lösen: sie haben eine Liste von Zutaten und wollen wissen, welches Gericht sie zubereiten möchten. Dann sind Sie erledigt.

Das liegt daran, dass Cassandra eine hybride Datenbank ist: Sie ist gleichzeitig eine Key-Value-Datenbank und speichert Daten in breiten Spalten. Wenn wir in der Sprache Java oder Kotlin sprechen, könnte man es so beschreiben:

Map<RowKey, SortedMap>

Das heißt, eine Map, in der sich noch eine sortierte Map befindet. Der erste Schlüssel dieser Map ist der Row-Key oder Partition-Key – der Partitionierungsschlüssel. Der zweite Schlüssel, der den Zugriff auf die bereits sortierte Map ermöglicht, ist der Clustering-Key.

Um die Verteilung der Datenbank zu veranschaulichen, zeichnen wir drei Knoten. Jetzt müssen wir verstehen, wie wir die Daten auf die Knoten verteilen. Denn wenn wir alles in einen Knoten stecken (es können übrigens tausend, zweitausend oder fünf – so viele wie nötig – sein), ist das nicht wirklich verteilungsorientiert. Daher benötigen wir eine mathematische Funktion, die eine Zahl zurückgibt. Einfach eine Zahl, ein langes int, das in einen bestimmten Bereich fällt. Und ein Knoten wird für einen Bereich verantwortlich sein, der zweite für den nächsten, der n-te für den n-ten.

Cassandra. Wie man nicht stirbt, wenn man nur Oracle kennt

Diese Zahl wird mithilfe einer Hash-Funktion ermittelt, die auf den Partition-Key angewendet wird. Das ist die Spalte, die in der Direktive für den Primärschlüssel angegeben ist, und das ist die Spalte, die der erste und wichtigste Schlüssel in der Map sein wird. Sie bestimmt, auf welchen Knoten welche Daten gelangen. Die Tabelle wird in Cassandra nahezu mit der gleichen Syntax wie in SQL erstellt:

CREATE TABLE users (
	user_id uuid,
	name text,
	year int,
	salary float,
	PRIMARY KEY(user_id)

)

Der Primärschlüssel besteht in diesem Fall aus einer einzigen Spalte, die auch der Partitionierungsschlüssel ist.

Wie werden unsere Benutzer verteilt? Ein Teil gelangt in einen Knoten, ein anderer Teil in einen weiteren und ein Teil in den dritten. Das ergibt eine gewöhnliche Hash-Tabelle, auch Map genannt, in Python ein Dictionary, also eine einfache Key-Value-Struktur, aus der wir alle Werte lesen können, und wir können nach Schlüssel lesen und schreiben.

Cassandra. Wie man nicht stirbt, wenn man nur Oracle kennt

Select: Wenn Allow Filtering zu Full Scan wird, oder wie man es nicht machen sollte

Lass uns eine Select-Anweisung schreiben: select * from users where userid = Es funktioniert so ähnlich wie bei Oracle: Wir schreiben ein select, geben die Bedingungen an und alles funktioniert, die Benutzer werden abgerufen. Aber wenn wir beispielsweise einen Benutzer mit einem bestimmten Geburtsjahr auswählen, beschwert sich Cassandra, dass sie die Anfrage nicht ausführen kann. Denn sie weiß überhaupt nichts darüber, wie unsere Daten zum Geburtsjahr verteilt sind - sie hat nur eine Spalte als Schlüssel angegeben. Dann sagt sie: „Gut, ich kann diese Anfrage trotzdem ausführen. Fügen Sie allow filtering hinzu.“ Wir fügen die Direktive hinzu, alles funktioniert. Und in diesem Moment geschieht das Unheil.

Wenn wir mit Testdaten arbeiten, läuft alles wunderbar. Aber wenn Sie die Anfrage in der Produktion ausführen, wo wir zum Beispiel 4 Millionen Datensätze haben, dann sieht es nicht gut aus. Denn allow filtering ist eine Direktive, die es Cassandra erlaubt, alle Daten aus dieser Tabelle von allen Nodes zu sammeln, allen Rechenzentren (falls es viele in diesem Cluster gibt), und erst dann zu filtern. Das ist vergleichbar mit einem Full Scan, und es gibt kaum jemanden, der davon begeistert wäre.

Wenn wir nur Benutzer nach IDs bräuchten, wäre das akzeptabel. Aber manchmal müssen wir andere Anfragen formulieren und andere Einschränkungen auf die Abfrage anwenden. Deshalb erinnern wir uns: Das ist alles eine Map, die einen Partitionierungsschlüssel hat, aber darin gibt es eine sortierte Map.

Und sie hat auch einen Schlüssel, den wir Clustering Key nennen. Dieser Schlüssel besteht seinerseits aus den Spalten, die wir auswählen, durch den Cassandra versteht, wie die Daten physisch sortiert werden und auf jeder Node gespeichert werden. Das heißt, für einen bestimmten Partition key sagt der Clustering key, wie genau die Daten in diesen Baum eingeordnet werden und welchen Platz sie dort einnehmen.

Es ist wirklich ein Baum, da wird einfach ein Comparator aufgerufen, dem wir eine bestimmte Menge von Spalten in Form eines Objekts übergeben, und er wird ebenfalls in Form einer Aufzählung von Spalten definiert.

CREATE TABLE users_by_year_salary_id (
	user_id uuid,
	name text,
	year int,
	salary float,
	PRIMARY KEY((year), salary, user_id)

Beachten Sie die Direktive Primary key, bei der das erste Argument (in unserem Fall das Jahr) immer der Partition key ist. Er kann aus einer oder mehreren Spalten bestehen, was nicht wichtig ist. Falls es mehrere Spalten gibt, müssen diese erneut in Klammern gesetzt werden, damit der Präprozessor der Sprache versteht, dass es sich um den Primary key handelt, gefolgt von allen anderen Spalten — Clustering key. Diese werden im Comparator in der Reihenfolge übergeben, in der sie erscheinen. Das heißt, die erste Spalte ist relevanter, die zweite weniger relevant und so weiter. Wie wir für Datenklassen schreiben, zum Beispiel die Felder equals: wir listen die Felder auf und geben an, welche wichtiger und welche weniger wichtig sind. In Cassandra sind dies, vereinfacht gesagt, die Felder der Datenklasse, auf die der geschriebene equals angewendet wird.

Wir legen die Sortierung fest und bringen Einschränkungen an.

Es ist wichtig zu beachten, dass die Sortierreihenfolge (absteigend, aufsteigend, ganz egal) genau zum Zeitpunkt der Erstellung des Schlüssels festgelegt wird und später nicht mehr geändert werden kann. Sie bestimmt physisch, wie die Daten sortiert werden und wie sie gespeichert werden. Wenn der Clustering key oder die Sortierreihenfolge geändert werden müssen, muss eine neue Tabelle erstellt und die Daten dorthin übertragen werden. Mit einer bereits bestehenden Tabelle funktioniert das nicht.

Cassandra. Wie man nicht stirbt, wenn man nur Oracle kennt

Wir haben unsere Tabelle mit Benutzern gefüllt und gesehen, dass sie zunächst nach Geburtsjahr und dann innerhalb jeder Node nach Gehalt und Benutzer-ID angeordnet sind. Jetzt können wir Daten abfragen und Einschränkungen hinzufügen.

Unser funktionierender where, und, und die Benutzer stehen uns zur Verfügung, und alles ist wieder gut. Aber wenn wir versuchen, nur einen Teil des Clustering keys zu verwenden, und zwar den weniger relevanten, wird Cassandra sofort darauf hinweisen, dass es in unserem Mapping keinen Platz finden kann, wo sich dieses Objekt befindet, dessen Felder für den Comparator null sind, und wo sich dieses, das wir gerade festgelegt haben, befindet. Ich muss alle Daten von dieser Node erneut abrufen und filtern. Das ist vergleichbar mit einem Full Scan innerhalb der Node, was schlecht ist.

In jeder unklaren Situation erstelle eine neue Tabelle.

Wenn wir die Möglichkeit haben wollen, Benutzer nach ID, Alter oder Gehalt abzurufen, was tun wir dann? Nichts. Einfach zwei Tabellen verwenden. Wenn wir Benutzer auf drei verschiedene Arten abrufen müssen, werden es drei Tabellen sein. Die Zeiten, in denen wir Speicherplatz auf der Festplatte sparen konnten, sind vorbei. Das ist die günstigste Ressource. Sie kostet viel weniger als die Antwortzeit, die für den Benutzer verheerend sein kann. Es ist viel angenehmer für den Benutzer, etwas in einer Sekunde zu erhalten, als in 10 Minuten.

Wir tauschen den übermäßigen Speicherbedarf, den denormalisierten Daten, gegen die Möglichkeit, gut zu skalieren und zuverlässig zu arbeiten. Denn tatsächlich kann ein Cluster, das aus drei Rechenzentren besteht, von denen jedes fünf Knoten hat, bei einem akzeptablen Datenbewahrungsniveau (wenn garantiert nichts verloren geht) den Tod eines Rechenzentrums vollständig überstehen. Und zusätzlich zwei Knoten in jedem der beiden verbleibenden. Und erst dann beginnen die Probleme. Das ist eine ziemlich gute Redundanz, die ein paar zusätzliche SSD-Laufwerke und Prozessoren kostet. Daher müssen wir einfache Regeln kennen, um Cassandra, das überhaupt kein SQL ist und in dem es keine Beziehungen oder Fremdschlüssel gibt, zu nutzen.

Wir entwerfen alles mit dem Fokus auf die Abfrage. Es sind nicht die Daten entscheidend, sondern wie die Anwendung damit arbeitet. Wenn sie verschiedene Daten auf verschiedene Arten oder dieselben Daten auf verschiedene Arten erhalten muss, müssen wir sie so ablegen, wie es für die Anwendung günstig ist. Andernfalls fallen wir in einen vollständigen Scan hinein und Cassandra wird uns keinen Vorteil bringen.

Denormalisierte Daten sind die Norm. Vergessen wir die normalen Formen, wir haben es nicht mehr mit relationalen Datenbanken zu tun. Wenn wir etwas 100 Mal ablegen, wird es 100 Mal abgelegt. Das ist immer noch billiger, als zu stocken.

Wir wählen die Schlüssel für die Partitionierung so, dass sie sich gleichmäßig verteilen. Wir wollen nicht, dass die Hashes unserer Schlüssel in einen engen Bereich fallen. Das heißt, das Geburtsjahr im obigen Beispiel ist ein schlechtes Beispiel. Es ist gut, wenn unsere Benutzer nach Geburtsjahr gut verteilt sind, und schlecht, wenn es sich um Schüler der 5. Klasse handelt — da wird die Partitionierung nicht gut funktionieren.

Die Sortierung wird einmalig beim Erstellen des Clustering-Keys ausgewählt. Wenn sie geändert werden muss, müssen wir unsere Tabelle mit einem anderen Schlüssel umschreiben.

Und das Wichtigste: Wenn wir die gleichen Daten auf 100 verschiedene Arten abrufen müssen, haben wir 100 verschiedene Tabellen.

Quelle: habr.com

60GB SSD 8Gb DDR4