Aus Linux-Magazin 08/2019

Erste Schritte mit Apache Cassandra

© luckybusiness, 123RF

Ausfallsicher, wirtschaftlich, hoch skalierbar und einfach zu administrieren behauptet die Open-Source-Datenbank Apache Cassandra zu sein. Ein paar Übungen sollen zeigen, ob Cassandras Werben zu trauen ist.

Cassandra in der Praxis

Etliche interessante Anwendungsbeispiele aus der Praxis vermittelte Ende Mai die Konferenz Datastax Accelerate, die der Hersteller des kommerziellen Cassandra-Ablegers, das Unternehmen Datastax, in National Harbor im Großraum Washington veranstaltete (Abbildung 1). Dort berichtete etwa Yahoo Japan davon, wie ihr Cassandra-Cluster für Apps, Logs und statistische Daten von 100 Nodes im Jahr 2016 auf heute 4700 Nodes in 170 Clustern angewachsen ist.

Abbildung 1: Datastax-CEO Billy Bosworth während seiner Keynote auf der Konferenz Datastax Accelerate.

Abbildung 1: Datastax-CEO Billy Bosworth während seiner Keynote auf der Konferenz Datastax Accelerate.

Allerdings bereite der Betrieb einer so großen Installation auch Probleme, insbesondere mit Blick auf die nötigen Reparaturprozesse nach Fehlern und Ausfällen, die oft hängen blieben oder endlos liefen. Kurzerhand entwickelte man dort ein eigenes Node Repair Tool, das im vorgestellten Beispiel die Reparaturzeit von 970 auf 67 Minuten senken konnte.

Andere Probleme hatten die Cassandra-Betreiber bei Instagram: Ihnen bereiteten vor allem die Latenzen in ihrem geografisch über Kontinente verteilten Cluster Kopfzerbrechen. So schufen sie sich mit AKKIO ein Tool, das versucht die Daten immer in geografischer Nähe des Anfragers zu speichern. Bei rund 80 Prozent der Daten gelingt das. Für Daten, die aus allen geografischen Regionen abgefragt werden – beispielsweise die Bilder von Prominenten –, existiert eine eigene Partition, die überall vorhanden ist. Der Cross-Partition-Traffic verringerte sich so um 20 Prozent, was bei dem Volumen, das Instagram erzeugt, eine Menge Geld spart.

Vor einer anderen Herausforderung stand der Großhändler Metro, der mehr als 750 Geschäfte in 35 Ländern betreibt und rund 150000 Angestellte beschäftigt. Wegen des bevorstehenden Brexit migrierte er 500 Cassandra-Knoten in 91 Clustern binnen weniger Monate im laufenden Betrieb und ohne Downtime von England nach Zentraleuropa. Dabei wurde die in Cassandra integrierte Replikation ausgenutzt.

Mehrfach Gegenstand von Vorträgen waren zudem der Betrieb von Cassandra in Containern unter Kubernetes oder in verschiedenen Public Clouds, die Integration mit Werkzeugen etwa für Analytics, Business Intelligence oder künstliche Intelligenz, der Entwurf datengetriebener Applikationen, die Caassandra-Administration oder die Sicherheit der Datenbank.

Zu den größten Ankündigungen der Konferenz zählte Datastax Constellation. Die Cloud-Datenplattform ermöglicht die einfache und schnelle Entwicklung und Bereitstellung von Cloudanwendungen. Die Plattform wird bald mit zwei neuen Cloudservices starten: Datastax Apache Cassandra as a Service und Datastax Insights. Ersteres ermöglicht flexible Skalierbarkeit bei Cassandra-Clustern und eine verbrauchsbasierte Abrechnung unterstützt durch die Stabilitäts- und Performance-Erweiterungen von Datastax Enterprise. Letzteres dient dem Monitoring des Clusters und gibt Tuning-Empfehlungen.

Einst setzte sich Facebook das Ziel, seinen Nutzern eine Suchmöglichkeit in ihrer Inbox zu bieten. Dafür galt es, einen umgekehrten Index aller Messages zu speichern. Eine Datenbank jedoch, die das sicher und kostengünstig gekonnt hätte, gab es zu dieser Zeit nicht. Die bis dahin benutzte MySQL-Datenbank war bei einer Größe von 7 TByte für damals über 100 Millionen Nutzer an ihre Grenzen gelangt.

Also machten sich die beiden Facebook-Programmierer Avinash Lakshman (einer der Autoren von Amazons Dynamo) und Prashant Malik daran, ein völlig neues Storagesystem zu entwickeln, das es mit der Riesenmenge an Daten und ihrer hohen Wachstumsrate aufnehmen konnte und dabei ausfallsicher, schnell und wirtschaftlich sein sollte [1]. Heraus kam Cassandra [2].

Facebook veröffentlichte Cassandra im Jahr 2008 unter einer Open-Source-Lizenz. Darauf aufbauend gründeten zwei ehemalige Rackspace-Mitarbeiter 2010 eine eigene Firma, um 2011 einen kommerziellen Cassandra-Ableger ins Rennen zu schicken, der heute Datastax Enterprise (DSE) heißt. Er verfügt über etliche zusätzliche Features. DSE wird heute von einigen der größten Unternehmen weltweit eingesetzt, darunter Netflix, Microsoft, Facebook und dessen Tochter Instagram, Deloitte, The Home Depot oder Walgreens.

