Podcast

Distributed Databases – Teil 1

Über Konsistenz, Replikation und Partitionierung von Datenbanken

Was sind eigentlich Konsistenzmodelle und welche Rolle spielen sie für Datenbanken? Wieso verteilt man Datenbanken und welche Probleme bringt das mit sich? Wieso macht es Sinn, Daten zu replizieren und welche Möglichkeiten gibt es dafür? Und was machen wir, wenn die Datenmengen zu groß werden für einen einzelnen Knoten? Diese und andere Fragen beantwortet Lucas Dohmen in dieser ersten Folge über verteilte Datenbanken.
Listen to other episodes

Shownotes & Links

Transkript

show / hide transcript

Stefan Tilkov: Hallo und herzlich willkommen zu einer neuen Episode des innoQ-Podcasts. Das heutige Thema sind Distributed Databases, verteilte Datenbanken, und mein Gast heute ist der gute Lucas! Hallo, Lucas. Sag kurz etwas zu dir, bitte.

Lucas Dohmen: Ich bin der Lucas, ich bin Senior Consultant bei innoQ und ich mache unter anderem den innoQ Podcast, aber heute lasse ich mich mal interviewen. Ich beschäftige mich hauptsächlich mit Webentwicklung bei innoQ, im Frontend, im Backend, mit Datenbanken und allem möglichen und in meiner Vergangenheit habe ich auch mal für eine Datenbank-Firma gearbeitet und habe mich da intensiv mit diesem Thema beschäftigt und deswegen teile ich auch mal gerne mein Wissen über Datenbanken.

Stefan Tilkov: Ich freue mich darauf, etwas von dir zu lernen. Lass uns doch mal ganz kurz damit beginnen, dass du mir mal kurz erklärst, wozu man Datenbanken überhaupt braucht.

Lucas Dohmen: Wenn man sich so eine Webanwendung anguckt, dann machen das ja viele gerne so, dass diese Webanwendung an sich stateless ist. Die soll keinen state haben, damit ich die fünfzehnmal nebeneinander deployen kann. Und wenn ich dann noch mehr load habe, dann noch mehr dazu packen kann, aber es gibt da irgendwelche Dinge, über die sollten alle Instanzen Bescheid wissen von meiner Applikation. Beispielsweise: Welche Benutzer gibt es in meinem System, welche Nutzdaten habe ich, und darum haben sich manche Leute überlegt, dafür wäre es doch cool, wenn wir dafür eine separate Anwendung hätten, die diesen state verwaltet, damit der Rest dann darauf zugreifen kann und stateless sein kann. Es gibt ja auch einen anderen Ansatz, beispielsweise die SQLite-Datenbank ist ja so eine embeddable database, da verwalte ich meine Daten aus dem Prozess heraus, aus meiner Anwendung heraus, aber gerade, wenn ich etwas mehr skalieren möchte, bietet es sich halt an, die Datenbank als separaten Prozess auf einer separaten Maschine zu haben und die Datenbank bietet mir dann halt Möglichkeiten, Daten zu persistieren und vor allem sie auch wieder effizient abzufragen. Also, sie zu verändern, sie abzufragen und so weiter. Dafür ist eine Datenbank da.

Stefan Tilkov: Ok, also unser Kontext sind alle Datenbanken, die extern zu meinem eigentlichen Applikationsprozess sind, zum Beispiel MySQL, PostgreSQL oder solche Enterprise-Geschichten wie Oracle oder DB2 oder NoSQL-Sachen wie Redis, Cassandra…wir reden über alles eigentlich.

Lucas Dohmen: Genau. Und haben den Begriff auch weit gefasst, manche sagen ja vielleicht: Redis ist keine Datenbank, sondern vielleicht eher ein key value store. Wir fassen das als einen weiten Begriff, aber ein bisschen mit einem Fokus auf der Web-Perspektive, das ist das, was ich am besten kenne. In embedded systems gibt es bestimmt auch Datenbanken, aber keine Ahnung, wie das da funktioniert.

Stefan Tilkov: Gut. Wir haben jetzt ja schon unterschiedliche Datenbanken genannt, aber ich glaube der Hauptpunkt, über den wir heute reden wollen, ist ein anderer Unterschied oder ein anderer wesentlicher Aspekt. Vielleicht sagst du uns mal, worin sich solche Datenbanken noch unterscheiden können.

Lucas Dohmen: Datenbanken können sich grundsätzlich erstmal in ihrem Datenmodell unterscheiden, in der Art, wie wir sie abfragen und so weiter, das würde ich jetzt mal ausklammern aus dieser Episode, da können wir auch gerne mal wann anders drüber sprechen, aber das wollen wir nicht betrachten. Wir wollen über verteilte Datenbanken sprechen, da ist ein wesentlicher Unterschied, wie sie mit Konsistenz umgehen. Wenn wir jetzt mehrere Operationen auf einer Datenbank ausführen, dann gibt es, wenn ich die Datenbank von außen betrachte und Operation A, B und C ausführe, Kombinationen von Operationen, wo ich sage: Das macht Sinn, dass sie das tut und es gibt Kombinationen, da sage ich: Das macht nicht soviel Sinn. Wenn ich der Variable A den Wert “2” zuweise und dann die Variable A auslese und “3” zurückkriege, dann bin ich überrascht. Dann würde ich sage: Das ist nicht das, was ich erwarte, ich hatte jetzt gehofft, “2” zurückzukriegen. Aber es kann durchaus Modelle geben, wo das korrekt ist, dass ich jetzt “3” zurückkriege, und das bezeichnen wir als Konsistenzmodell. Also, die Eigenschaft von einem System, wenn man es von außen betrachtet, welche Kombinationen von Operationen und deren Ergebnissen einen Sinn ergeben.

