Hallo, Habr.
Ich bin Misha Butrimov und möchte Ihnen ein wenig über Cassandra erzählen. Meine Ausführungen werden insbesondere für diejenigen hilfreich sein, die noch nie mit NoSQL-Datenbanken gearbeitet haben, denn sie weist viele Besonderheiten und potenzielle Hürden auf, die es zu beachten gilt. Wenn Sie außer Oracle oder einer anderen relationalen Datenbank noch nichts gesehen haben, werden Ihnen diese Informationen das Leben erleichtern.
Was macht Cassandra so besonders? Es handelt sich um eine NoSQL-Datenbank, die ohne einen Single Point of Failure entwickelt wurde und sich gut skalieren lässt. Wenn Sie ein paar Terabyte für eine Datenbank benötigen, fügen Sie einfach Knoten zum Cluster hinzu. Möchten Sie sie auf ein weiteres Rechenzentrum ausdehnen? Fügen Sie Knoten zum Cluster hinzu. Die Verarbeitung von mehr Anfragen pro Sekunde (RPS) erhöhen? Auch hier fügen Sie zusätzliche Knoten hinzu. Es funktioniert auch in die andere Richtung.
Was kann sie noch gut? Sie ist in der Lage, viele Anfragen zu verarbeiten. Aber wie viele? 10, 20, 30, 40 Tausend Anfragen pro Sekunde? Das ist nicht viel. Auch 100.000 Schreibanfragen pro Sekunde sind nicht viel. Es gibt Unternehmen, die behaupten, 2 Millionen Anfragen pro Sekunde zu verarbeiten. Auf diese Aussage müsste man wohl glauben.
Cassandra unterscheidet sich grundlegend von relationalen Daten, was sehr wichtig ist zu beachten.
Nicht alles, was gleich aussieht, funktioniert auch gleich.
Eines Tages kam ein Kollege zu mir und fragte: „Hier ist die CQL Cassandra Query-Sprache, und sie hat ein SELECT-Statement, es gibt ein WHERE und ein AND. Ich schreibe Buchstaben, und es funktioniert nicht. Warum?“. Wenn Sie Cassandra wie eine relationale Datenbank behandeln, steht dem Ende Ihrer Bemühungen nichts im Wege. Und ich propagiere das nicht, das ist in Russland verboten. Sie werden einfach etwas falsch konzipieren.
Beispielsweise kommt ein Kunde zu uns und sagt: „Lass uns eine Datenbank für Serien oder eine Datenbank für ein Rezeptverzeichnis erstellen. Wir werden dort Gerichte mit Zutaten oder eine Liste von Serien und Schauspielern haben.“ Wir antworten erfreut: „Lass uns anfangen!“. Es sind nur zwei Bytes zu übertragen, ein paar Tabellen, und alles wird sehr schnell und zuverlässig funktionieren. Das klingt großartig, bis die Kunden wiederkommen und sagen, dass die Hausfrauen auch die umgekehrte Aufgabe lösen wollen: Sie haben eine Liste von Zutaten und möchten wissen, welches Gericht sie zubereiten können. Dann sind Sie erledigt.
Das liegt daran, dass Cassandra eine hybride Datenbank ist: Sie fungiert sowohl als Key-Value-Datenbank als auch als Wide-Column-Datenbank. Wenn wir das in Java oder Kotlin beschreiben möchten, wäre es so:
Map<RowKey, SortedMap>
Das heißt, eine Map, die eine sortierte Map beinhaltet. Der erste Schlüssel dieser Map ist der Row Key oder Partition Key – der Partitionierungsschlüssel. Der zweite Schlüssel, der auf die bereits sortierte Map verweist, ist der Clustering Key.
Um die Verteilung der Datenbank zu veranschaulichen, zeichnen wir drei Nodes. Jetzt müssen wir verstehen, wie die Daten auf die Nodes verteilt werden. Denn wenn wir alles in eine Node stopfen (es kann davon übrigens tausend, zweitausend oder fünf sein – ganz egal), spricht das nicht wirklich für Verteilung. Daher benötigen wir eine mathematische Funktion, die eine Zahl zurückgibt. Einfach eine Zahl, ein langer int, der in einen bestimmten Bereich fällt. So wird eine Node für einen Bereich zuständig sein, die zweite für einen zweiten, die n-te für den n-ten.