Was ist Cassandra?

Cassandra ist eine in Java implementierte, fehlertolerante, verteilte und spaltenorientierte NoSQL-Datenbank des Zuschnitts Wide Column Store. Diese Spielart nutzt so genannte Column Families, Gruppen zusammengehöriger Spalten, die relationalen Tabellen ähneln. Anders als beim relationalen Modell lassen sich hier aber jederzeit Spalten pro Zeile hinzufügen oder wieder löschen.

Column Families können ihrerseits wiederum Gruppen von Spalten enthalten, dann heißen sie Super Column Families. Dem Begriff der Datenbank im relationalen Sinn kommt im Universum des Wide Column Store der Keyspace am nächsten, der mehrere Column Families sowie Funktionen oder Views enthalten kann. Allgemein gibt es einen Keyspace pro Applikation. Der Cluster wiederum beherbergt dann die Keyspaces.

Cassandra benutzt jeden der Knoten im Cluster, die zusammen einen logischen Ring bilden und auch über verschiedene Rechenzentren verteilt sein können, zum Schreiben und Lesen. Die Rollen Master und Slave gibt es hier nicht, die Knotenrechner dürfen verhältnismäßig billige Hardware von der Stange sein.

Ein konsistenter Hash-Algorithmus ermittelt für jedes Datum seine Position auf dem Ring, durchläuft den Ring im Uhrzeigersinn, bis er den ersten Knoten findet, dessen Position größer als die des Datums ist, und bestimmt diesen Knoten als Verantwortlichen für das Datum. Er, der so genannte Koordinator, übernimmt auch die Replikation des Datums auf eine konfigurierbare Anzahl weiterer Knoten. Dabei kann er einstellbaren Policies folgen und etwa darauf achten, dass die Replikation immer auch Knoten in entfernten Rechenzentren einbezieht.

Da jeder Knoten nur für den Abschnitt des Ringes bis zu seinem Vorgänger Verantwortung trägt, betrifft ein Ausfall oder das Hinzukommen eines Knotens niemals den gesamten Ring, sondern nur die unmittelbaren Nachbarn des betroffenen Knotens.

Auch Cassandra kann allerdings nicht alle wünschenswerten Ziele gleichzeitig erreichen. Bekanntlich besagt ja das CAP-Theorem, dass in einem verteilten System immer nur zwei der drei Anforderungen Consistency C (Konsistenz), Availability A (Verfügbarkeit) und Partition Tolerance P (Ausfalltoleranz) zu garantieren sind (Abbildung 2).

Abbildung 2: Nur zwei der drei Eigenschaften sind zu garantieren.

Abbildung 2: Nur zwei der drei Eigenschaften sind zu garantieren.

Cassandra verbürgt sich für A und P und bietet dafür nur letztliche Konsistenz (Eventual Consistency). Es kann also vorkommen, dass zeitgleiche identische Abfragen auf zwei verschiedenen Knoten zu unterschiedlichen Ergebnissen führen, weil zu schreibende Daten nicht überall sofort gespeichert wurden. Stattdessen wird die Schreiboperation bestätigt, wenn ein Quorum der Replikas, an die die Anforderung weitergeleitet wurde, die Operation bestätigt hat. Die Ergebnisse sind dann zwar letzten Endes irgendwann wieder konsistent, aber unter Umständen nicht zum Abfragezeitpunkt. Damit muss der Anwender leben.

Die Anzahl der Knoten lässt sich jederzeit aufstocken, um Engpässen vorzubeugen, und es kann immer eine Anzahl Knoten ausfallen, ohne die Funktion der Datenbank zu beeinträchtigen.

Buch führen über Bücher

Zu Beginn soll eine einfache Tabelle als Beispiel dienen, die sich alle Bücher merkt, die das Linux-Magazin seit Anfang 2018 auf seiner Bücherseite rezensiert hat. Pro Titel speichert die Tabelle die ISBN, das Erscheinungsjahr, den Titel, den Buchverlag, die Namen von bis zu drei Autoren, die Anzahl der Auflagen und die Nummer der Ausgabe, in der die Rezension erschienen ist. Gäbe es eine solche Tabelle in MySQL, so ließe sie sich jetzt sehr einfach zum Beispiel nach der Ausgabenummer sortieren (Listing 1). Beinahe ebenso leicht wäre auch eine etwas komplexere Fragestellung, wie die nach der Anzahl der rezensierten Titel pro Verlag (Listing 2) zu lösen.

Listing 1

Sortierte Titelliste

01 +-------------------------+--------------------------------+---------+-------+
02 | Autor1                  | Titel                          | Verlag  |Ausgabe|
03 +-------------------------+--------------------------------+---------+-------+
04 | Tim Philipp Schäfers    | Hacking im Web                 |Franzis  | 18/01 |
05 | Richard Eisenmenger     | Nur noch dieses Level!         |Rheinwerk| 18/01 |
06 | Joachim Zuckarelli      | Statistik mit R                |O'Reilly | 18/02 |
07 | Sebastian Erlhofer      | Website-Konzeption und Relaunch|Rheinwerk| 18/02 |
08 [...]