Stefan Tilkov: Aha. Lass uns nochmal kurz sehen, ob ich das auch richtig einsortiere. Wenn wir hier von Konsistenz sprechen, dann gilt das auch schon, wenn wir die Datenbank nicht auf n Maschinen verteilt haben oder so, sondern wir reden hier von, wie in deinem Beispiel gerade, vielleicht fünf, sechs, sieben, acht Instanzen zum Beispiel meiner Webanwendung, die aber mit einer einzigen Datenbank spricht. Und schon dann müssen wir uns über dieses Thema Gedanken machen.

Lucas Dohmen: Richtig. Das ist ganz wichtig. Wir betrachten jetzt erstmal für den Anfang einen einzelnen Knoten. Wir haben eine Datenbank, es gibt kein Leader-Follower-Modell oder so, wir haben einfach nur eine Datenbank und die fragen wir ab. Wenn die Maschine ausgeschaltet würde, dann gäbe es keine Datenbank.

Stefan Tilkov: Im Beispiel von gerade könnte das sein: Client eins hat dieses A auf “2” gesetzt und Client zwei ruft das ab und wir müssen uns Gedanken machen: Soll der jetzt, wenn er danach abfragt, den Wert bekommen, der kleiner ist und so weiter.

Lucas Dohmen: Richtig. Aber auch, wenn derselbe Client es abfragt. Es kann ja durchaus sein, dass er abfragt und dann kriegt er etwas anderes zurück, als er geschrieben hat. Das wäre ja auch möglich.

Stefan Tilkov: Ok. Wie du schon gesagt hast: Das wäre für mich vielleicht überraschend, also lass uns doch vielleicht mal über konkrete Konsistenzmodelle sprechen. Welche gibt es denn da so?

Lucas Dohmen: Es gibt unendlich viele. Ich könnte mir selbst ein Konsistenzmodell ausdenken, das, egal was ich schreibe, mir immer “Hund” zurückgibt. Das wäre auch ein Konsistenzmodell.

Stefan Tilkov: Sehr konsistent: Hund.

Lucas Dohmen: Genau. Aber vielleicht nicht sinnvoll. Und mit der Zeit haben sich viele Leute verschiedene Konsistenzmodelle überlegt. Teilweise sind diese Konsistenzmodelle aus der Zeit, wo verschiedene Hersteller SQL-Datenbanken hergestellt haben, es gab noch nicht so viele Standards und die haben dann versucht, zu beschreiben, wie sich ihre Datenbank verhält, um zu sagen: Das ist konsistent. Und der eine Datenbank-Hersteller hat gesagt: So ist konsistent und der andere Datenbank-Hersteller hat gesagt: So ist konsistent. Das war im Nachhinein nicht mehr so gut vergleichbar, also hat man versucht, das teilweise zu standardisieren, aber es gibt auch ganz viele Wissenschaftler, die sich erstmal damit beschäftigen, quasi Konsistenzmodelle zu kategorisieren und in eine Baumstruktur zu bringen.

Eine von dieses Baumstrukturen ist von Peter Bailis, der hat das mal alles so schön angeordnet und es gibt so ein paar Konsistenzmodelle, die sind wirklich wichtig. Eins davon ist zum Beispiel Linearisierbarkeit. Linearisierbarkeit ist ein Konsistenzmodell, welches ein intuitives Konsistenzmodell ist. Es besagt, dass, wenn ich eine Operation ausführe, es einen Zeitpunkt gibt, zu dem ich die Operation starte und einen Zeitpunkt, zu dem mir dieses Ding eine Antwort sagt: “Ich habe es geschafft”. Nehmen wir beispielsweise die MySQL: Ich schicke eine query hin, das ist der Startpunkt und irgendwann kriege ich eine Antwort zurück und das ist das Ende. Die Linearisierbarkeit sagt jetzt: Irgendwann, zwischen dem Zeitpunkt, an dem ich es absende und dem Zeitpunkt, an dem ich es zurückkriege, muss die Operation passieren. Das ist jetzt intuitiv erst einmal richtig, ne? Das bedeutet aber in der Konsequenz, dass, wenn ich jetzt einmal etwas schreibe, eine Antwort bekomme, dass es geschrieben wurde, und dasselbe dann wieder lese und die Antwort zurückkriege, dass ich es gelesen habe. Dann muss vor meinem Lesen das Schreiben passiert sein, weil ich habe ja schon eine Antwort gekriegt.

Stefan Tilkov: Wir reden aber von deinen Requests, von denen, die du nacheinander abschickst. Wir reden nicht davon, dass ich konkurrierend auch noch etwas anderes an die Datenbank schicke.

Lucas Dohmen: Genau. Für den einfachen Einstieg erst einmal das, aber die Linearisierbarkeit interessiert sich nicht dafür, wer was getan hat, sondern es sagt einfach nur aus, dass es einen Anfangsstrich und einen Endstrich gibt und irgendwo dazwischen meine Operation passieren muss.

Stefan Tilkov: Ich runzle die Stirn, weil ich es noch nicht ganz verstanden habe, aber mach mal weiter.

Lucas Dohmen: So, und wenn wir das betrachten, dann könnte es ja jetzt passieren, dass ich zur Datenbank sage: Setze A auf “2”. Und du sagst kurz danach: Setze A auf “3”. Und dann kriegst du die Antwort zurück und ich kriege die Antwort zurück. Jetzt kann es ja entweder sein, dass das, was du gesagt hast, zuerst geschrieben wurde, oder dass das, was ich gesagt habe, zuerst geschrieben wurde. Die Linearisierbarkeit sagt nichts darüber aus, was davon korrekt ist. Beides könnte sein, aber eins davon muss sein. Also, es darf nicht passieren, dass eine Mischung aus unseren beiden Sachen darin steht und es darf auch nicht passieren, dass keine von unseren beiden Sachen darin steht. Sondern, wenn ich quasi die Reihenfolge der Operationen, die auf der Datenbank passieren, betrachte, dann gibt es definitiv eine Reihenfolge, in der es passiert ist und es gibt verschiedene Möglichkeiten, welche es gewesen sein könnte…

Stefan Tilkov: …und alle sehen dieselbe Reihenfolge. Lass es mich versuchen, es in meinen eigenen Worten nochmal zu sagen: Das würde jetzt zum Beispiel bedeuten, für mich rein technisch, da wird irgendetwas zwischen gepuffert oder so, da kommen Netzwerk-Requests herein, die werden gepuffert und von asynchronen Threads irgendwann einmal auf die Datenbank übertragen. Und damit könnte es passieren, dass wir beide, nach unserem eigenen zeitlichen Empfinden, was wir, wenn wir es geschickt haben, eigentlich erwarten, dass ein bestimmtes Ergebnis zurückkommt, aber aus bestimmten Gründen kommt ein anderes, weil zufällig nicht deterministisch der eine Puffer voll war und der andere noch nicht, der eine Thread mehr Zeit hatte, da kommt also etwas anderes heraus, als wir erwartet hätten, aber, was auch immer herauskommt, wir sehen beide die gleiche Welt. Die gleiche Reihenfolge. Es passiert nacheinander und alle sehen die gleiche Reihenfolge.

Lucas Dohmen: Genau. Und vor allem, und das ist der Punkt, warum es wichtig ist zu sagen: Es passiert, bevor ich die Antwort zurückkriege, also spätestens dann, wenn ich die Antwort zurückkriege, ist es passiert, ist gerade das Thema Caches. Wenn die Datenbank mich lesen lässt, von einem Cache, dann muss sie dafür sorgen, dass bevor ich die Antwort zurückkriege, sie diesen Cache invalidiert hat. Damit, wenn ich jetzt nochmal lese, ich nicht den alten Wert kriege. Das erfüllen schonmal ein ganzer Haufen von Systemen nicht, diese Eigenschaft.

Stefan Tilkov: Linearisierbarkeit ist keinesfalls so selbstverständlich, wie sie sich vielleicht anhört.

Lucas Dohmen: Genau. Das ist ein relativ starkes Konsistenzmodell.

Stefan Tilkov: Ok. Also die Konsistenzmodelle, das ist auch das mit der Baumstruktur, die du gerade erwähnt hast, die lassen sich unter anderem nach der Stringenz oder Striktheit, nach der sie etwas gewährleisten, einsortieren.

Lucas Dohmen: Genau. Ich kann zum Beispiel sagen: Jedes System, was linearisierbar ist, ist automatisch auch sequenziell. Sequenziell ist eine andere Konsistenzklasse, aber ich weiß, durch die Eigenschaften von Sequenzialität, dass es dann auch linearisierbar sein muss. Und dadurch kann ich mir so einen Baum denken. Auf der Spitze…

Stefan Tilkov: Ist es nicht gerade andersherum? Weil es linearisierbar ist, muss es auch sequenziell sein?

Lucas Dohmen: Wenn es linearisierbar ist, ist es mindestens auch sequenziell.

Stefan Tilkov: Das meinte ich. Aber nicht andersherum.

Lucas Dohmen: Und auf der obersten Spitze von dem Baum steht die strong serializability, ich weiß nicht, wie man das auf deutsch am besten sagt. Und wenn man die hat, dann hat man alles andere auch.

Stefan Tilkov: Und was macht die noch mehr als Linearisierbarkeit?

Lucas Dohmen: Die strenge Serialisierbarkeit hat noch Aussagen über Transaktionen, also in dem Modell der Linearisierbarkeit gibt es Transaktionen nicht. Das, was ich gerade beschrieben habe, das hat nichts mit Transaktionen zu tun. Ich rede ja nur über einzelne Dinge, die ich tue. Und die strenge Serialisierbarkeit, die nimmt jetzt auch noch Aspekte von einem anderen Aspekt, wo man über Konsistenz von Systemen, die halt Transaktionen ausführen.

Stefan Tilkov: Transaktion meint hier in dem Fall einfach eine Folge von Aktionen? Diese fünf Dinge, diese zehn Dinge und diese fünfzehn Dinge werden jeweils als ein Block betrachtet und bei starker Serialisierbarkeit, so würde ich das übersetzen, wäre eben auch die Garantie mit dabei.

Lucas Dohmen: Genau. Und über eine Transaktion kann man auch verschiedene Aussagen treffen. Also man könnte sagen: Eine Transaktion muss immer so ausgeführt werden, dass, wenn eine zweite Transaktion ausgeführt wird, dann darf die währenddessen nicht auf die Datenbank zugreifen, die dürfen sich nicht überschneiden, das könnte ich aber auch herausnehmen, da gibt es dann wieder verschiedene Punkte, da müssen wir jetzt nicht im Detail drauf eingehen, da gibt es auf jeden Fall auch verschiedene Stufen von: Wie strikt gehen wir mit solchen Sachen um? Eigentlich geht es oft darum, intuitiv würden viele Leute sagen: Ich will immer das Strengste. Das alles konsistent ist. Das ist auch immer das, was am teuersten ist, was am meisten Zeit kostet, was am meisten Koordination kostet. Deswegen müssen wir immer ein bisschen abwägen und darüber nachdenken, was brauchen wir wirklich? Welche Garantien brauchen wir? Deswegen sind diese Garantie-Klassen so wichtig.

Meiner Meinung nach ist es auch ein bisschen Aufgabe des Datenbank-Herstellers, einem zu sagen: Das ist das, was ich dir wirklich versprechen kann. Das ist das, was unsere Datenbank dir bieten kann. Dann kann ich sagen: Das ist okay für mich. Oder: Das ist nicht okay für mich. Aber ich muss eine informierte Entscheidung darüber treffen können, ob das, was die Datenbank mir verspricht, meinen Use-Case abbildet. Meistens braucht man weniger Konsistenz, als man erst einmal denkt. Viele sagen erst einmal, sie brauchen die höchste Konsistenz überhaupt, aber oftmals ist das gar nicht so der Fall. Denn gerade, wenn man das Gesamtsystem betrachtet, hat das ja vielleicht auch wieder ein Konsistenzmodell. Dann gehen solche Prinzipien schnell ein bisschen zurück. Aber man muss eine informierte Entscheidung darüber treffen: Was brauchen wir?

Stefan Tilkov: Letztendlich ist es ja auch ein Trade-Off zwischen zum Beispiel dem Durchsatz und der Parallelität, die ich erlaube, weil wenn, also nicht nur Performance im weitesten Sinne, aber eben auch dieser Aspekt, dass Dinge gleichzeitig passieren dürfen, weil ich eben nicht unbedingt alles nacheinander machen muss, was eigentlich unmittelbar einleuchtet.

Lucas Dohmen: Genau. Auch wenn wir über eine einzelne Datenbank sprechen, möchte ich ja vielleicht, dass die multi threaded ist, und dass da verschiedene Dinge gleichzeitig passieren können, damit fünfzehn Clients sich connecten können und nicht nur einer, dann muss ich vielleicht schon trade offs treffen. Weil, wenn ich sagen würde: Während eine Transaktion passiert, darf nichts anderes passieren, dann würde ich ja mein System quasi zu einem single threaded, also meine Datenbank zu einer Single-Threaded-Datenbank machen.

Stefan Tilkov: Wenn eine Transaktion zehn Sekunden dauert, dann warten eben alle anderen mindestens eben diese zehn Sekunden.

Lucas Dohmen: Genau.

Stefan Tilkov: Okay, habe ich verstanden. Jetzt haben wir darüber gesprochen, dass wir solche einzelnen Knoten haben, also all das, was wir gerade diskutiert haben, gilt auch schon dann, wenn wir praktisch einen oder n Clients plus einen Datenbankserver haben. Warum würden wir jetzt auf die Idee kommen, so einen Datenbankserver selbst auch nochmal zu verteilen, auf mehrere verschiedene Maschinen?

Lucas Dohmen: Genau. Dafür gibt es zwei verschiedene Gründe. Also eigentlich drei. Der eine Grund ist, weil man das so macht. Aber die zwei guten Gründe sind Ausfallsicherheit und Datenmengen. Große Datenmengen. Für mich ist der viel wichtigere Punkt die Ausfallsicherheit, weil Datenmengen, das ist so ein Thema. Viele denken immer, sie hätten so große Datenmengen, dass es nicht auf einen Server passt und das ist fraglich, weil mittlerweile auf so einen Server schon echt viele Daten passen. Aber der Aspekt der Ausfallsicherheit ist eigentlich etwas, das wir alle auch schon kennen, wenn man ein klassisches set up hat. Man hat eine PHP-Anwendung und dahinter eine MySQL. Dann möchte man vielleicht dafür sorgen, dass, falls der MySQL-Server explodiert, man nicht gar keine Daten mehr hat. Das ist eine Anforderung, die nicht unüblich ist. Und eine Möglichkeit ist natürlich ein Back-Up zu schreiben, aber vielleicht möchte ich keine n Zeiteinheiten brauchen, bis ich wieder oben bin, weil ich jetzt erstmal mein Tape-Back-Up wieder einspielen muss, sondern ich möchte vielleicht innerhalb von zehn Sekunden oder so wieder da sein und da gibt es dieses Konzept von diesem hot back up oder so…

Stefan Tilkov: Stand-By.

Lucas Dohmen: Genau! Wir sprechen bei solchen verteilten Datenbanken dann von leadern und followern. Leader ist jemand, der Schreiboperationen entgegen nimmt und sie dann an alle follower weiterleitet und die follower folgen diesem leader und sagen: Okay, alles, was du sagst, machen wir und von uns kannst du auch lesen. Du kannst jetzt darauf lesend zugreifen, aber nicht schreibend, weil das ist kein leader, der darf nicht weiter verteilen. Und wenn wir jetzt einen follower hinter unsere Datenbank hängen, auf den von außen keiner zugreifen kann, dann könnten wir dafür sorgen, dass, wenn der abstürzt, dann der andere zum leader wird. Und dann können meine PHP-Knoten alle wieder mit diesem Knoten sprechen, als Ersatz.

Stefan Tilkov: Das ist das, was man früher Master-Slave genannt hat, aber nicht mehr sagt, weil man heute solche Begriffe vermeidet und deshalb leader, follower.

Lucas Dohmen: Genau.

Stefan Tilkov: Das heißt, es kann zu jedem Zeitpunkt nur einen leader geben, das vereinfacht alles ein kleines bisschen. Und wenn der eben aus irgendwelchen Gründen nicht mehr da ist, wird ein anderer ausgewählt. Okay, habe ich verstanden. Was haben wir noch?

Lucas Dohmen: Das ist aber noch nicht alles zu diesem System, das ist das, was wir als single leader bezeichnen, damit kann ich auf jeden Fall schonmal meine reads skalieren, ich könnte dafür sorgen, dass man von außen auch auf meine follower zugreifen kann und dann kann ich von denen lesen.

Stefan Tilkov: Ein Zusatznutzen, sozusagen. Ich habe den schon mal bereitstehen, für den wird alles, da kommen wir sicher gleich drauf, hin repliziert, damit der einspringen kann, wenn mein leader abschmiert, aber ich kann ihn auch einfach zum Lesen benutzen, um mehr Leselast zu unterstützen.

Lucas Dohmen: Und ich könnte auch einfach n follower haben. Der single leader sagt nur, es gibt einen leader und es gibt beliebig viele follower. Und ein follower kann natürlich auch einem anderen follower followen, wenn wir dann so eine Kette aufbauen wollen, da gibt es so verschiedene Ansätze, aber grundsätzlich können wir damit keine writes skalieren. Wir können nur sagen: Wenn ich auf den leader schreibe, darf ich nur auf den schreiben, auf die anderen nicht. Und damit kann ich halt reads skalieren oder so ein Back-Up fahren. Was ich damit habe, ist dann erstmal diese Replikation. Also, ich muss ja irgendwie dafür sorgen, dass die Information von diesem leader zu seinen followern kommt.

Da gibt es so vier bis fünf übliche Ansätze, wie man das macht, aber erst einmal müssen wir darüber reden: Machen wir das synchron oder asynchron? Das ist eine wichtige Entscheidung, weil das - also was bedeutet das? Wenn ich eine synchrone Replikation mache, dann schreibe ich auf den leader, der sagt seinen followern: Bitte schreibt das. Wartet dann darauf, dass sie sagen: Habe ich gemacht. Und dann antwortet er. Das bedeutet, dass ich immer davon ausgehen kann, dass alle die aktuellen Daten haben, weil vorher wird es nicht bestätigt, dass es geklappt hat. Wenn ich asynchron vorgehe, dann sagt der leader nur: Ich schreibe bei mir auf die disc und sorge dafür, dass ich alles weiß und sage im Hintergrund meinen followern Bescheid: Übrigens, ihr solltet euch darum kümmern. Aber ich warte nicht darauf, dass sie antworten, bis ich das bestätige.

Stefan Tilkov: …das bestätigst, dass das geschrieben wurde.

Lucas Dohmen: Genau. Das ist wieder ein performance trade off, ne? Also, wenn ich auf alle meine follower warte, bis sie geschrieben haben, brauche ich natürlich länger, um einen write zu bestätigen. Wenn ich das aber nicht tue und mein leader stürzt ab, dann kann es einen Datenverlust geben von n Sekunden, weil, da reden wir von dem Replikationslack, das wie die der Replikation hinterherhinken, diese Zeit kann verloren gehen, wenn der leader abstürzt oder abbrennt oder was auch immer. Das heißt, da müssen wir schon eine Entscheidung treffen. Wir entscheiden uns zwischen einer hohen Sicherheit oder einer gewissen Schnelligkeit und bei einem follower ist das vielleicht noch eine einfache Entscheidung, aber bei drei followern ist es vielleicht schon eine nicht mehr so einfache Entscheidung.

Stefan Tilkov: Und wie machen Datenbanken, die dieses Modell verfolgen, das typischerweise? Machen die immer das eine oder immer das andere oder kann man das konfigurieren?

Lucas Dohmen: Bei vielen kann man es konfigurieren, bei einigen nicht. Ich weiß jetzt nicht im Detail, bei welchen Datenbanken es wie ist, das sollte auf jeden Fall dokumentiert sein, wie das funktioniert, wie die Replikation abläuft, aber oftmals kann man es konfigurieren. Gerade bei den klassischen SQL-Datenbanken. Und da gibt es jetzt verschiedene Möglichkeiten, wie man repliziert, also welche Informationen schicke ich weiter? Eine ganz naive Variante, die MySQL ganz, ganz lange verfolgt hat, ist: Jedes Kommando, das ich kriege, schicke ich auch einfach an meine follower. Das klingt erst einmal okay, aber ist es nicht. Weil, wenn ich beispielsweise ein Ding habe, das automatisch einen Time-Stamp in einer Datenbank hat, dann würde jetzt der Datenbank-Time-Stamp auf dem leader anders sein, als auf den followern. Das heißt, hier kann schon eine Inkonsistenz passieren.

Deswegen ist es mittlerweile üblich, dass man den sogenannten write ahead log zu den followern hinschickt, das ist quasi eine logische Abfolge von Dingen, die mit meinen Daten passiert sind, aber mit dem Ergebnis, was passiert ist. Und sehr, sehr viele Datenbanken haben einen write ahead log, das heißt, sie schreiben erst einmal in den write ahead log: Das wird jetzt passieren. Und dann manipulieren sie ihre Datenstrukturen und das ist quasi so ein “Wenn ich abstürze, kann ich den nochmal abspielen und dann habe ich meinen Datenbestand wieder hergestellt und wenn ich den verschicke, dann kann ich sicherstellen, dass alle dasselbe Verständnis von dem aktuellen Stand haben und nicht nur ein ungefähr gleiches Verständnis”. Und als dritte Variante - also es gibt noch zwei, drei mehr - aber als dritte Variante, die jetzt zum Beispiel in dem Postgres 10, das gerade herausgekommen ist, eingeführt wurde, ich glaube, das heißt logical replication, da rede ich über logische Dinge, die passiert sind. Das ist nicht ganz so fein granular oder detailverliebt wie das, sondern ich beschreibe mehr: Ich habe das getan und deshalb ist das passiert. Das ist die logische Replikation. Und was ich damit machen kann ist, dass ein follower zum Beispiel nur bestimmten Tabellen followed. Und alle anderen Operationen einfach ignoriert. Der kann also quasi nur einen Teil der Daten behalten. Und das ist so ein feature, das seit ein paar Datenbanken existiert.

Stefan Tilkov: Das sind alles Varianten, wo wir dieses Leader-Follower-Modell haben und die Einschränkung, wie du gerade schon gesagt hast, ist, dass wir einen leader haben. Wenn ich etwas schreiben will, dann gehe ich immer zur selben Stelle. Lass mich raten, mit dem nächsten Modell heben wir diese Restriktion auf.

Lucas Dohmen: Genau. Es gibt das sogenannte Multi-Leader-Modell, überraschender Name, wo ich jetzt mehrere leader haben kann, und da kaufen wir uns jetzt ein Problem ein. Und zwar: write conflicts. Beim Lesen gibt es jetzt nicht so viele Konflikte, aber beim Schreiben gibt es Konflikte. Was ist, wenn meine eine Anwendung mit dem leader 1 spricht und meine andere mit dem leader 2 und beide ändern jetzt gerade deinen Benutzernamen? Und der eine ändert ihn auf “Stefan” und der andere auf “Tilkov”. Dann haben wir das Problem: Wer von den beiden hat jetzt Recht? Wer von den beiden hat die Wahrheit? Das ist ein sogenannter write conflict. Die passieren, je nachdem, wie hoch das Write-Volumen auf meiner Datenbank ist, sehr selten bis sehr häufig. Das ist unterschiedlich. Da muss die Datenbank eine Strategie anbieten, wie sie mit diesen Konflikten umgeht. Eine Lösung ist das sogenannte read repair. Wenn ich lese und es gibt zwei Versionen der Wahrheit, dann sagt mir die Datenbank Bescheid: Hey, da gibt es zwei Versionen, du musst jetzt entscheiden, welche richtig ist. Und dann wird die andere weggeworfen.

Stefan Tilkov: Das wäre wie mein guter, alter, jetzt sage ich schlimme Worte, Lotus-Notes-Replikationskonflikt.

Lucas Dohmen: Genau. Lotus Notes ist ja der geistige Vater von CouchDB und in CouchDB ist auch ein multi leader system und da gibt es dann auch die Möglichkeit, die aufzulösen. Dafür müssen wir aber auch erst einmal wissen, dass es einen Konflikt gab! Das ist ganz wichtig und das setzt Kausalität voraus. Es gibt zwei Möglichkeiten: Ich setze Kausalität voraus oder ich ignoriere Kausalität. Wenn ich Kausalität ignoriere, kann ich zum Beispiel sagen: Ich gucke einfach auf die Uhr und das, was neuer ist, also was den höheren time stamp hat, das behalte ich. Das ist die last write wins Strategie, um Konflikte zu lösen. Die hat den Nachteil, dass mir in dieser Philosophie Daten verloren gehen können.

Wir nehmen ganz einfach einen counter, da stand vorher “3” drin, du liest “3”, machst “4” daraus, ich lese “3”, mache “5” daraus, dann wäre das richtige Ergebnis ja vermutlich “6”, weil wir irgendwie etwas hoch zählen zusammen. In diesem last write win würde aber jetzt entweder “5” oder “4” drinstehen. Das ist ja falsch, es ist einfach etwas verloren gegangen. Und deswegen gibt es durchaus Möglichkeiten, wie wir feststellen können, welchen Wert kanntest du denn als letztes? Wir könnten zum Beispiel sagen, wir versionieren alle Schreibzugriffe auf eine bestimmte Zelle, und wenn du schreibst, musst du sagen: Ich schreibe und das, was ich als letztes kannte, das war die Version fünf. Und wenn da dann der andere sagt: Ich habe auch geschrieben und das letzte, das ich kannte, ist Version fünf und nicht deine Version sechs, dann weiß ich, ihr beide wusstet nichts voneinander, als ihr geschrieben habt, also muss ich jetzt einen Konflikt auflösen. Und in der CouchDB-Welt beispielsweise, bedeutet das jetzt erstmal, der Leser muss mit seinem Business-Verstand, also mit der Business-Logik in meiner Anwendung, diesen Konflikt irgendwie lösen können. Das ist bei einem counter einfach, in dem Fall müsste man jetzt einfach die “6” da hinein schreiben, bei anderen Sachen ist es vielleicht nicht so einfach.

Stefan Tilkov: Das erinnert mich so ein bisschen an die alte Strategie, dass man konkurrierende Zugriffe auch bei einer einzelnen Datenbank irgendwie abgleicht, wie wenn man sich beim Schreiben vergewissert, dass das auch zu dem passt, was man gelesen hat, nur ist der Unterschied bei dem, was du jetzt geschildert hast, eben, dass es mehrere Stellen gibt, an denen geschrieben wird. Deswegen reicht es nicht, wenn ich mich beim Schreiben einfach vergewissere, dass das passt, weil das sehe ich ja nicht. Ich sehe ja eben nur, was ich vorher schon gelesen habe auf diesem leader, während ein anderer auch einen für sich völlig konsistenten Zustand hat und erst beim Replizieren merkt die Datenbank, dass das Ganze nicht zusammenpasst.

Lucas Dohmen: Genau. Weil jeder leader muss ja wieder zu den anderen, die dann auch replizieren. Er muss ja sagen: Ich habe jetzt übrigens dieses Datum verändert, mach das auch. Genau. Dann gibt es eine dritte Variante, wir hatten single leader, multi leader und dann gibt es als Letztes…

Stefan Tilkov: Moment, ich muss noch etwas fragen. Bei den multi leadern. Du hast vorhin gesagt, ich kann das beim Lesen auflösen, also beim Lesen merke ich ja, dass ich zwei Einträge da drin habe und dann muss mein Client irgendwie entscheiden, was auch immer passiert. Hast du auch noch über andere Möglichkeiten gesprochen, also könnte ich nicht sicherstellen, dass, wenn ich schreibe, ich dann doch wieder auf alle koordiniert schreibe, ist das nicht auch eine Lösungsstrategie?

Lucas Dohmen: Ah, ok. Es gäbe auch noch die Variante: Ich löse Konflikte beim Schreiben, aber das funktioniert halt nicht so richtig, weil das Problem ist, wenn jetzt die ganzen leader erstmal - also ich bin jetzt ein leader und mir sagt jemand: Bitte schreib “5”. Und dann sage ich jetzt erstmal zu dir, als anderem leader: Bitte schreib “5”. Dann habe ich nichts gewonnen in meinem multi leader set up. Weil, jetzt ist es ja genauso langsam, als hätte ich nur einen leader. Weil jetzt muss ich ja wieder auf alle leader warten, bis ich etwas schreiben kann. Deswegen ist das eine Möglichkeit, aber keine sinnvolle.

Stefan Tilkov: Ok.

Lucas Dohmen: Genau und dann gibt es als dritte Möglichkeit noch, die Leaderless-Datenbanken, die gar keine leader haben. Und da ist es jetzt so: Ich habe jetzt, sage ich mal, drei Knoten. Dann gehe ich als Client hin und schreibe auf alle drei und sage bei allen dreien: Bitte schreibe diesen Wert. Und wenn alle drei geschrieben haben, dann sage ich: Ok, ich habe geschrieben. Dann habe ich keinen leader, wenn einer von denen abstürzt, ist es egal, ich kann auf die anderen zwei noch schreiben, das ist leaderless. Wenn ich jetzt immer auf alle schreiben würde, hätte ich wieder nichts gewonnen, aber da gibt es jetzt die Variante des sogenannten Quorums. Das heißt, ich entscheide, beim Schreiben müssen mindestens zwei von meinen drei Datenbanken diesen write kriegen und bestätigen. Sonst nehme ich das nicht an. Oder egal, ich schreibe auf eine, das reicht, das wäre auch ein Quorum. Da gibt es jetzt die Variante: Entweder macht das jetzt der Client, der hat das einprogrammiert und spricht mit allen oder es gibt so Coordinator-Knoten, die sorgen dafür. Also du sprichst mit dem coordinator und der verteilt das dann auf die Knoten.

Stefan Tilkov: Theoretisch könnte einfach irgendein Knoten diese Rolle annehmen, ich könnte einfach sagen, ich wähle irgendeinen und der spielt dann für diesen Request eben den coordinator.

Lucas Dohmen: Genau. Es ist eher eine Rolle, als ein dedizierter Knoten oder so. Genau. Und das ist das sogenannte write quorum: ich sage auf so und so viele Knoten muss ich schreiben, bevor das okay ist. Und es gibt das sogenannte read quorum, das heißt, wenn ich lese, lese ich nicht von einem Knoten, sondern ich lese von mehreren Knoten. Und wenn wir jetzt unsere drei Knoten hätten, dann könnten wir beispielsweise sagen, wir machen ein write quorum von zwei und ein read quorum von zwei. Das würde bedeuten, ich schreibe jetzt auf zwei von den Knoten und jetzt liest jemand und der liest auch mindestens von zwei Knoten, das heißt, es muss ja mindestens einer dabei sein, der mein write gekriegt hat und so kann ich Konsistenz herstellen. Vorausgesetzt keiner von den Knoten stürzt ab zwischendurch, dann kann natürlich trotzdem etwas verloren gehen. Das sind die Leaderless-Datenbanken, die nennt man auch Dynamo-Datenbanken, weil diese Idee von Amazon kommt, die haben die Datenbank Dynamo geschrieben und darüber mehrere Paper geschrieben, die Dynamo Papers und da haben sie halt dieses System beschrieben.

Stefan Tilkov: Ok. Das, wie viele andere Dinge auch, packen wir natürlich in die Shownotes. Wir haben extrem exzessive Shownotes. Sind wir jetzt durch mit den Modellen?

Lucas Dohmen: Das sind die Modelle, die ich kenne. Genau und Beispiele für Leaderless-Datenbanken wären Riak, Cassandra und Voldemort. Und für multi leader ist das größte Beispiel, das ich kenne, eigentlich nur CouchDB. Und single leader sind fast alle SQL-Datenbanken, Redis, das sind so Systeme, wie man die einordnen könnte.

Stefan Tilkov: Jetzt hast du einen Punkt vorhin noch erwähnt, den können wir vielleicht jetzt noch in diese Episode mit hereinnehmen, bevor wir dann einen Cut machen und uns den Rest für die zweite Episode aufsparen. Und zwar hast du vorhin gesagt, dass wir über zwei Aspekte bei Verteilung sprechen, einmal über die Replikation, das haben wir jetzt gemacht, alle haben die gleichen Daten und wir müssen uns Gedanken darüber machen, ob wir überall lesen dürfen oder überall schreiben dürfen oder nur in Teilen schreiben dürfen, der andere Aspekt waren die großen Datenmengen, als Grund für die Verteilung. Lass uns doch darüber noch kurz sprechen.

Lucas Dohmen: Genau. Viele Datenbanken wollen ja ihre Work… - also wenn wir über einen Datenbank sprechen, sprechen wir einmal über die Gesamtdatenmenge, die wir haben, und über das sogenannte working set. Das ist der Teil der Daten, der heiß ist, der immer superschnell verfügbar sein soll und deswegen im RAM zur Verfügung stehen soll. Weil ich kann natürlich eine richtig große Festplatte haben und dann alles von der Festplatte lesen, das wäre ein bisschen langsam. Aber bei bestimmten Daten, die oft gebraucht werden, möchte ich auf jeden Fall, dass die immer aus dem RAM gelesen werden. Wenn also meine Daten in den RAM passen, den ich mir leisten kann, dann kann ich das auf einen einzelnen Knoten packen. Wenn ich hingegen mehr Daten habe, als in den RAM passen, den ich mir für eine einzelne Maschine leisten möchte, dann muss ich mir überlegen: Wie sorge ich dafür, dass nicht jede Maschine alle Daten haben muss? Und das nennen wir dann Partitionierung. Also ich sage: Bitte alle Daten, die von meinem Kunden XY sind, landen auf diesem Server und alle Daten, die von meinem Kunden AB sind, landen auf diesem Server. Das wäre eine Art von Partitionierung. Und dann muss jemand, der lesen oder schreiben will, wissen: In welcher Partition befindet sich das, was ich suche? Und dann kann er halt diesen dediziert ansprechen. Und das ist das System der Partitionierung.

Stefan Tilkov: War das, was du gerade gesagt hast, ein Beispiel zur Illustration oder war das realistischerweise so, dass ich mir Gedanken machen muss, ob ich die Kunden aus der Gruppe A und die Kunden aus der Gruppe B jetzt auf verschiedene Systeme verteile, das klingt ja ziemlich aufwendig.

Lucas Dohmen: Genau, das klingt aufwendig, es ist aber etwas, das einige Leute machen. Beispielsweise gibt es ja ein großes shop system as a service, Shopify, die machen das beispielsweise so, dass sie ihre Partitionierung nach Kunden machen, weil ein Problem, das ich bei der Partitionierung habe, ist, wenn ich jetzt eine Anfrage stellen möchte, die zwei Partitionen betrifft, dann wird es teuer für mich, weil ich jetzt wieder viele Netzwerk-Requests machen muss, um eine query auszuwerten. Ich möchte immer versuchen, dass meine queries immer nur eine Partition betreffen, damit ich das auf einem einzelnen Knoten ausführen kann. Und deswegen ist das ein nicht ganz unattraktives Modell, wenn ich so einen ganz klaren Schnitt habe, wie zum Beispiel bei Shopify.

Stefan Tilkov: Weil die einfach wenig kundenübergreifende use cases haben. Verstehe.

Lucas Dohmen: Es gibt aber auch viele Leute, die können das nicht einfach so sagen, die wollen aber trotzdem ihre großen Datenmengen ablegen und da gibt es als ganz einfache Strategie, dass ich mir den Primärindex angucke und ich sage: Alle mit dem Index von null bis dreitausend kommen in den ersten Teil, alle von dreitausend bis sechstausend kommen in den zweiten Teil und so weiter. Man kann das so zerstückeln. Da benutze ich meinen Primärindex mit einer key range einfach, um das zu verteilen. Jeder von diesen Abschnitten ist ein shard und wir sagen: Okay, das ist der shard und das ist der shard und das ist der shard und so kann dann auch jemand, der liest, sagen: Okay, ich weiß, dass mein key in der und der key range ist, also muss ich den shard fragen.

Stefan Tilkov: Ein shard ist ein Splitter, eine Scherbe…

Lucas Dohmen: Genau. Eine Scherbe meiner Daten. Genau. Und das ist die eine Möglichkeit. Wenn aber zum Beispiel mein Schlüssel der Nachname der Person oder der volle Name der Person ist - vielleicht kein superguter Schlüssel, aber nehmen wir den jetzt mal - dann wäre das Problem, dass, wenn ich jetzt sage: Alle von A-F sind auf diesem und alle von G-E sind auf diesem, dass dann vielleicht die von M-L viel größer sind, als die anderen…

Stefan Tilkov: …als die von X-Z oder so, ja.

Lucas Dohmen: Genau. Und deswegen gibt es als weitere Variante, dass ich, bevor ich diesen key nehme, um zu sharden, ich ihn erst hashe und dann sharde. Und dann habe ich hoffentlich, wenn ich einen guten Hashing-Algorithmus habe, eine gute Verteilung auf meine shards. Und interessant ist natürlich, dass wir Replikationen und sharding auch kombinieren können. Wir können sagen: Wir teilen alles in shards auf und jede von den shards replizieren wir. Und dann können wir so dafür sorgen, dass jedes auf mindestens drei Servern liegt, aber ich fünf Server und jedes Datum liegt auf mindestens drei von denen. Und wenn dann zwei abstürzen, habe ich auf jeden Fall keine Daten verloren.

Stefan Tilkov: Okay. Haben wir jetzt alles gesagt, was wir zu sharding sagen müssen?

Lucas Dohmen: Ich glaube. Das ist ein Thema, über das man noch viel mehr reden kann, wir versuchen ja so ein bisschen einen Überblick zu geben, man kann zum Beispiel auch über einen sekundären Index sharden, zusätzlich, dann wird es aber schnell kompliziert, da würde ich jetzt nicht ins Detail gehen, aber wer sich so etwas angucken will, der sollte sich auf jeden Fall die Dynamo-Datenbanken angucken, weil die behandeln alle dieses Thema.

Stefan Tilkov: Vielleicht ganz kurz noch: Welche Datenbanken unterstützen so etwas?

Lucas Dohmen: Sharding ist ein ganz großer Stärkepunkt von den drei Dynamo-Datenbanken, also von Riak, Cassandra und Voldemort, aber andere Datenbanken unterstützen das auch, beispielsweise ArangoDB unterstützt das, verschiedene Datenbanken. Das ist ein gar nicht so wenig verbreitetes Thema, es ist aber halt für manche so quasi der Kernaspekt der ganzen Datenbank, das ist gerade bei den Dynamo-Datenbanken so, bei anderen ist es mehr so ein Nebenschauplatz.

Stefan Tilkov: Gut, wir haben jetzt über grundlegende Konsistenzmodelle gesprochen, wir haben über Replikation und über sharding gesprochen. Das soll für diese Episode erstmal reichen. In der nächsten Episode beschäftigen wir uns dann damit, wenn etwas schiefgeht. Ich freue mich schon darauf! Danke bis jetzt, Lucas.

Lucas Dohmen: Sehr gerne! Dann bis zum nächsten Mal.

TAGS

In Memoriam ∞ CEO & Principal Consultant

Stefan was a founder and Principal Consultant at INNOQ Germany, where he spent his time alternating between advising customers on new technologies and taking the blame from his co-workers for doing so. He was a frequent speaker at conferences and author of numerous articles.

We mourn the loss of Stefan.

Alumnus

Lucas was a senior consultant at INNOQ until August 2023. He works on the architecture, conception, and implementation of web applications on the front and back end. He programs in Ruby and JavaScript and helps with technology decisions & the adoption of different NoSQL solutions. Lucas is one of the authors of the book “The Rails 7 Way”. You can hear his voice on the INNOQ podcast quite regularly. He does open source and community work (like organizing and teaching at the local CoderDojo).