Diese Zahl wird durch eine Hash-Funktion generiert, die genau auf den sogenannten Partition Key angewendet wird. Dies ist die Spalte, die in der Primärschlüssel-Direktive angegeben wird, und sie ist der erste und wichtigste Schlüssel der Map. Sie bestimmt, auf welchem Knoten welche Daten landen. Die Tabelle wird in Cassandra fast mit derselben 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 Spalte, die auch der Partitionierungsschlüssel ist.
Wie werden unsere Benutzer verteilt? Ein Teil landet auf einem Knoten, ein Teil auf einem anderen und ein Teil auf einem dritten. Es entsteht eine gewöhnliche Hash-Tabelle, auch Map genannt, in Python ein Dictionary, und es handelt sich um eine einfache Key-Value-Struktur, aus der wir alle Werte lesen können, sowohl zum Lesen als auch zum Schreiben über den Schlüssel.

Select: wenn allow filtering in einen Full Scan umschlägt, oder wie man es nicht machen sollte.
Lassen Sie uns eine Auswahlabfrage schreiben: select * from users where user_id = . Es funktioniert ähnlich wie bei Oracle: Wir schreiben ein SELECT, geben Bedingungen an und alles funktioniert, die Benutzer werden abgerufen. Wenn wir jedoch zum Beispiel 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 über das Geburtsjahr verteilt sind — als Schlüssel ist nur eine Spalte angegeben. Dann sagt sie: „Gut, ich kann diese Anfrage dennoch ausführen. Fügen Sie allow filtering hinzu“. Wir fügen die Direktive hinzu, alles funktioniert. Und in diesem Moment passiert etwas Schreckliches.
Wenn wir mit Testdaten arbeiten, ist alles hervorragend. Aber wenn Sie eine Anfrage in der Produktion ausführen, wo wir zum Beispiel 4 Millionen Datensätze haben, läuft es nicht sehr gut. Denn allow filtering ist eine Direktive, die es Cassandra ermöglicht, alle Daten aus dieser Tabelle von allen Knoten zu sammeln, und erst dann zu filtern. Das ist vergleichbar mit einem Full Scan, und ich bezweifle, dass das jemand begeistert. von Rechenzentren (wenn es viele in diesem Cluster gibt), und erst dann zu filtern. Das ist vergleichbar mit einem Full Scan, und ich bezweifle, dass jemand davon begeistert ist.
Wenn wir nur Benutzer anhand ihrer IDs benötigen würden, wäre das für uns in Ordnung. Aber manchmal müssen wir andere Abfragen durchführen und andere Einschränkungen auf das Sampling anwenden. Daher erinnern wir uns: Das ist alles eine Map, die einen Partitionierungsschlüssel hat, aber darin befindet sich eine sortierte Map.
Und auch diese hat einen Schlüssel, den wir Clustering-Key nennen. Dieser Schlüssel besteht wiederum aus den Spalten, die wir auswählen, und mit dessen Hilfe Cassandra versteht, wie die Daten physisch sortiert werden und auf jedem Knoten gespeichert sind. Das bedeutet, dass der Clustering-Key für einen bestimmten Partition-Key erklärt, wie die Daten in diese Baumstruktur eingefügt werden und welchen Platz sie darin einnehmen.
Es ist tatsächlich ein Baum; es wird einfach ein Comparator aufgerufen, dem wir eine bestimmte Menge an Spalten als Objekt übergeben, und er wird ebenfalls als Aufzählung der Spalten angegeben.
CREATE TABLE users_by_year_salary_id (
user_id uuid,
name text,
year int,
salary float,
PRIMARY KEY((year), salary, user_id)
Bitte beachten Sie die Direktive "Primärschlüssel", bei der das erste Argument (in unserem Fall das Jahr) immer der Partitionsschlüssel ist. Er kann aus einer oder mehreren Spalten bestehen, das ist nicht entscheidend. Wenn es mehrere Spalten gibt, müssen diese nochmals in Klammern gesetzt werden, damit der Preprocessor der Sprache versteht, dass es sich um einen Primärschlüssel handelt, gefolgt von allen anderen Spalten – dem Clustering-Schlüssel. Diese werden dann im Comparator in der Reihenfolge übergeben, in der sie erscheinen. Das bedeutet, die erste Spalte ist die bedeutendste, die zweite ist weniger bedeutend und so weiter. So wie wir es für Datenklassen machen, zum Beispiel bei den Feldern equals: wir listen die Felder auf und geben an, welche wichtiger und welche weniger wichtig sind. In Cassandra sind dies, um es so auszudrücken, die Felder der Datenklasse, auf die der geschriebene equals angewendet wird.
Wir legen die Sortierung fest und setzen Einschränkungen.
Es ist wichtig zu beachten, dass die Sortierreihenfolge (ob absteigend, aufsteigend oder egal) in dem Moment festgelegt wird, in dem der Schlüssel erstellt wird, und dass sie danach nicht mehr verändert werden kann. Sie bestimmt physisch, wie die Daten sortiert werden und wie sie gespeichert sind. Wenn der Clustering-Schlüssel oder die Sortierreihenfolge geändert werden müssen, muss eine neue Tabelle erstellt und die Daten dorthin übertragen werden. Mit einer bereits bestehenden Tabelle ist das nicht möglich.

Wir haben unsere Tabelle mit Benutzern gefüllt und festgestellt, dass sie zunächst nach Geburtsjahr und dann innerhalb jeder Node nach Gehalt und Benutzer-ID angeordnet sind. Nun können wir Abfragen durchführen und Einschränkungen anwenden.
Unser funktionierender where, und, und wir erhalten die Benutzer, und alles ist wieder gut. Wenn wir jedoch versuchen, nur einen Teil des Clustering-Schlüssels zu verwenden, und zwar einen weniger signifikanten, wird Cassandra sofort melden, dass es in unserer Map keinen Platz finden kann, wo dieses Objekt mit diesen Feldern für den Comparator null ist und wo das neue, das wir gerade gesetzt haben, gespeichert ist. Ich müsste alle Daten von dieser Node erneut abrufen und filtern. Das entspricht einem Full Scan innerhalb der Node, was nicht ideal ist.
In jeder unklaren Situation erstellen Sie eine neue Tabelle.
Wenn wir Benutzer nach ID, Alter oder Gehalt abrufen möchten, was tun wir? Nichts. Einfach zwei Tabellen verwenden. Wenn wir Benutzer auf drei verschiedene Arten abrufen müssen, gibt es drei Tabellen. Die Zeiten, in denen wir Speicherplatz auf der Festplatte sparen wollten, sind vorbei. Speicher ist die günstigste Ressource. Er kostet viel weniger als die Antwortzeit, die für den Benutzer schädlich sein kann. Es ist für den Benutzer viel angenehmer, innerhalb von einer Sekunde etwas zu erhalten, als nach 10 Minuten.
Wir tauschen überflüssigen Speicherplatz und denormalisierte Daten gegen die Möglichkeit, gut zu skalieren und zuverlässig zu arbeiten. Tatsächlich kann ein Cluster, bestehend aus drei Rechenzentren mit jeweils fünf Nodes, bei einem akzeptablen Datenverlustniveau (bei dem sicher niemand etwas verliert) den kompletten Ausfall eines Rechenzentrums überstehen. Zudem bleiben in den beiden verbleibenden Rechenzentren jeweils zwei Nodes vorhanden. Erst dann beginnen die Probleme. Diese Art der Redundanz ist recht gut und kostet nur ein paar zusätzliche SSDs und Prozessoren. Daher ist es wichtig, einfache Regeln zu kennen, um Cassandra effektiv nutzen zu können, die definitiv kein SQL ist und keine Relationen oder Fremdschlüssel enthält.
Wir gestalten alles ausgehend von der Anfrage. Nicht die Daten sind entscheidend, sondern wie die Anwendung mit ihnen arbeitet. Wenn sie verschiedene Daten auf unterschiedliche Weise abrufen muss oder dieselben Daten auf unterschiedliche Weise benötigt, sollten wir sie so anordnen, dass es für die Anwendung bequem ist. Andernfalls laufen wir Gefahr, in einen Full Scan zu geraten, was uns keinerlei Vorteile von Cassandra bringen wird.
Die Denormalisierung von Daten ist ganz normal. Wir verzichten auf die Normalformen; wir haben keine relationalen Datenbanken mehr. Wenn wir etwas 100 Mal ablegen, liegt es 100 Mal vor. Das ist immer noch günstiger, als die Leistung zu beeinträchtigen.
Wir wählen die Schlüssel für die Partitionierung so aus, dass sie gleichmäßig verteilt sind. Wir wollen nicht, dass der Hash unserer Schlüssel in einen engen Bereich fällt. Das heißt, das Geburtsjahr im obigen Beispiel ist nicht ideal. Es ist gut, wenn unsere Nutzer nach Geburtsjahr gut verteilt sind, und schlecht, wenn es um Schüler der 5. Klasse geht – da wäre die Partitionierung nicht optimal.
Die Sortierung wird einmal beim Erstellen des Clustering-Keys festgelegt. Wenn sie geändert werden muss, müssen wir unsere Tabelle mit einem anderen Schlüssel umstellen.
Und das Wichtigste: Wenn wir die gleichen Daten auf 100 verschiedene Arten abrufen müssen, haben wir 100 verschiedene Tabellen.
Quelle: habr.com