Listing 2

Rezensionen pro Verlag

01 # SELECT Verlag, COUNT(*) AS 'Anzahl' FROM buecher GROUP BY Verlag ORDER BY Anzahl DESC;
02
03 +-----------+--------+
04 | Verlag    | Anzahl |
05 +-----------+--------+
06 | Rheinwerk |     10 |
07 | O'Reilly  |      6 |
08 | Wiley     |      5 |
09 | Hanser    |      5 |
10 | Mitp      |      4 |
11 | C.H. Beck |      2 |
12 | Tintal    |      1 |
13 | Franzis   |      1 |
14 | Dpunkt    |      1 |
15 | Apress    |      1 |
16 +-----------+--------+

Mit Cassandra ist dergleichen auf direktem Weg nicht möglich. Zwar verfügt die Datenbank über eine an SQL angelehnte Abfragesprache namens CQL, doch ist die stark auf den Einsatz in einer partitionierten, verteilten NoSQL-Datenbank zugeschnitten, die mit Petabytes an Daten umgehen können soll. Sprachkonstrukte, die zum Beispiel voraussetzen, dass ein Filterkriterium mit jeder einzelnen Zeile zu vergleichen ist, könnten da zu unkalkulierbaren Performanceproblemen führen. Sie sind deshalb nur unter Vorbehalt möglich. Subqueries gibt es gleich gar nicht.

Ebenso fehlt die Suche mit Wildcards. Noch gravierender: Auch Joins, die in der relationalen Welt Verbindungen zwischen Tabellen knüpfen, sind nicht vorgesehen. Im MySQL-Fall hätte der Datenbank-Entwerfer beispielsweise die Namen der Verlage in eine eigene Tabelle ausgelagert, um sie in der Haupttabelle nicht redundant zu speichern. Dort hätte er nur eine ID abgelegt, die auf die Verlagsnamen-Tabelle verweist. Das geht mit Cassandra nicht. Stattdessen nimmt man hier die redundante Datenspeicherung bewusst in Kauf. Alles, was eine Abfrage ermitteln will, muss sich in einer Column Family befinden.

Zudem sind auch die aus SQL bekannten Aggregatfunktionen wie »AVG()«, »SUM()«, »COUNT()«, »MAX()« oder »MIN()« per se nicht vorhanden. Sie ließen sich zwar auch als benutzerdefinierte Funktionen nachrüsten, üblicher ist es aber, anstelle von »COUNT()« eine gesonderte Tabelle anzulegen, die einen Zähler enthält, den jedes »INSERT« oder »DELETE« aktualisiert.

Cassandra verlangt wegen dieser Besonderheiten nach einer im Vergleich zu relationalen Datenbanken völlig anderen Technik für die Datenmodellierung. Während der Datenbank-Entwickler in der relationalen Welt von der Tabelle als Grundelement ausgeht und Beziehungen zwischen Tabellen durch Joins ausdrückt, ist der Ausgangspunkt bei Cassandra die Abfrage.

Oft wird man eine Tabelle so entwerfen, dass sie genau eine Abfrage optimal unterstützt. Wo es nicht möglich ist, zusammengehörige Daten in einer Tabelle zusammenzuführen, muss die Applikationslogik die Verbindung herstellen.

Für das Beispiel der Buchbesprechungen könnte das bedeuten, dass der Entwerfer zusätzlich noch eine Tabelle pro Verlag mit einer Zählspalte anlegt, um die Anzahl der rezensierten Titel dieses Verlags zu ermitteln, ohne auf »COUNT()« angewiesen zu sein. Die Applikation wäre dann dafür verantwortlich, bei jedem Einfügen oder Löschen auch die Zähltabelle zu aktualisieren.

Wenn eine Tabelle – wie im Beispiel – nur wenige Daten enthält, klappt natürlich auch noch das Filtern. Der erste Versuch wird aber in einer Fehlermeldung enden, die auf möglicherweise schwer kalkulierbare Auswirkungen auf die Performance verweist. Ein Anwender, der sich sicher ist, dass dies in seinem Fall nicht eintreten kann, muss daher das Filtern mit »ALLOW FILTERING« explizit erlauben (Listing 3).

Listing 3

ALLOW FILTERING

01 # SELECT Autoren, Titel FROM titles WHERE Verlag='Mitp' ALLOW FILTERING;
02
03  autoren                              | titel
04 --------------------------------------+-----------------------------------------
05                     {1: 'Ralf Jesse'} | Embedded Linux mit Raspberry Pi und Co.
06  {1: 'F. Chollet', 2: 'J.J. Allaire'} |           Deep Learning mit R und Keras
07             {1: 'Aditya Y. Bhargava'} |                    Algorithmen kapieren
08               {1: 'Winfried Seimert'} |                                Inkscape

Bücher können eine unterschiedliche Anzahl Autoren haben. Im relationalen Modell – und auch in Cassandra – ließe sich nun eine Anzahl Felder für Autoren reservieren, etwa drei: »Autor1«, »Autor2« und »Autor3«. Hat das Buch nur einen Autor, enthalten »Autor2« und »Autor3« Nullwerte. Hat das Buch aber vier Autoren, müsste einer entfallen.

In Cassandra existiert eine elegante Lösung für dieses Problem: Maps. Das sind Felder, die eine beliebige Anzahl Schlüssel-Wert-Paare derselben Bedeutung aufnehmen können. Damit ließe sich für die Autoren ein Feld so definieren wie in Listing 4, das auch Inserts in eine solche Tabelle und ein Select demonstriert:

Listing 4

Maps

01 # CREATE TABLE titles
02 (
03   isbn   TEXT,
04   jahr   INT,
05   titel  TEXT,
06   verlag TEXT,
07   autoren MAP<INT,TEXT>,
08   auflage INT,
09   ausgabe TEXT,
10   PRIMARY KEY(verlag, ausgabe, isbn)
11 );
12
13 # INSERT INTO titles(isbn, jahr, titel, verlag, autoren, auflage, ausgabe) VALUES('978-3836244091', 2017, 'Nur noch dieses Level!', 'Rheinwerk', {1 : 'Richard
   Eisenmenger'}, 1, '18/01');
14
15 INSERT INTO titles(isbn, jahr, titel, verlag, autoren, auflage, ausgabe) VALUES('978-3836262477', 2018, 'Objektorientierte Programmierung', 'Rheinwerk', {1 :'Bernhard Lahres', 2 : 'Gregor Rayman', 3 : 'Stefan Strich'}, 4, '18/09');
16
17 # SELECT titel FROM titles WHERE autoren CONTAINS 'Stefan Strich' ALLOW FILTERING;
18
19  titel
20 ----------------------------------
21  Objektorientierte Programmierung

Alles in Ordnung

Ein weiterer Unterschied, der allen Bauchweh bereitet, die an SQL gewöhnt sind, betrifft die Sortierung. Dazu ist zunächst ein Blick auf die verschiedenen Schlüssel einer Cassandra-Tabelle nötig:

  • Wird als Primary Key nur eine einzelne Spalte definiert, so ist dieser Schlüssel zugleich der so genannte Partition Key. Er bestimmt die Lokalität der Daten, er entscheidet also darüber, auf welchem Node der verteilten Datenbank die Daten landen. Dazu ermittelt, wie schon erwähnt, ein Algorithmus anhand eines Hashwerts, der aus dem Partition Key berechnet wird, ein Token, das einem bestimmten Node fest zugeordnet ist.
  • Bei einem zusammengesetzten Primary Key ist der Partition Key die erste Komponente, also der erste Ausdruck in der Schlüsseldefinition.
  • Dabei darf der Partition Key seinerseits auch aus mehreren Spalten bestehen, die in diesem Fall in der Definition einzuklammern sind.
  • Die zweite Komponente des zusammengesetzten Primary Key heißt dann Clustering Key, er bestimmt die Sortierreihenfolge innerhalb der Partition. Nur anhand dieses Sortierkriteriums lassen sich Daten geordnet zurückliefern.

Deshalb ist im Bedarfsfall bereits beim Anlegen der Tabelle der Primary Key so zu konstruieren, dass er aus mehreren Teilen besteht und sein zweiter Teil, der Clustering Key, die später gewünschte Sortierreihenfolge reflektiert. Zusätzlich ist »ORDER BY« nur verwendbar, wenn eine EQ- oder IN-Bedingung den Partition Key einschränkt.

Um Ähnliches zu erhalten wie im MySQL-Beispiel (Listing 1), wäre zunächst der Primary Key so zu definieren, dass die Ausgabe-Spalte zum Clustering Key wird:

PRIMARY KEY (verlag, ausgabe, isbn)

Danach müsste zum Beispiel eine EQ-Bedingung den Partition Key (hier die Spalte »verlag«) einschränken – dann ließe sich das Ergebnis nach der Ausgabenummer sortieren (Listing 5),

Listing 5

Sortierte Ausgabe

01 # SELECT autoren, titel, verlag, ausgabe FROM titles WHERE verlag='Hanser' ORDER
     BY ausgabe ALLOW FILTERING;
02
03  autoren               | titel                                   | verlag | ausgabe
04 -----------------------+-----------------------------------------+--------+---------
05 {1: 'Dirk Louis',
06  2: 'Peter Müller'}    |Java                                     | Hanser | 18/08
07 {1: 'Herbert Dowalil'} |Grundlagen des modularen Softwareentwurfs| Hanser | 18/08
08 {1: 'Jonas Freiknecht',
09  2: 'Stefan Papp'}     |Big Data in der Praxis                   | Hanser | 18/10
10 {1: 'Jörg Frochte'}    |Maschinelles Lernen                      | Hanser | 18/11
11 {1: 'Heiko Kalista'}   |Python 3                                 | Hanser | 19/05

Auf jeden Fall muss sich der Cassandra-Anwender also bereits beim Datenbankentwurf gründlich überlegen, welche Abfragen die Datenbank später beantworten können soll. Die Freiheit, wie beim relationalen Modell erst im Nachhinein über die Auswertung nachzudenken, gibt es hier nicht.

Mehr Daten und ein Cluster

Ein zweites Beispiel soll einen typischeren NoSQL-Anwendungsfall illustrieren: Diesmal sind Messwerte und Zeitstempel zu speichern. Außerdem soll anstelle einer einzelnen Instanz von Cassandra ein kleiner Cluster zum Einsatz kommen, der sich für Demozwecke mit Hilfe von Docker aufbauen lässt. Die Messdaten sind hier Laufzeiten eines Ping auf die Linux-Magazin-Webseite.

Dafür führt Cron ein einfaches Shellskript minütlich aus, das im Wesentlichen lediglich aus einer einzigen Zeile besteht:

/bin/ping -c1 -D -W5 www.linux-magazin.de | /home/jcb/Cassandra/pingtest.pl

Unter Umständen muss es auch noch ein paar Environmentvariablen für Perl setzen. Das Perl-Skript »pingtest.pl« (Listing 6) nimmt die Ausgaben von »ping« entgegen, extrahiert mit einem regulären Ausdruck die Werte und speichert sie in der Cassandra-Datenbank. Im Ernstfall wäre hier eine Fehlerbehandlung nötig, an dieser Stelle soll es aber so einfach und simpel wie möglich bleiben.

Listing 6

pingtest.pl

01 #!/usr/bin/perl
02 use Cassandra::Client;
03
04 my $client=Cassandra::Client->new(
05     contact_points=>['172.21.0.2', '172.21.0.4', '172.21.0.5'],
06     username => 'admin',
07     password => '********',
08     keyspace => 'pingtest'
09 );
10
11 $client->connect;
12
13 foreach my $line ( <STDIN> ) {
14 if($line =~ /\[(\d+.\d+)\](.*)time=(\d+\.\d+)/) {
15     $client->execute("INSERT INTO pingtime (tstamp, tvalue) VALUES(?, ?)",
16     [$1*1000, $3],
17     { consistency => "one" });
18   }
19 }
20
21 $client->shutdown;

Der Zeitstempel, den »ping« dank der Option »-D« ausgibt, repräsentiert die Unix-Zeit, also die Anzahl der Sekunden seit dem 1. Januar 1970, 0:00 Uhr. Er wird hier mit 1000 multipliziert, um einen Wert in Millisekunden zu erhalten, wie ihn Cassandra braucht. Zu bedenken wäre weiter, dass er eine Zeitangabe in UTC darstellt, weshalb es zur MESZ eine Zeitdifferenz von zwei Stunden gibt (MESZ=UTC+2).

Wenn der Admin den Cluster später mehrmals anhält und wieder anlaufen lässt, dann können sich übrigens dabei die internen IP-Adressen der Knotenrechner ändern. Passiert das, sind die Adressen im Perl-Skript anzupassen, ansonsten findet es keinen Datenbankserver mehr. Das Skript verwendet das Modul Cassandra::Client, das der Anwender vorher am besten aus dem Comprehensive Perl Archive Network (CPAN) installiert. Alle Abhängigkeiten – die sind in diesem Fall umfangreich – löst die Installationsroutine dann automatisch auf.

Am einfachsten startet man dazu eine interaktive CPAN-Shell mit

perl -MCPAN -e shell

und gibt dort ein: »install Cassandra:: Client«. Außerdem wird ein C-Compiler benötigt.

Die Column Family, die Zeitstempel und Ping-Laufzeit speichern soll, ist einfach:

CREATE TABLE pingtime (tstamp timestamp, tvalue float, PRIMARY KEY(tvalue, tstamp) ) WITH default_time_to_live = 259200 AND CLUSTERING ORDER BY (tstamp DESC);

So kommen zwar noch lange keine Terabytes an Daten zusammen, aber immerhin knapp 1500 Einträge pro Tag. Wer die Menge begrenzen will, könnte – wie im Beispiel oben – für die gesamte Tabelle einen TTL-Wert (in Sekunden) vorgeben. Cassandra löscht die Daten dann automatisch nach Ablauf dieser Frist, im Beispiel also nach drei Tagen. Statt für die ganze Tabelle, ist die Aufbewahrungszeit auch pro »INSERT« definierbar. In diesem Fall wäre an das entsprechende Statement ein »USING TTL 259200« anzuhängen.

Das ist ein weiteres nützliches Feature, das einen möglicherweise dazu bewegen könnte, auch dann über einen Einsatz von Cassandra nachzudenken, wenn es sich nicht gleich um unüberschaubare Datenmengen handelt.

Ein echtes Highlight von Cassandra kam aber bis jetzt nur am Rande vor: Für Clustering und Replikation – und damit für eine hochperformante und ausfallsichere Datenbank – muss der Admin nicht allzu viel tun. Das meiste macht die Datenbank automatisch.

In der Praxis

Es empfiehlt sich, die folgenden Schritte nicht unbedingt auf einem schwachbrüstigen Laptop nachzuvollziehen – der könnte nämlich mindestens zeitweise schwer bedienbar sein, wenn ihn die anfallende Last überfordert.

Was muss der Admin tun, um einen kleinen Cassandra-Cluster auf Basis von Docker einzurichten? Er muss zuerst natürlich Docker installieren und zusätzlich Docker Compose, ein Tool für die Verwaltung von Multi-Container-Applikationen. Das würde alles auch zum Beispiel mit Docker Swarm oder Kubernetes funktionieren, und tatsächlich müsste er zu einer dieser Alternativen greifen, wenn der Cluster über verschiedene physische Hosts verteilt laufen soll – aber dieses Beispiel soll sich bewusst auf einen Host beschränken, das hält den Konfigurationsaufwand gering und die Installation einfach.

Zum Beispiel unter Ubuntu gelingt die Installation der nötigen Software am einfachsten über die Paketverwaltung. Danach ist die Docker-Steuerung auch über die TCP-Schnittstelle statt nur über das lokale Socketfile zu ermöglichen. Hierfür editiert der Admin als Root »/lib/systemd/system/docker.service« und setzt ein Kommentarzeichen vor:

ExecStart=/usr/bin/dockerd -H fd://

Danach fügt er stattdessen ein:

ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2375

Es folgt ein »systemctl daemon-reload«, dann »service docker restart«. Danach kann er sich als Root wieder ausloggen und unter einer unprivilegierten Benutzerkennung den Erfolg testen:

curl http://localhost:2375/version

Im Anschluss sollte der Anwender für die Clusterkonfiguration irgendwo in seinem Homeverzeichnis ein Unterverzeichnis »cassandra« erzeugen und darin eine Datei »docker-compose.yml« mit dem Inhalt aus Listing 7 anlegen. Es enthält alle Angaben, die Docker braucht, um die richtigen Images zu finden, herunterzuladen, auszupacken, auf der gewünschten Anzahl Knoten zu installieren, eine Netzwerkverbindung zwischen den Knoten einzurichten sowie Storagevolumes anzulegen.

Listing 7

docker-compose.yml

01 version: '3'
02 services:
03     # Konfiguration für den Seed-Knoten DC1N1
04     # Der Name könnte etwa für Data Center 1,
         Node 1 stehen
05     DC1N1:
06         image: cassandra:3.10
07         command: bash -c 'if [ -z "$$(ls -A /var/lib/cassandra/)" ] ; then sleep 0; fi && /docker-entrypoint.sh cassandra -f'
08         # Netzwerk für die Kommunikation der Knoten untereinander
09         networks:
10             - dc1ring
11         # Mapped das Volume auf ein lokales  Verzeichnis.
12         volumes:
13             - ./n1data:/var/lib/cassandra
14         # Umgebungsvariable für die  Cassandra-Konfiguration.
15         # CASSANDRA_CLUSTER_NAME muss auf  allen Knoten identisch sein.
16         environment:
17             - CASSANDRA_CLUSTER_NAME=Test Cluster
18             - CASSANDRA_SEEDS=DC1N1
19         # Exponierte Ports für die Kommunikation im Cluster
20         expose:
21             # Intra-node communication
22             - 7000
23             # TLS intra-node communication
24             - 7001
25             # JMX
26             - 7199
27             # CQL
28             - 9042
29             # Thrift service
30             - 9160
31         # empfohlene Cassandra
             Ulimit-settings
32         ulimits:
33             memlock: -1
34             nproc: 32768
35             nofile: 100000
36
37     DC1N2:
38         image: cassandra:3.10
39         command: bash -c 'if [ -z "$$(ls -A /var/lib/cassandra/)" ] ; then sleep 60; fi && /docker-entrypoint.sh cassandra -f'
40         networks:
41             - dc1ring
42         volumes:
43             - ./n2data:/var/lib/cassandra
44         environment:
45             - CASSANDRA_CLUSTER_NAME=Test Cluster
46             - CASSANDRA_SEEDS=DC1N1
47         depends_on:
48               - DC1N1
49         expose:
50             - 7000
51             - 7001
52             - 7199
53             - 9042
54             - 9160
55         ulimits:
56             memlock: -1
57             nproc: 32768
58             nofile: 100000
59
60     DC1N3:
61         image: cassandra:3.10
62         command: bash -c 'if [ -z "$$(ls -A /var/lib/cassandra/)" ] ; then sleep 120; fi && /docker-entrypoint.sh cassandra -f'
63         networks:
64             - dc1ring
65         volumes:
66             - ./n3data:/var/lib/cassandra
67         environment:
68             - CASSANDRA_CLUSTER_NAME=Test Cluster
69             - CASSANDRA_SEEDS=DC1N1
70         depends_on:
71               - DC1N1
72         expose:
73             - 7000
74             - 7001
75             - 7199
76             - 9042
77             - 9160
78         ulimits:
79             memlock: -1
80             nproc: 32768
81             nofile: 100000
82
83     # Eine webbasierte GUI für das Verwalten der Container.
84     portainer:
85         image: portainer/portainer
86         networks:
87             - dc1ring
88         volumes:
89             - /var/run/docker.sock:/var/run/docker.sock
90             - ./portainer-data:/data
91         # Zugriff auf das Web-Interface vom Host aus via
92         # http://localhost:10001
93         ports:
94             - "10001:9000"
95 networks:
96     dc1ring:

Das Compose-File definiert vier Services als Komponenten der Applikation. Jeder Service verwendet genau ein Docker-Image. Drei der Services sind Cassandra-Knoten, der vierte – Portainer – stellt ein einfaches Web-GUI zur Steuerung der Servicecontainer bereit.

Der Abschnitt »environment« setzt einige Environmentvariablen, die auf dem jeweiligen Knoten später gleichnamige Einstellungen im zentralen Cassandra-Konfigurationsfile »/etc/cassandra/cassandra.yaml « überschreiben. Auf diese Weise ist es möglich, mit nur einem Image für alle Cassandra-Knoten auszukommen und doch jeden Knoten einzeln zu konfigurieren. Neben den hier gezeigten gibt es noch eine ganze Reihe weiterer dieser Variablen, die bei Bedarf verwendbar wären. Details finden sich in der Dokumentation des Cassandra-Image [3].

Auf den Namen des Image folgt im Compose-File ein relativ kryptisches Shellkommando. Es dient dazu, beim allerersten Start, wenn das Datenverzeichnis von Cassandra noch leer ist, die Knoten nacheinander im Abstand von einer beziehungsweise zwei Minuten zu starten. Würde der Anwender das beim ersten Start gleichzeitig versuchen, würde es nämlich nicht funktionieren.

Nun ist auf dem Host noch die Environmentvariable »$DOCKER_HOST« zu setzen

export DOCKER_HOST=localhost:2375

– und schon lässt sich der Installationsprozess, der eine kleine Weile dauern kann, aus dem »cassandra«-Verzeichnis folgendermaßen starten:

docker-compose up -d

Portainer

Sobald alles abgearbeitet ist, lässt sich unter »localhost:10001« das GUI von Portainer im Browser aufrufen (Abbildung 3). Es bietet eine Übersicht über die laufenden Container nebst ihrem Status, die Images, auf denen sie beruhen, die Netzwerke und Volumes.

Abbildung 3: Die Startseite des GUI bietet eine &Uuml;bersicht &uuml;ber die Komponenten.

Abbildung 3: Die Startseite des GUI bietet eine Übersicht über die Komponenten.

Von hier aus kann sich der Anwender zu spezielleren Ansichten weiterklicken. Dabei gelangt er beispielsweise zu einer Übersichtsseite über die Container, die sich von dort aus starten, stoppen, pausieren oder aus einer Pause zurückholen lassen (Abbildung 4). Genauso lassen sich hier Container entfernen oder hinzufügen. Außerdem verrät die Übersicht die IP-Adressen der einzelnen Container im internen Netz. Der Admin kann von hier aus in das Container-Log schauen (Abbildung 5), sich ein paar Performance-Statistiken pro Container anzeigen lassen, die Konfiguration des Containers inspizieren oder sich in einer Shell auf dem Container einloggen.

Abbildung 4: Die Container-&Uuml;bersichtsseite. Das letzte Icon der &raquo;Quick Actions&laquo; &ouml;ffnet &uuml;brigens die Shell auf dem Container.

Abbildung 4: Die Container-Übersichtsseite. Das letzte Icon der »Quick Actions« öffnet übrigens die Shell auf dem Container.


Abbildung 5: Blick ins Log eines Cassandra-Containers aus dem Web-GUI Portainer heraus.

Abbildung 5: Blick ins Log eines Cassandra-Containers aus dem Web-GUI Portainer heraus.

Dringt er noch tiefer in die Details eines einzelnen Containers vor, ist es möglich, aus dem laufenden Container ein neues Image zu erzeugen, um beispielsweise Änderungen in der Datenbankkonfiguration dauerhaft zu machen. Auch die Zugriffsrechte auf den Container sind hier einstellbar.

Auf der Cluster-Spielwiese

Als letzten Test, ob alles auch wie gewünscht funktioniert, kann sich der Admin via Portainer auf einem der Knotenrechner eine Bash-Shell öffnen und dort eingeben:

nodetool status

Nodetool ist ein umfangreiches Werkzeug zum Verwalten, Monitoren und Reparieren des Cassandra-Clusters. Die Ausgabe des Unterkommandos »status« soll im Prinzip so aussehen, wie die Abbildung 6 zeigt. Es müssen alle drei Nodes erscheinen, der Status sollte jeweils Up »U« und Normal »N« sein und jedem Knoten sollte ein etwa gleich großer Anteil an den Daten gehören.

Abbildung 6: Mit dieser Ausgabe signalisiert das Kommando &raquo;nodetool status&laquo;, dass alles in Ordnung ist.

Abbildung 6: Mit dieser Ausgabe signalisiert das Kommando »nodetool status«, dass alles in Ordnung ist.

Nun ist der Admin endlich eingeladen, ein wenig mit dem Cluster herumzuspielen. Dazu loggt er sich vom Docker-Host etwa auf dem Knoten DC1N1 mit dessen IP-Adresse und dem für »cqlsh« in der Konfiguration reservierten Port – das ist der Default-Port 9042 – ein:

jcb@hercules:~$ cqlsh -uadmin 172.21.0.2 9042

Das setzt allerdings neben dem Cluster eine lokale Cassandra-Installation voraus, die die CQL-Shell mitbringt. Alternativ kann er sich auch via Portainer eine Linux-Shell auf einem der Knoten öffnen und dann dort »cqlsh« starten. Danach kann er den Keyspace für das Pingzeiten-Beispiel im Drei-Knoten-Cluster jetzt mit drei Replicas anlegen:

CREATE KEYSPACE pingtest WITH replication = {'class':'SimpleStrategy','replication_factor':3};

HA ohne Mühe

Ohne dass der Admin sonst noch etwas dafür tun müsste, wird die Datenbank nun Kopien jeder Zeile im Keyspace »pingtest« auf jedem Knoten im Cluster speichern. Passend dazu lässt sich der Consistency Level entweder interaktiv pro Session oder über die Query in »pingtest.pl« versuchsweise auf verschiedene Werte einstellen.

Die Einstellung »one« etwa bedeutet, dass nur ein Knoten die Schreib- oder Leseoperation bestätigen muss, damit die Transaktion als bestätigt gilt. Bei »two« oder »three« müssen es entsprechend mehr Knoten sein, bei »all« alle. Bei »quorum« muss eine Mehrheit der Knoten in allen Rechenzentren, über die der Cluster verteilt ist, die Operation bestätigen, bei »local_qourum« reicht eine konfigurierbare Mindestzahl im selben Rechenzentrum, in dem sich der Koordinator-Knoten für diese Zeile befindet.

Auf diese Weise erreicht Cassandra eine so genannte Tunable Consistency, was bedeutet, dass der Anwender bei jeder einzelnen Abfrage vorgeben kann, was ihm wichtiger ist: Bei einer IoT-Anwendung reicht vielleicht die Bestätigung eines Knotens, dafür gewinnt die Datenbank an Performance, weil sie auf keine weiteren Knoten zu warten braucht, die das Ergebnis bekräftigen. Bei einer Finanzapplikation möchte der Anwender dagegen möglicherweise eher auf Nummer sicher gehen und sich das Ergebnis der Operation lieber von einer Mehrheit der Knoten bestätigen lassen. In diesem Fall nimmt er in Kauf, dass dies ein paar Millisekunden länger dauert.

Hat der Administrator im verteilten Keypace »pingtest« die oben gezeigte Tabelle »pingtime« eingerichtet und den schon beschriebenen Cronjob aktiviert, sodass Daten einlaufen, dann sollte er jetzt auf allen Knoten dieselben Daten aus der Tabelle abrufen können. Wenn das so ist, dann läuft die Replikation wunschgemäß.

Nun könnte der DBA via Portainer versuchsweise einzelne Knoten herunterfahren. Mit nachfolgenden Selects wird er – je nach Consistency-Einstellung – auch dann eine Fehlermeldung provozieren, wenn er sich an einen überlebenden Knoten wendet, der die Daten eigentlich zur Verfügung hat, aber keine Mitstreiter in erforderlicher Anzahl mehr findet, die das Ergebnis bestätigen können.

Aktiviert der Admin einen außer Betrieb genommenen Knoten später wieder, ist im Idealfall gar nichts zu tun. Cassandra kümmert sich selbstständig um die Resynchronisation. Voraussetzung dafür ist, dass in der zentralen Cassandra-Konfigurationsdatei »cassandra.yaml« die Variable »hinted_handoff_enabled« auf »true« gesetzt ist. Dann speichert Cassandra die vom zeitweilig ausgefallenen Knoten verpassten Schreiboperationen in Form so genannter Hints und holt sie automatisch nach, sobald der Knoten wieder verfügbar ist. Für kompliziertere Fälle – etwa wenn der Knoten so lange abwesend war, dass der Platz für Hints ausgegangen ist – kennt das Nodetool-Werkzeug ein Repair-Kommando.

Wer sich ein wenig in die Cassandra-Dokumentation [4] einliest, der wird noch auf viele interessante Kommandos und Verfahren stoßen, die sich mit dem kleinen Testcluster ausprobieren lassen. So gewinnt man ein Gefühl für die Datenbank, das sich sicher auszahlt.

Fazit

Cassandra ist eine mächtige verteilte NoSQL-Datenbank, besonders geeignet für Umgebungen, die große Datenmengen bewältigen müssen und stark wachsen. Etliche fortgeschrittene Features wie die eingebaute Ausfallsicherheit und die automatische Replikation sowie die Selbstheilung nach Knotenausfall machen sie für ein breites Spektrum an Anwendungsszenarien interessant. Allerdings dürfte die Migration von einem herkömmlichen relationalen RDBMS hin zu Cassandra nicht ohne Weiteres möglich sein, weil Daten und Abfragen auf andere Weise designt sein müssen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 8 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben