Cassandra, Pumba und der Ernst des Lebens

Datenkonsistenz bei NoSQL-Datenbankclustern im Grenzfall

Julia Thrandorf, Andre Haucke

© Evstigneev Alexander/Shutterstock.com

Die Datenbank ist die Hüterin der Konsistenz – normalerweise. Aber wie schlagen sich die NoSQL-Datenbankcluster unter Stress? Wie sieht es mit der Datenkonsistenz aus, wenn das System plötzlich unter Dauerlast steht oder sporadisch einzelne Knoten ausfallen?

Im ersten Teil unseres Artikels in Ausgabe 11.18 haben wir bestimmte Konsistenzmodelle definiert und gezeigt, wie diese mit wenig Aufwand überprüft werden können. Die Methode ist unabhängig von der Ablagestruktur und lässt sich problemlos auch auf andere Datenbankmanagementsysteme (DBMS) bzw. unterschiedliche Versionen übertragen. Da der Datenbankcluster als Blackbox verwendet wird, kann die Methode natürlich keinen Beweis für die interne Korrektheit erbringen. Allerdings können wir mit entsprechend hoher Wahrscheinlichkeit davon ausgehen, dass die Konsistenz gewahrt bleibt. Durch die Erhöhung der Durchläufe bzw. mit zunehmender Testdauer präzisiert sich diese Wahrscheinlichkeit weiter.

Werfen wir nun einen Blick auf die Stress- und Grenzfälle, mit denen ein Datenbankcluster in der Realität konfrontiert ist. Dazu setzen wir den Cluster im ersten Schritt mit YCSB unter Last. Anschließend simulieren wir mit Docker und Pumba den Ausfall einzelner Knoten. Konkret zeigen wir das wieder beispielhaft mit Apache Cassandra.

Mit YCSB die Performance messen

YCSB ist ein Benchmarking-Framework für Performancemessungen von NoSQL-Datenbanken, das 2010 von Yahoo! entwickelt wurde. Das Framework besteht aus einem Client, der den Workload generiert, und einer Sammlung von sechs Standard-Workloads. Die Standard-Workloads bilden verschiedene Anwendungsfälle ab (Tabelle 1). Dazu beinhalten sie unterschiedliche Verhältnisse an Lese- und Schreiboperationen oder führen diese in einer bestimmten Reihenfolge aus. Beispielsweise kann Workload A (Update heavy workload) verwendet werden, um einen Session Store zu simulieren, der die neuesten Aktivitäten erfasst. Das Verhältnis von Lese- und Schreibzugriffen liegt in diesem Fall bei 50 zu 50. Jeder Workload besteht aus zwei Phasen: einer Loading-Phase und einer Transaktionsphase. In der Loading-Phase wird zunächst eine Datenbasis erstellt und es werden entsprechend des Werts des Parameters recordcount unterschiedliche Datensätze angelegt.

In der Transaktionsphase werden anschließend Lese- und Schreiboperationen auf den Datensätzen ausgeführt und Performancemessungen vorgenommen. Durch den Parameter operationcount können wir dabei festlegen, wie viele Lese- und Schreibzugriffe ausgeführt werden sollen.

Workload Beschreibung Anwendungsbeispiel
Workload A: Update heavy workload Besteht je zur Hälfte aus Lese- und Schreibvorgängen. Session Store, der kürzlich ausgeführte Aktionen aufzeichnet.
Workload B: Read mostly workload Besteht aus einem Lese-/Schreibmix von 95/5. Foto-Tagging: das Hinzufügen eines Tags ist eine Schreiboperation, am häufigsten werdenb Tags jedoch gelesen.
Workload C: Read only Besteht zu hundert ÜProzent aus Leseoperationen. Benutzerprofilcache, in dem Profile an anderer Stelle erstellt werden (z. B. Hadoop).
Workload D: Read latest workload Bei diesem Workload werden neue Datensätze eingefügt, die zuletzt eingefügten Datensätze sind am beliebtesten. Benutzerstatus-Updates: User wollen das Neueste lesen.
Workload E: Short ranges In diesem Workload werden kleine Bereiche anstelle von einzelnen Datensätzen abgefragt. Konversationen in Threads, in denen jede Abfrage für die Beiträge in einem bestimmten Thread gilt. Hier wird davon ausgegangen, dass Beiträge nach der Thread-ID gruppiert sind.
Workload F: Read-modify-write In diesem Workload liest der Client einen Datensatz, ändert ihn und schreibt die Änderungen zurück. Benutzerdatenbank, in der Benutzerdatensätze vom Benutzer gelesen und geändert oder Benutzeraktivitäten aufgezeichnet werden.

Tabelle 1: Übersicht der Standard-Workloads von YCSB

Cassandra unter Last

Um das Konsistenzverhalten von Cassandra unter Last zu testen, setzen wir unseren Cluster wieder mit Hilfe von Docker auf. Parallel zu unserem Client für die Überprüfung der Konsistenzmodelle starten wir YCSB und setzen Cassandra damit unter Last (Abb. 1).

Abb. 1: Testaufbau eines Cassandra Clusters unter Last mit Hilfe von YCSB

Abb. 1: Testaufbau eines Cassandra Clusters unter Last mit Hilfe von YCSB

Mit Hilfe einer docker-compose.yml-Datei wird das Cluster bestehend aus drei Nodes gestartet (1.). Um YCSB verwenden zu können, muss in der Datenbank ein Keyspace ycsb sowie eine Tabelle usertable vorhanden sein. Diese konfigurieren wir im nächsten Schritt und starten dazu eine CQL-Shell innerhalb des Containers node1 (2.). In der CQL-Shell führen wir dann die Queries zum Anlegen des Keyspaces und der Tabelle aus (3.). Was nun fehlt, ist eine Datenbasis, auf der YCSB Lese- und Schreibzugriffe ausführen kann. Diese laden wir im nächsten Schritt (4.) ins Cluster und übergeben dabei mit den Parametern -P workloads/workloada und -P workloads/large.dat zwei Property-Dateien. Die Datei workloada ist der Standard Workload A, den YCSB mitliefert. In der Datei large.dat haben wir die Parameter recordcount und operationcount mit 500 000 und 1 000 000 definiert. Damit legt YCSB 500 000 unterschiedliche Datensätze als Datenbasis an und führt in der TransaktionsPhase 1 000 000 Schreib- und Leseoperationen aus. Unser Cluster ist nun einsatzbereit, sodass wir anschließend (5.) unsere Anwendung und parallel dazu YCSB in der TransaktionsPhase starten. Nach dem Testdurchlauf wird das Cluster wieder heruntergefahren (6.), um gleiche Bedingungen für jeden Durchlauf sicherstellen zu können (Kasten: „In sechs Schritten zum Lasttest“).

In sechs Schritten zum Lasttest

1. Aufsetzen und Starten des Clusters:

docker-compose up -d2.

2. Starten einer CQL-Shell in Container node1 des Clusters:

docker run -it --network=cassandra_clusternet --link node1:cassandra --rm cassandra:3.11.1 cqlsh cassandra

3. Anlegen des Keyspaces ycsb (1) und der Tabelle usertable (2) innerhalb der CQL-Shell:

(1) CREATE KEYSPACE IF NOT EXISTS ycsb WITH REPLICATION = {’class’:’NetworkTopologyStrategy’, ’datacenter1’: 3};
(2) CREATE TABLE IF NOT EXISTS ycsb.usertable (y_id varchar, field0 varchar, field1 varchar, field2 varchar, field3 varchar, field4 varchar, field5 varchar, field6 varchar, field7 varchar, field8 varchar, field9 varchar, PRIMARY KEY (y_id));

4. Ausführen der Loading-Phase von YCSB:

./bin/ycsb load cassandra-cql -p hosts=”192.168.99.100” -P workloads/workloada -P workloads/large.dat -s > ./Ergebnisse/loadingResults.dat

5. Start der Anwendung (1) und der TransaktionsPhase von YCSB (2):

(1) java -jar conchecker.jar configurationCassandra.json
(2) ./bin/ycsb run cassandra-cql -p hosts=”192.168.99.100” -P workloads/workloada -P workloads/large.dat -s > ./Ergebnisse/transactionResults.dat

6. Herunterfahren des Clusters:

docker-compose down –v

Ergebnisse unter Last

Wir haben jedes Konsistenzmodell in 500 000 Testdurchläufen mit verschiedenen Konstellationen der Konsistenzlevel überprüft und dabei das Cluster durch die parallele Ausführung von YCSB zusätzlich unter Last gesetzt. Das führte jedoch nur zu geringen Veränderungen hinsichtlich der Einhaltung bzw. der Verletzung der getesteten Konsistenzmodelle. Bei der Überprüfung von RYWC traten unter den gleichen Konfigurationen wie auch schon ohne Last Verletzungen auf. Lediglich bei der Konfiguration ANY und QUORUM wurden Verletzungen festgestellt, die zuvor nicht aufgetreten sind. Bei dem Konsistenzlevel ANY müssen Schreibzugriffe entweder von einem Node bestätigt werden, oder der Koordinator speichert einen Hint und führt die Schreiboperation zu einem späteren Zeitpunkt aus. Aufgrund der erhöhten Last kann so ein Flaschenhals bei der Abarbeitung der Schreibzugriffe entstehen. Bei einem Lesekonsistenzlevel von QUORUM kann dann unter Umständen kein Read Repair stattfinden, wenn die Mehrheit an Nodes den gleichen veralteten Datenbestand hat. Das führt letztlich zu einer Verletzung des Konsistenzmodells (Tabelle 2).

Tabelle 2: Ergebnisse im Vergleich: Cassandra Cluster ohne und mit Last

Tabelle 2: Ergebnisse im Vergleich: Cassandra Cluster ohne und mit Last

Ausfallszenarien

NoSQL-Datenbanken gelten aufgrund der redundanten Speicherung von Daten als besonders ausfallsicher. Technische Einschränkungen oder Fehler in der Infrastruktur sind bei verteilten Systemen allerdings nie hundertprozentig auszuschließen und unvermeidbar. Es können beispielsweise Netzwerkprobleme wie Einschränkungen der Bandbreite oder erhöhte Latenzen auftreten. Einzelne Server können geplant und ungeplant ausfallen und dadurch vorübergehend nicht erreichbar sein. Die Frage ist, wie verteilte Datenbanken damit umgehen und was man vom NoSQL-System seiner Wahl im Fehlerfall noch erwarten kann. Das wollen wir am Beispiel von Cassandra herausfinden und überprüfen, welche Auswirkungen der zufällige Ausfall einzelner Knoten des Clusters auf die Einhaltung der Konsistenzmodelle hat. Dafür nehmen wir Pumba zu Hilfe.

Pumba testet Chaos

Pumba ist ein Open-Source-Tool, das für das sogenannte Chaos-Testing, auch Resilience Testing genannt, entwickelt wurde. Es basiert auf den Ideen des im Jahr 2011 von Netflix entwickelten Chaos Monkey, das für das Testen von Amazon-Web-Services-(AWS-)Infrastrukturen eingesetzt werden kann. Pumba verwendet einen ähnlichen Ansatz und ermöglicht, Chaostests gegen Docker-Container auszuführen. Dabei lassen sich Container pausieren, was einem vorübergehenden Ausfall eines Servers entspricht. Pumba dient außerdem dazu, Netzwerkprobleme wie Paketverlust oder Verzögerungen zu injizieren.

Cassandra trifft Pumba

Für unseren Test setzen wir wieder ein Cassandra-Cluster mit drei Knoten auf. Zusätzlich wird Pumba innerhalb derselben Docker-Maschine gestartet (Abb. 2).

Abb. 2: Testaufbau für den Ausfall einzelner Knoten mit Hilfe von Pumba

Abb. 2: Testaufbau für den Ausfall einzelner Knoten mit Hilfe von Pumba

Wir starten zunächst wieder über unsere docker-compose.yml-Datei den Cluster. Nachdem der Cluster aufgesetzt ist, wird die Anwendung gestartet und unmittelbar danach innerhalb derselben Docker-Maschine Pumba ausgeführt (2.). Pumba pausiert daraufhin im Abstand von 300 Sekunden für jeweils 30 Sekunden einen beliebigen der drei Cassandra Nodes. Der Cluster nimmt das als Ausfall des jeweiligen Nodes wahr. Nach dreißig Sekunden wird der pausierte Container durch Pumba fortgesetzt. Der Node meldet sich wieder im Cluster an. Nach dem Testdurchlauf wird der Cluster wieder heruntergefahren (3.) (Kasten: „Cassandra und Pumba im Team“).

Cassandra und Pumba im Team

1. Aufsetzen und Starten des Clusters

docker-compose up -d

2. Start der Anwendung (1) und Pumba (2):

(1) java -jar conchecker.jar configurationCassandra.json
(2) docker run –d –v //var/run/docker.sock:/var/run/docker.sock gaiaadm/pumba pumba –-random –-interval 300s pause –-duration 30s node1 node2 node3

3. Herunterfahren des Clusters

docker-compose down –v

Ergebnisse mit Pumba

Trotz des zufälligen Ausfalls einzelner Knoten konnte Cassandra das Niveau bezüglich der Einhaltung der getesteten Konsistenzmodelle weitestgehend halten. Testdurchläufe mit einem Konsistenzlevel von ALL, unabhängig ob für Schreib- oder Lesezugriffe, konnten nicht abgeschlossen werden. Sobald ein Node während der Durchführung ausgefallen ist, konnte das Level erwartungsgemäß nicht mehr erreicht werden, was zu einer Fehlermeldung seitens Cassandra und dem Abbruch des Testdurchlaufs führte. Die Testdurchläufe sind dementsprechend mit einem E (Exception) gekennzeichnet (Tabelle 3).

Tabelle 3: Ergebnisse im Vergleich: Cluster ohne und mit Ausfall einzelner Nodes

Tabelle 3: Ergebnisse im Vergleich: Cluster ohne und mit Ausfall einzelner Nodes

Ausblick

Wie wir gesehen haben, lassen sich mittels YCSB NoSQL-Cluster recht einfach unter Last setzen. Dadurch gelang es uns, einzelne Fehler zu provozieren. Die Abwägung, wie wahrscheinlich eine solche Hochlastsituation und wie gravierend ein solcher Fehler ist, muss natürlich der Entwickler oder Softwarearchitekt vornehmen. Der einfache und automatisierte Testaufbau sorgt für nachvollziehbare Ergebnisse und sollte bei der Dokumentation der Entscheidung ebenfalls festgehalten werden.

Durch den Testaufbau mit Docker lassen sich via Pumba sehr einfach Serviceausfälle und Infrastrukturstörungen simulieren. Für die Demonstration in diesem Artikel haben wir uns auf Ausfälle konzentriert. In der Praxis wird man jedoch auch häufig mit Schwankungen in der Latenz konfrontiert, die zu höchst unerwarteten Ergebnissen führen können.

Es ist klar, dass stärkere Konsistenzgarantien mit höherer Rechenleistung und damit mit Performance erkauft werden. Durch die hier gezeigten Last- und Stresstests bekommt man durch ihr Laufzeitverhalten bereits einen ersten Eindruck, wie sich einzelne Konfigurationen auf die Performance auswirken können. Ebenso ist zu beachten, dass die hier gezeigten Konsistenztests nicht ohne weiteres geeignet sind, richtige Performancetests zu ersetzen. Denn dazu gehört neben der Analyse der Schreib- und Leserate sowie einer Aufwärmphase insbesondere auch eine Analyse der konkreten Abfrage- und Schreiboperationen.

Alle Quelltexte sind in voller Ausführlichkeit unter GitHub zu finden und laden zum Experimentieren ein. Hier finden sich neben den Implementierungen für Cassandra auch Beispiele für MongoDB und Redis.

Geschrieben von
Julia Thrandorf
Julia Thrandorf
Julia Thrandorf arbeitet bei EXXETA in Leipzig als DevOps und Application Developer. Sie hat Informatik studiert und in ihrer Masterarbeit die Konsistenzgarantien von verschiedenen NoSQL-Datenbankclustern untersucht.
Andre Haucke
Andre Haucke
André Haucke arbeitet als Team Lead bei EXXETA in Leipzig. Neben Team- und Technologieentwicklung liegen seine Schwerpunkte auf Softwarearchitektur und der Anwendung agiler Methoden.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: