Shownotes & Links
- The network is reliable
- A few reminders about CAP
- Highly Available Transactions: Virtues and Limitations
- The trouble with timestamps
- CRDTs: Consistency without concurrency control
- A Comprehensive Study of Convergent and Commutative Replicated Data Types
- CRDT Library in Ruby
Transkript
Lucas Dohmen: Guten Tag.
Stefan Tilkov: Lucas, stellst du dich auch heute noch einmal ganz kurz vor?
Lucas Dohmen: Ich bin Lucas und arbeite für innoQ als Senior Consultant. Ich beschäftige mich mit dem Web, Front-End, Back-End, Datenbanken und all diesem Kram.
Stefan Tilkov: In der letzten Episode, die man wahrscheinlich gehört haben sollte, bevor man diese anhört, haben wir über Datenbanken und verteilte Datenbanken gesprochen: Sobald man eine einzelne Datenbank hat, die mit mehreren Clients spricht, spricht man über das Thema ‚verteilte Datenbanken‘. Wir haben uns über Konsistenzmodelle unterhalten; dabei waren Linearisierbarkeit und Serialisierbarkeit Beispiele für solche Konsistenzmodelle, und dass man die Datenbank selbst natürlich auch noch verteilen kann. Dabei muss man sich auf der einen Seite mit Replikationen bei verschiedenen Modellen auseinandersetzen und mit ‚Sharding‘, also der Aufteilung der Daten auf verschiedene Modelle. Worüber sprechen wir heute?
Lucas Dohmen: Bei allem, worüber wir bis jetzt gesprochen haben, sind wir von dem ‚perfekten Netzwerk‘ ausgegangen und auch da können schon bestimmte Probleme auftreten, zum Beispiel Schreibkonflikte usw. Nun ist es aber leider so, dass die verteilten Systeme und insbesondere die verteilten Datenbanken von bestimmten Problemen gequält werden, die leider nicht änderbar sind. Über diese Probleme, Lösungsansätze und Konsequenzen möchten wir heute sprechen.
Stefan Tilkov: Also lass uns doch über das stabilste aller Elemente sprechen, nämlich das Netzwerk.
Lucas Dohmen: Genau.
Stefan Tilkov: Wenn ich so ein Netzwerk habe, dann geht da ja nichts schief, oder?
Lucas Dohmen: Genau, da kann im Prinzip gar nichts schief gehen, deswegen sollte man alles immer verteilen.
Stefan Tilkov: Okay, fangen wir an. Wenn wir von Netzwerken reden, sprechen wir von dem, was wir normalerweise gewohnt sind: TCP/IP Netzwerke. Wollen wir uns da von unten nach oben langsam annähern?
Lucas Dohmen: Genau, wenn wir über TCP/IP sprechen, müssen wir erst einmal über IP sprechen. IP ist erst einmal asynchron, ein asynchrones Protokoll, das heißt, dass es keine Ordnung gibt: Wenn du mir zwei Nachrichten schickst, dann kann es sein, dass erst die Erste ankommt und dann die Zweite oder erst die Zweite und dann die Erste.
Stefan Tilkov: Oder nur eine von beiden.
Lucas Dohmen: Oder nur eine von beiden. Das hat erst einmal nichts mit Ordnung zu tun. Das IP Protokoll gibt da keine Garantie. Genauso kann es auch sein, dass das Paket über IP verloren geht und nie bei mir ankommt und ich bekomme es nicht mit, du bekommst es nicht mit, es ist einfach weg. Genauso kann es aus verschiedensten Gründen passieren, dass dein Paket zwei Mal bei mir ankommt und ich damit das Paket zwei Mal habe und vielleicht irgendetwas doppelt mache. Es ist auch noch so, und das ist das größte Problem über das wir gleich noch im Detail sprechen werden, dass es langsame Pakete geben kann, bei denen es ewig dauert, bis sie ankommen. Die drei Probleme, die ich jetzt als erstes genannt habe, sind welche, die TCP löst. Dafür, dass die geordnet ankommen, gibt es die ‚sequence numbers‘, also jedes Paket bekommt eine Sequenznummer: Kommt jetzt erst 2 und dann 1 an, dann weiß ich, da muss ich erst einmal auf 1 warten, bevor ich etwas mit 2 anfangen kann und dann ordne ich sie wieder um. Genauso gibt es auch noch das ‚ACK‘: Wenn du mir etwas schickst, dann sage ich dir ‚ist angekommen‘, damit du weißt, dass es funktioniert hat. Dadurch verhindere ich zwar keine Paketverluste, aber ich bemerke sie und kann dann das Paket noch einmal schicken. Durch die Sequenznummer kann ich auch dafür sorgen, dass ich bei Paketen, die zweimal bei mir angekommen sind, eines weg werfe.
Das ändert aber erst einmal nichts daran, dass manche Pakete ziemlich lange brauchen. Es verstärkt das Problem sogar noch: Wenn du mir zwei Pakete schickst und das zweite geht super schnell übers Netzwerk und das erste geht ganz langsam, dann ist das zweite schon bei mir angekommen und ich kann damit nichts tun und muss erst einmal warten, bis das Erste kommt. Die Zeiten, die da verstreichen, sind nicht zu vernachlässigen. Es wird auch immer schlimmer, wenn das Netzwerk mehr belastet ist, wenn meine Router stärker belastet sind usw., dann wird dieses Problem größer.
Ein anderer Ansatz ist da ‚UDP‘, wo man darauf hofft, dass etwas ankommt. Deshalb ist es schneller, aber es hat die Eigenschaft, dass Informationen verloren gehen können. Wir können uns jetzt vorstellen, gerade wenn wir über eine Datenbank sprechen, dass das natürlich keine tollen Bedingungen sind. Wenn zum Beispiel ein Paket mit einer Schreibanweisung verloren geht, dann mache ich das gar nicht, weil ich nie davon gehört habe, das ist schlecht. Deswegen gibt es, glaube ich, wenig Diskussion darüber, dass man immer TCP benutzt, wenn man mit einer Datenbank spricht, um trotz des asynchronen Netzwerkes bestimmte Probleme zu verringern.
Stefan Tilkov: Dann geht auch gelegentlich mal etwas schief. Gelegentlich ist das Netzwerk einfach mal nicht da, oder kurz mal nicht da oder so.
Lucas Dohmen: Richtig, das ist eine ganz große Problemklasse, die viele einfach, ich glaube, ignorieren, weil sie einfach hoffen, dass es nicht passiert. Zum Beispiel haben wir ein Rack mit drei von meinen Servern und ein zweites Rack mit drei weiteren Servern: Da kann es natürlich sein, dass die Verbindung zwischen diesen beiden Racks aus irgendeinem Grund verloren geht und sie jetzt nicht mehr miteinander reden können. Was macht die Datenbank jetzt? Sie kann entweder sagen: „Ich arbeite weiter“ oder „Ich höre auf zu arbeiten“. Das ist etwas, worüber wir gleich noch sprechen. Die Datenbank muss sich über diese Probleme bewusst sein.
Interessant ist auch, viele stellen sich nun vor: Da muss jetzt das Kabel heraus gezogen worden sein oder so, aber es kann auch einfach sein, dass eine bestimmte Netzwerkverbindung in meinem Rechenzentrum überlastet ist und total langsam wird. Für mich als Anwender ist das nicht unterscheidbar, ob das eine gecuttete Netzwerkverbindung oder eine super langsame Verbindung ist. Wenn ich beispielsweise immer Heartbeats schicke, um zu schauen: „Bist du noch lebendig, bist du noch lebendig…“ und die Nachricht braucht auf einmal 30 Sekunden, dann gehe ich erst einmal davon aus: Du bist leider abgestürzt und ich kann nicht mehr mit dir reden. Die Konsequenz daraus ist noch viel schlimmer, als wenn du wirklich abgestürzt wärst. Wenn du wirklich abgestürzt wärst, ich schicke dir eine Nachricht, die kommt nicht an, dann weiß ich, dass du leider weg bist, wir müssen damit klar kommen und wir brauchen einen neuen Stefan. Aber wir können damit umgehen. Wenn du in Wirklichkeit aber noch da bist und die Nachricht kommt jetzt 30 Sekunden später an, während dich alle anderen quasi abgeschrieben haben und den Knoten ersetzen müssen, kommst du plötzlich wieder hoch und hast diese ganzen Nachrichten empfangen und weißt nicht, dass die jetzt ewig lang gebraucht haben. Da können ganz interessante Effekte passieren: Du denkst vielleicht, dass alle anderen weg sind und lebst jetzt erst einmal alleine weiter.
Stefan Tilkov: Ich glaube, wahrscheinlich bekommt jeder bei der Vorstellung Kopfschmerzen, dass so ein Problem nicht nur auftritt, sondern irgendwann auch wieder aufhört und dann auf einmal wieder alle zueinander finden müssten. Sie müssten überlegen: Haben sie jetzt schon für diesen Ausfall korrigiert oder nicht? Und wie bekommt man das auseinander sortiert? Das ist genau die Herausforderung bei dem Ganzen.
Lucas Dohmen: Richtig. Da gibt es viele Geschichten von größeren Firmen, zum Beispiel GitHub, die einmal ein ganz großes Problem mit ihren MySQL Servern hatten, wo ein Netzwerk-Link ganz langsam wurde. Wir hatten über single leader und multi leader gesprochen. In einem single leader-Setup hat man vielleicht eine automatische leader election, das heißt wenn die Lesenden denken: Der leader ist tot, wir ernennen dich zu unserem neuen leader. Aber in Wirklichkeit ist er gar nicht tot, sondern nur sehr langsam und lebt weiter. Wenn er dann wieder da ist, sagt er: Ich möchte jetzt weiter schreiben. Da kann es zu großen, langen Ausfällen kommen. Und das sind Probleme, die viele Leute extrem unterschätzen, weil sie nur von dem einfachen Fall ausgehen, das Netzwerk sei gekappt, aber ein viel häufigeres Problem ist, dass das Netzwerk langsam geworden ist. Da gibt es dann Sachen wie Timeouts, wo man entscheiden muss, wie viel Timeout genommen werden kann oder nicht und das macht das Netzwerk zu einem sehr teuflischen Ding.
Stefan Tilkov: Ein Begriff, den ich häufig gehört habe, ist Partition. Kannst du uns kurz erklären, was das ist?
Lucas Dohmen: Eine Partition ist ein ‚durchgeschnittenes‘ Netzwerk. Statt einem Netzwerk haben wir nun zwei Netzwerke. Aus der einen Partition kann ich nicht mehr in die andere Partition. Der einfache Fall, wie gesagt, ist, dass das Kabel zwischen den beiden gekappt ist, aber auch ein sehr langsames Netzwerk ist eine Netzwerk-Partition.
Stefan Tilkov: Wie können wir uns dem jetzt nähern? Wann ist denn alles gut? Wie definieren wir denn die Situation, in der alles gut ist?
Lucas Dohmen: Die Situation ist gut, wenn alle mit allen sprechen können.
Stefan Tilkov: Also auch, wenn sie mir Fehler zurück liefern, ist das dann auch Teil der ‚Gut-Situation‘?
Lucas Dohmen: Wenn sie untereinander sprechen, um herauszufinden, ob alle noch da sind, dann ist das gut, wenn diese Nachrichten durchkommen und man weiß, dass dieser Server sagt: Ich kann nicht mehr und ich habe mich auch ausgeschaltet. Dann ist das für uns gut, weil wir nicht das Problem „Netzwerk“, sondern das Problem „Server ist ausgefallen“ haben. Wenn wir fragen: Geht es dir gut und es kommt keine Antwort durch, dann ist das nicht gut.
Stefan Tilkov: Wie definieren wir denn Verfügbarkeit?
Lucas Dohmen: Das ist ein Begriff, der oft falsch verwendet wird. Wir reden in einem verteilten Netzwerk über Maschinen als Knoten, denn es könnten auch virtuelle Maschinen oder Docker Container sein (oder was als nächstes kommt, Micro-Kernels…). Dann reden wir von einem Knoten, der verfügbar ist, wenn er mir sinnvolle Antworten liefern kann. Eine nicht sinnvolle Antwort ist zum Beispiel wenn ein 500er Fehler geworfen wird. Ich habe beispielsweise ein System mit drei Knoten gebaut, und wenn der eine Knoten mit den anderen zweien nicht mehr reden kann, dann wirft er auf alles, was ich frage, einen 500er Fehler. Dann ist er nicht verfügbar.
Stefan Tilkov: Obwohl er im Netz ist.
Lucas Dohmen: Genau, er ist im Netz, er ist nicht kaputt, sein Speicher ist in Ordnung, alles ist gut. Aber er antwortet mir nicht mehr, weil er seine Partner nicht erreichen kann, also ist er nicht verfügbar.
Stefan Tilkov: Einverstanden.
Lucas Dohmen: Wenn wir jetzt auf ein Gesamtsystem schauen, dann können wir auch von dem Gesamtsystem als verfügbar oder nicht verfügbar sprechen. Eine totale Verfügbarkeit bedeutet, alle Knoten, die nicht abgestürzt sind, sind verfügbar. Alle von denen geben mir sinnvolle Antworten. Es gibt den weicheren Begriff der hohen Verfügbarkeit oder high availability, über den man oft spricht. Das ist in seiner Bedeutung ein nicht klar definierter Begriff. Für mich bedeutet das: Die Mehrheit der Knoten, die nicht abgestürzt sind, sind verfügbar. Das ist also Verfügbarkeit, wenn man auf ein Gesamtsystem schaut: Ist es ein total verfügbares, ein hoch verfügbares oder ein nicht hoch verfügbares System?
Stefan Tilkov: Vielleicht hat es der ein oder andere Zuhörer schon bemerkt, wir haben jetzt drei Begriffe im Laufe dieser Episode erwähnt. Wir haben am Anfang über Konsistenzmodelle gesprochen, zwischendurch Partition erklärt und haben gerade über Verfügbarkeit, also availability gesprochen. Das ist also consistency, partition tolerance (wobei wir letzteres nicht genannt hatten) gesprochen. Kannst du uns das irgendwie zusammenbauen und erklären, was das jetzt alles miteinander zu tun hat?
Lucas Dohmen: Also, wenn ich über eine Datenbank spreche, dann muss ich darüber sprechen, wie sie sich verhält, wenn so eine Partition auftritt. Ganz intuitiv: Ich habe meine drei Datenbankserver. Jetzt antwortet der eine auf einmal nicht mehr. Was tut meine Datenbank? Sie hat im Prinzip zwei Möglichkeiten. Entweder ist es ihr egal, sie antwortet trotzdem und tut so, als wäre sie weg und ich antworte weiterhin oder sie sagt "Stop, ich kann jetzt nichts mehr tun solange der eine Server nicht da ist“, kann sie nicht mehr Konsistenz garantieren und sendet nur noch 500er. Das ist das sogenannte ‚CAP‘-Theorem: Entweder gehen wir diesen einen Weg oder wir gehen diesen anderen. Auch wenn es wesentlich wissenschaftlicher bewiesen wurde, als ich das gerade gemacht habe, ist das für mich auch intuitiv irgendwie klar. Ich muss einen von diesen beiden Wegen gehen.
Stefan Tilkov: Eine Verkomplizierung, die einen immer wahnsinnig macht, bis man sie irgendwann einmal durchdringt ist, dass diese Begriffe x-fach belegt sind. Dass das ‚C‘ im CAP Theorem, Consistency, eigentlich in dem, was wir vorher gesagt haben, nur ein bestimmtes Konsistenzmodell ist, richtig?
Lucas Dohmen: Nein, jein.
Stefan Tilkov: Danke, da dachte ich einmal, ich wüsste was, aber ist in Ordnung, ich kann damit umgehen.
Lucas Dohmen: Ich persönlich bin kein großer Fan von dem CAP-Theorem. Dieser intuitive Konsistenzbegriff ist nicht ausreichend, da hast du schon Recht, also was bedeutet konsistent? Viel interessanter ist, und darüber hatten wir schon in der ersten Folge kurz gesprochen, die Kategorisierung von Peter Baileys, der diese ganzen Konsistenzmodelle kategorisiert hat. Auf den Baum bezogen, diesen Teil des Baumes kannst du niemals haben, wenn du high available sein möchtest.
Stefan Tilkov: high available heißt jeder Knoten, der da ist, kann antworten. Aber war high available nicht die Mehrheit der Knoten?
Lucas Dohmen: Ok – richtig. Wenn ich total availability will, dann sind diese [Konsistenzmodelle] nicht verfügbar für mich.
Stefan Tilkov: Also bestimmte Konsistenzmodelle sind mit total availability nicht zusammen zu bringen. Das wäre dann die gleiche Aussage wie aus dem CAP Theorem: Bestimmte Sachen gehen einfach nicht. Sie sind nicht nur mühselig oder schwierig, sie gehen einfach nicht, weil man es nicht machen kann. Entweder ich gebe diese total availability auf und gehe auf high availability -
Lucas Dohmen: Also selbst high availability, wir können eigentlich nicht über high availability reden, wenn wir über diese Forschung sprechen, weil high availability keine feste Definition hat. Deswegen sprechen wir lieber über total availability, weil es da keine Deutung gibt.
Stefan Tilkov: Einverstanden. Das war jetzt alles nur wieder Geschwurbel über das, was du gerade im Prinzip schon gesagt hast, das, was intuitiv ist: Wenn ich mir zwei Maschinen vorstelle, und die müssen ständig synchron gehalten werden und ich kann eine von den beiden nicht erreichen, dann muss ich mich entscheiden, ob ich aufhöre zu arbeiten – da stelle ich sicher, dass die nicht out of synch gehen – oder ich akzeptiere, dass ich mit einer davon arbeite und riskiere, dass das irgendwie auseinander läuft und dann wieder zusammen gebracht werden muss. Das ist in der Tat intuitiv.
Lucas Dohmen: Und wenn man das gerne weitertreiben und mehr Verständnis dafür haben möchte, dann kann man sich angucken, was Peter Baileys erforscht hat, und das ist bewiesen und wesentlich mehr, als man in einem Podcast erklären kann oder teilweise wesentlich mehr, als ich verstehen kann. Er hat für verschiedene dieser Konsistenzmodelle gezeigt, das kann gar nicht gehen, wenn du total availability hast. Das Erschreckende daran ist, dass es extrem viele von diesen Konsistenzmodellen sind. Linearisierbarkeit ist beispielsweise eine davon und kann es nicht geben, wenn man total availability haben möchte. Dass das nicht geht, ist erst einmal traurig, aber es ist leider nicht veränderbar, es ist einfach so. Wir können das etwas aufweichen, zum Beispiel gibt es da so eine ‚Abart‘ von der high availability, namens stick availability und das bedeutet, dass jeder Client immer mit dem gleichen Server spricht. Also sticky wie eine sticky session. Dann sind ein paar verfügbar, die unter der total availability nicht verfügbar sind, aber es sind immer noch nicht besonders viele. Ist meine Datenbank also ‚CP‘ oder ‚AP‘? Das heißt: Ist sie konsistent oder ist sie available?
Stefan Tilkov: Das klingt so absolut, aber die Frage ist, was sie priorisiert, richtig?
Lucas Dohmen: Genau.
Stefan Tilkov: Also wenn sie CP ist, priorisiert sie Konsistenz sehr hoch, wichtiger als Verfügbarkeit. Und wenn sie AP ist, bedeutet das nicht, dass sie inkonsistent ist oder ihr Konsistenz egal ist, sondern, dass sie -
Lucas Dohmen: Immer im Falle einer Partition, wie entscheide ich mich?
Stefan Tilkov: Das heißt, wenn wir uns ein Oracle Cluster vorstellen, und man einfach davon ausgeht, dass es keine Partition gibt, dann ist das totaler Quatsch.
Lucas Dohmen: Genau, das steht nicht in den SLAs…
Stefan Tilkov: Wenn dann in meinem Oracle-Datenbank-Cluster eine Partition auftritt, wenn die Maschinen in diesem Cluster nicht mehr miteinander kommunizieren können, dann ist das so ein katastrophaler Fehler, dass wir da anhalten, zumindest wenn ein erheblicher Teil nicht mehr erreichbar ist. Die sind vielleicht der leichtere Teil, also lass uns auf den anderen fokussieren. Wir reden über diese Datenbanken, bei denen wir die Verfügbarkeit unglaublich wichtig nehmen und haben den Anwendungsfall, bei dem wir im Internet Bestellungen entgegen nehmen. Dann ist uns wahnsinnig wichtig, dass diese Bestellungen entgegen genommen werden und es ist nicht so unglaublich wichtig, dass die alle insgesamt konsistent sind, das heißt, egal welchen Knoten ich frage, ich immer die gleiche Anzahl an Bestellungen heraus bekomme. Es ist mir wichtiger, dass ich schreiben kann, als dass ich konsistent lesen kann. Ist das ein gutes Beispiel für so ein Verfügbarkeits-Ding? Irgendwann wird das dann auch wieder konsistent, was man dann Eventual consistency nennt.
Lucas Dohmen: Genau, gerade in einem deutschen Podcast ist es wichtig zu erwähnen, dass ‚Eventual-Consistency‘ nicht eventuelle Konsistenz ist. Eventual ist ein zeitlicher Aspekt, der ausdrückt: Irgendwann wird es konsistent sein. Wenn ich high available bin, bin ich aber nicht automatisch Eventual-Consistent, das ist ganz wichtig. Ich kann ein high available-System bauen, was einfach niemals konsistent ist. Es kommt zu einer Partition und es ist nichts mehr konsistent. Wenn ich den einen frage, bekomme ich fünf zurück, wenn ich den anderen frage, bekomme ich sieben zurück. Da ist das Konsistenzmodell: nicht konsistent.
Stefan Tilkov: Ein fragwürdiges Konsistenzmodell in der Tat.
Lucas Dohmen: Ja, ein sehr fragwürdiges Konsistenzmodell.
Lucas Dohmen: Und das ist auch ein Konsistenzmodell, das bei vielen Datenbanken leider die Realität ist. Eventual consistency heißt, für jedes Ding, das passiert, irgendwann werden alle darüber Bescheid wissen und sich einig sein. Wenn ich das erfüllen möchte, dann muss ich bestimmte Dinge tun, um das zu schaffen.
Stefan Tilkov: Was für Dinge sind das, wie kann ich das jetzt erreichen?
Lucas Dohmen: Wir haben da schon ein bisschen drüber gesprochen, als wir über die multileader gesprochen haben, über diese write-Konflikte. Wir müssen uns jetzt vorstellen, unser armer Cluster wurde in zwei Teile geteilt. Und die einen Clients reden mit dem einen Teil, die anderen Clients reden mit dem anderen Teil. Das ist das, was wir sagen, das ist okay in unserem high available system; wir haben das als akzeptabel bezeichnet. In der Zeit der Partition ist es natürlich so, dass, wenn ich mit dem einen Cluster rede, ich niemals Antworten vom anderen Cluster bekommen kann, weil es gerade eine Partition ist. Also ist es während der Partition nicht konsistent. Wenn ich hier frage, wie oft hat sich der Stefan eingeloggt, kommt drei Mal und hier fünf Mal, weil die sich einfach nicht einig sind. Irgendwann erholt sich das Netzwerk. Der Netzwerkadministrator ist aufgestanden, hat den Stecker wieder eingesteckt und jetzt können die beiden Teile wieder miteinander reden, dann müssen sie jetzt ja Entscheidungen treffen können, was passiert: Du hast das mitbekommen, ich habe das mitbekommen, was tun wir jetzt damit? Und da gibt es, wie gesagt, eine ganz, ganz einfache Lösung, das ist dieses last write wins. Jeder hat so eine Uhr in seinem Computer -
Stefan Tilkov: Hast du nicht gerade gesagt, die ganz einfache Lösung?
Lucas Dohmen: Ja, das ist die ganz einfache: Jeder schaut auf seine Uhr und sagt, dann und dann habe ich diese Änderung gemacht. Und wenn jetzt wieder beide miteinander sprechen können, dann schauen beide, okay, ich habe das um 13:02 Uhr bekommen und du hast das um 13:00 Uhr bekommen, wir nehmen das um 13:02 Uhr, das andere wird weggeworfen. Das ist last write wins.
Stefan Tilkov: Okay. Das klingt aber nicht so wirklich praktikabel, oder? Allein aus dem banalen Grund, dass wir uns dann darüber einig sein müssten, ob es gerade 13:02 ist oder nicht.
Lucas Dohmen: Genau. Das ist der eine Aspekt: Wenn wir jetzt davon ausgehen würden, die Uhr würde überall die gleiche Uhrzeit anzeigen - da können wir gleich gerne noch mal drüber reden - selbst dann ist es nicht gut, weil -
Stefan Tilkov: Das ist vielleicht nicht das, was ich will. So wie du in dem Beispiel in der letzten Episode gesagt hast, wenn wir beide etwas erhöhen, zum Beispiel.
Lucas Dohmen: Genau. Da haben wir darüber gesprochen, dass es dann ja passieren könnte, wenn wir beide einen Counter hochzählen und wir nehmen jetzt einfach einen von den beiden, dann sind ja die „Hochzähler“ bei dir nicht mehr da, die sind einfach verloren gegangen. Das heißt, selbst wenn diese Uhr total super wäre, hätte ich einen Schreibverlust. Aber dazu kommt, dass Uhren nicht ganz so cool sind, wie man erst einmal naiv denkt. Man geht erst einmal davon aus, eine Uhr ist monoton - also mathematisch gesehen monoton. Das heißt, es kann nie sein, dass es erst 13:02 Uhr ist und dann 13:01 Uhr. Die Zeit bleibt entweder gleich oder geht voran. Sie geht niemals zurück. Und man geht davon aus: Auf diesem Computer ist es 13:02 Uhr, dann ist es auch auf diesem Computer 13:02 Uhr. Beide Annahmen sind leider falsch. Zum Einen, das Monotone bei einer normalen Uhr, der sogenannten wall clock, ist nicht erfüllt, weil wir zum Beispiel Aspekte haben wie die leap seconds, die Schaltsekunden heißt das, glaube ich, auf deutsch.
Stefan Tilkov: Ja.
Lucas Dohmen: Also, wir wollen die Ungenauigkeiten von diesem historischen Kalender ausgleichen und deswegen einigen wir uns alle darauf, diese Sekunde machen wir alle zweimal, dann sind wir auf einmal nicht mehr monoton in unserer Uhr. Genauso kann es passieren, dass mein Computer ein bisschen zu schnell läuft in seiner Uhr. Und jetzt kommt der NTP-Server, der sagt, hey, das ist die richtige Uhrzeit, und ich sage, oh, ich bin zu schnell gelaufen, ich muss jetzt in meiner Zeit zurückgehen. Dann habe ich auf jeden Fall keine monotone Uhr mehr. Ich habe nicht mehr die Eigenschaft, dass ich sagen kann, das ist wirklich davor passiert, wenn ich nur auf diesen time stamp schaue. Und das ist schon ein bisschen beunruhigend, wenn man so darüber nachdenkt, dass diese Uhr eigentlich nicht so eine gute Idee ist.
Und da gibt es noch viele andere Aspekte, die darauf Einfluss nehmen. Ein wichtiger Aspekt von den Uhren ist auch das, was wir über die Netzwerke gesagt haben: Wenn wir über NTP reden, dann ist es jetzt erst mal so: Du bist der NTP-Server und ich bin der Server und ich sage, hey, gib mir mal die Zeit. Dann ist schon ein Problem da, weil das ja nicht instantan zurückkommt, sondern ich frage an und dann kommt die Antwort zurück. Das heißt, auch das ist schon eine Schätzung, dass wir irgendwo, wenn du sagst, es ist dieser Sekundenpunkt und ich muss ja dann schätzen, wo zwischen diesen zwei Punkten von meiner Anfrage und der Antwort hast du denn jetzt diese Zeit genommen? Natürlich haben sich da viele Leute schlaue Dinge für überlegt, aber es ist immer eine gewisse Approximation, wir können das nicht exakt sagen, dass es jetzt exakt diese Uhrzeit ist. Und diese Abweichung ist manchmal nur im Millisekundenbereich, manchmal ist sie im Nanosekundenbereich, dass man es gar nicht merkt, aber manchmal wird sie dann auch ein bisschen schlechter, wenn das Netzwerk zum Beispiel gerade überlastet ist und dann die Uhr synchronisiert wird. Und da wird es halt schnell gefährlich, wenn wir uns auf die Uhr verlassen. Und deswegen sollten wir uns eigentlich nie auf die Uhr verlassen, wenn wir über Reihenfolgen oder so etwas sprechen.
Stefan Tilkov: Was gibt es denn für Alternativen?
Lucas Dohmen: Eine Alternative ist - Wir wollen eigentlich immer wissen, auf welcher Basis passiert das, was gerade passiert? Wenn ich sage, ich setze von A auf B, dann möchte ich wissen, welche Version von diesem Datensatz ist das? Weil ich dann sagen kann, ich sehe gerade hier Datensatz 3 und ich möchte jetzt 4 erzeugen und mein Vierer-Datensatz basiert auf dem Dreier-Datensatz. Wenn du das gleichzeitig auch tust, dann hast du auch eine Version 4 und der Computer kann sagen, hier ist ein Konflikt passiert. Wir haben also eine Konfliktentdeckung, die jetzt passiert.
Stefan Tilkov: Das löst sozusagen das Problem der Uhr, der wall clock?
Lucas Dohmen: Genau: Wir benutzen statt einer wall clock so einen Kausalitätsbaum und den nennen wir vector clock. Das sind quasi einfach Kausalitätsketten. Und die sagen, okay, in dieser Kausalitätskette gab es leider eine Verzweigung, also muss irgendjemand mir helfen, die zu lösen.
Stefan Tilkov: Wir könnten dann immer noch ein last write win machen? Wir könnten als Strategie einfach immer den letzten gewinnen lassen?
Lucas Dohmen: Genau. Das sagt jetzt erst mal nichts darüber aus, wer gewinnt. Wir haben nur jetzt die Fähigkeit dazu bekommen, dass wir wissen, es ist ein Konflikt aufgetreten, aber es sagt noch nichts darüber aus, wie wir den lösen.
Stefan Tilkov: Das geht bei dem anderen theoretisch aber auch. Ich könnte auch über die wall clock bemerken - mit welcher Genauigkeit auch immer -, dass ein Konflikt aufgetreten ist und könnte den anders auflösen, anstatt den letzten gewinnen zu lassen. Oder ist das unüblich?
Lucas Dohmen: Ja, genau, aber in dem Fall würde ich mich schon wieder sehr auf die Uhr verlassen.
Stefan Tilkov: Ja, mit allen Nachteilen, die die Uhr hat.
Lucas Dohmen: Grundsätzlich gibt es das Thema Konfliktentdeckung und Konfliktlösung und das sind zwei unabhängige Themen.
Stefan Tilkov: Einverstanden. Gut, was können wir denn machen, um diese Konflikte besser aufzulösen?
Lucas Dohmen: Eine Möglichkeit, die zum Beispiel bei CouchDB eingebaut war, die bei Riak ganz lange der Standard war, ist der read repair. Darüber hatte ich auch kurz in der letzen Folge gesprochen: Wir sagen, wenn wir lesen, bekommen wir mit, das sind die zwei oder drei Versionen, die sein könnten, bitte löst das auf. Und dann geben wir der Applikation die Aufgabe, den Konflikt zu lösen.
Stefan Tilkov: Mit diesen vector clocks wäre das ja auch eine absolut gültige Strategie. Das lässt halt viel bei der Anwendung, die muss sich jetzt darum kümmern, aber sie hat zumindest eine gute Chance, sich darum zu kümmern. Sie weiß genau, was passiert ist.
Lucas Dohmen: Genau. Sie weiß, dass ein Konflikt da ist.
Stefan Tilkov: Sie weiß auch, wie der entstanden ist.
Lucas Dohmen: Richtig.
Stefan Tilkov: Weil sie sozusagen die Kausalitäten hat und sie kann dann selbst auf Anwendungsebene entscheiden, sind das jetzt zwei Produkte, die ich in einen Warenkorb gelegt habe, oder sind das zwei Namensänderungen oder zwei Adressänderungen oder sonst irgendetwas; sie kann sie unterschiedlich behandeln.
Lucas Dohmen: Genau. Und manche Konflikte kann die Anwendung vielleicht selbst lösen, weil sie sagt, okay, für Counter kann ich zum Beispiel das Problem lösen, aber ob der Name jetzt der ist oder der, das kann ich leider nicht entscheiden, aber ich übergebe sie dem User. Also der Benutzer meiner Anwendung bekommt jetzt bei dem nächsten Login die Aufgabe: Bitte entscheide, was denn jetzt richtig ist.
Stefan Tilkov: Einverstanden.
Lucas Dohmen: Und das wäre eine Lösung, die gerade in diesem CouchDB System sehr verbreitet ist. Von CouchDB gibt es ja auch im Client, also im Webbrowser, eine Version. Und da ist das halt ein ganz, ganz wichtiges Instrument. Und oftmals wird dann einfach dem User die Aufgabe übermittelt, bitte löse die Konflikte. Denn das verteilte System - jeder hat eine eigene Datenbank in seinem Browser - ist natürlich noch viel verteilter als in einem Datacenter und da tritt das dann auch relativ häufig auf und wir müssen diese Konflikte lösen. Das ist dann entweder manuelle Konfliktlösung oder halt automatische Konfliktlösung, aber mit Logik, die ich als Programmierer in meiner Anwendung schreibe.
Stefan Tilkov: Okay. Andere Möglichkeiten, das Problem zu lösen?
Lucas Dohmen: Genau. Eine andere Möglichkeit sind die sogenannten CRDTs, die [Conflict-Free Data Types]. Das sind Datentypen, die automatisch ihre Konflikt lösen können. Und das ist ziemlich cool.
Stefan Tilkov: Das hört sich auch cool an.
Lucas Dohmen: Genau, das hört sich ziemlich cool an. Im Prinzip habe ich das eben schon erklärt. Und zwar bei dem ganz einfachen Beispiel von dem Counter habe ich es nicht so gut erklärt, weil das zum Beispiel eigentlich nicht ganz automatisch auflösbar ist. Denn wenn ich wirklich… Ich habe fünf dazu bekommen, du hast fünf dazu bekommen. Du hast aber noch eine zweite Version von dir, die mir auch sagt, du hast fünf dazu bekommen, und ich versuche jetzt, das alles zusammenzubringen. Dann kommt nachher fünfzehn raus, weil ich von dir fünf genommen habe, von deiner Kopie fünf und von mir fünf, aber eigentlich wären es zehn. Das ist eine Variante, die wir nicht so einfach automatisch auflösen können.
Eine ganz einfache Sache, die wir auflösen könnten, wäre ein Maximum. Wir haben Zahlen, du hast eine Zahl und ich habe eine Zahl, wir wollen immer das Maximum wissen. Da wäre es jetzt so, wenn du mir sagst, du hast die 5, ich habe die 7 und du sagst mir noch mal, du hast die 5, oder dein Replikant sagt, du hast die 5, trotzdem kommt nachher 7 raus. Das ist einfach so, denn es ist das Maximum. Und das wäre ein CRDT: Das ist ein Datentyp, bei dem man einfach die verschiedenen Versionen nehmen kann und dann, egal, wie oft ich die verschiedenen Sachen darauf nehme, es kommt immer dasselbe Ergebnis raus. Das heißt, es ist unempfindlich dagegen, wenn ich zweimal dasselbe hineinwerfe, dass dann trotzdem das richtige Ergebnis herauskommt. Das ist beim Maximum zum Beispiel erfüllt, bei einem Counter nicht. Denn beim Counter würde jetzt immer der Counter höher steigen, wenn ich das noch einmal reinmerge.
Das Minimum und das Maximum sind ganz, ganz einfache Beispiele für einen CRDT. Und das heißt, in dem Fall ist es dann so, dass die Datenbank diese Aufgabe übernimmt. Die Datenbank weiß, dieses Feld enthält einen CRDT-Wert und zwar ein Maximum. Das heißt, sie hat eine gewisse Information über die Semantik von meinen Daten und kann dann die Entscheidung treffen, okay, ich hatte einen Konflikt, wir sind jetzt wieder vereint, wir Cluster, und jetzt kann ich die Daten zusammenführen. Ich brauche dafür keine Hilfe, ich brauche dafür keine Applikation oder sonst irgendetwas.
Stefan Tilkov: Das heißt, der Datentyp wäre dann einfach ein Datentyp, den meine Datenbank hat, wie sie string und int und double oder so kann, so kann sie eben auch „Max CRDT“ und so etwas?
Lucas Dohmen: Genau. Das wäre ein Datentyp. Nun ist max eine von den Sachen, die vielleicht selten das sind, was man braucht. Es gibt halt ein ursprüngliches Paper, in dem die erklärt wurden, in dem ein paar eingeführt wurden. Mit der Zeit sind noch ein paar weitere hinzugekommen. Interessanter als max ist zum Beispiel, wenn ich zwei sets habe und ich möchte immer die Union, also die Vereinigungsmenge, von allen sets haben, ist das auch ein einfaches CRDT. Denn wenn dasselbe set noch einmal reingemergt wird, ändert sich nichts. Es ist alles in Ordnung. Nur ist das Problem, ich habe jetzt kein set mehr, sondern ich habe ein union set. Ich kann jetzt auf diesem set nicht auch die Operation Schnittmenge/intersection herausziehen. Ich kann nicht sagen, zieh etwas ab, weil es dann ja nicht mehr mergebar ist. Ich muss quasi vorher entscheiden bei dem CRDT, bist du eine set union oder bist du eine intersection union. Das kann für manche Fälle okay sein, für andere Fälle nicht.
Stefan Tilkov: Ich bekomme Kopfschmerzen bei der intersection union. Oder wolltest du intersection set oder union set oder so etwas sagen?
Lucas Dohmen: Intersection set.
Stefan Tilkov: Okay, also entweder es ist ein intersection set oder es ist ein union set.
Lucas Dohmen: Genau, danke.
Stefan Tilkov: Okay.
Lucas Dohmen: Und das ist jetzt schon eine ganz schöne Einschränkung, wenn ich mich vorher entscheiden muss, ich werde niemals Elemente aus meinem set herausnehmen, ist das vielleicht nicht so nützlich. Und da haben sie sich dann die so ein bisschen higher order CRDTs ausgedacht, die quasi unten drunter andere CRDTs benutzen, um noch coolere Sachen machen zu können. Und da haben sie zum Beispiel ein set eingeführt, da kann man sowohl Sachen hinzufügen, als auch entfernen. Und das finde ich einen ganz coolen Trick, das ist das sogenannte observed-removed set. Und da ist das so, wenn ich ein Element hinzufüge, dann füge ich das einfach hinzu, dann lege ich das einfach da hinein. Wenn ich eins entferne, dann entferne ich das aber nicht, sondern ich lege ein „Entfernen-Element“ an und sage, dieses Element wurde entfernt. Und zwar lege ich ein „Entfernen-Element“ an für jedes Mal, wo dieses Element hinzugefügt wurde, damit ich nachher drauf schauen und sagen kann, okay, es ist quasi in der Summe nullmal dieses Element drin, dann ist es nicht drin, oder es ist einmal drin, dann ist es einmal drin. Und so kann ich dann sowohl Dinge in meinem set hinzufügen, als sie auch wieder herausnehmen. Da gibt es dann noch ein paar andere, gerade in dieser set- und Zahlenwelt, da gibt es schon so einige, die man da benutzen kann. Die sind dann natürlich hochattraktiv für mich, wenn ich so eine high available-Datenbank habe und die das unterstützt und ich da dann solche Operationen einfügen kann.
Stefan Tilkov: Ich glaube, dass der eine oder andere Zuhörer das bestimmt, ähnlich wie ich, irgendwann in seinem Leben auch mal in schlecht programmiert hat. Denn wenn man irgendwie Daten von irgendwo bezieht und dann Änderungen darauf macht und dann nachher diese Änderungen in Teilen, sozusagen das Delta, wieder applizieren muss, macht man ja ganz ähnliche Sachen: Man merkt sich, was hat man entfernt, was hat man geändert und fummelt das irgendwie wieder zusammen. Und das ist eigentlich nur eine Formalisierung mit ein bisschen ordentlicher Informatik dahinter und mal ordentlich durchdacht. Bedeutet das jetzt, dass ich eine Datenbank brauche, die das unterstützt, oder wo bekomme ich jetzt diese CRDT-Fähigkeit her?
Lucas Dohmen: Grundsätzlich kann ich entweder eine Datenbank verwenden, die das kann, oder ich könnte eine wählen, die Konflikterkennung hat. Wenn ich eine Datenbank habe, die zum Beispiel vector clocks hat, dann kann ich ja die CRDTs quasi auf Anwendungsebene programmieren. Ich kann sagen, okay ich persistiere das so, dass ich die herauslesen kann, und wenn es jetzt einen Konflikt gibt, dann kann ich meine CRDT-Library benutzen, um den Merge zu machen. Es gibt zum Beispiel in Ruby solche Libraries, es gibt sie auch in Clojure usw. Die kann man einfach fertig benutzen und dann bräuchte ich nichts weiter von meiner Datenbank, als dass sie mir sagt, es gab einen Konflikt.
Stefan Tilkov: Und die muss irgendwie einen Blob speichern können, irgendwie.
Lucas Dohmen: Genau, es muss irgendwie meine Daten annehmen können. Aber es gibt auch Datenbanken, die das nativ unterstützen. Und da wäre so das größte Beispiel Riak. Da heißt das aus Marketinggründen “Riak Data Types“ und nicht CRDTs, aber das ist genau das. Und die sind halt auch schon sehr fortschrittliche data types. Also, es sind schon welche, wo du dann sagst, okay, dafür habe ich viele Anwendungsfälle, bei denen ich sie benutzen könnte, wo es jetzt nicht die ganz einfachen Sachen wie max und min sind. Da sind schon komplexere Sachen dabei. Und das ist da gerade interessant, aber man könnte es auch mit einer anderen Datenbank verwenden und dann halt Client-seitig lösen.
Stefan Tilkov: Gut. Wir müssen zum Abschluss kommen, damit wir so ungefähr unsere Zeitvorgaben einhalten. Lass uns doch nochmal kurz rekapitulieren. Das ist alles schon eine Menge Holz. Wenn man von dem Zeug noch nie irgendetwas gehört hat, wird man ziemlich erschlagen von all diesem Kram. Muss ich das denn überhaupt wissen? Interessiert mich das oder kann ich das ignorieren? Wie gehe ich denn damit um als normalsterblicher Anwendungsentwickler?
Lucas Dohmen: Ich finde, dass man schon grundsätzlich ein Verständnis dafür haben muss. Denn wenn wir eine Datenbank auswählen, dann hat das ganz viele Konsequenzen. Wenn ich jetzt, ohne zu wissen, was high availability oder Konsistenz ist, einfach eine high availability-Datenbank auswähle, dann habe ich vielleicht einen großen Fehler begangen, weil ich vielleicht totale Konsistenz brauchte. Und viele von diesen Entscheidungen sind eigentlich Businessentscheidungen. Also eigentlich muss ich meinen Entscheider, meinen Businessentscheider fragen, was brauchst du? Wir müssen dann darüber sprechen, du sagst, du brauchst - weil er selbst sagen wird, ich brauche die höchste Konsistenz aller Zeiten…
Stefan Tilkov: …und die höchste Verfügbarkeit.
Lucas Dohmen: Genau. Und dann müssen wir ihm eine informierte Entscheidung darüber geben, was das bedeutet. Was ist der Trade-off: was sind die Performance-Implikationen, was würde es für uns heißen, wenn wir wirklich auf volle Konsistenz setzen in unserer verteilten Datenbank und jetzt fallen zwei Knoten aus und unsere Anwendung ist nicht erreichbar. Wieviel Geld verlieren wir jetzt, dadurch dass die Anwendung nicht mehr funktioniert. Denn die Anwendung wird nur noch sagen, 500, 500, 500, ich kann hier nichts mehr machen. Und wir müssen gemeinsam mit den Leuten aus der fachlichen Welt oder auch aus der Businesswelt entscheiden, was ist denn schlimmer: Wenn wir eine gewisse Inkonsistenz haben über eine Zeit, eine eventual consistency haben, oder wenn wir wirklich die Datenbank anhalten müssen, weil wir nichts mehr machen können. Da müssen wir schon eine informierte Entscheidung treffen und deswegen ist es wichtig, ein Grundverständnis davon zu haben.
Man muss nicht alles im letzten Detail verstehen, aber wenn man von allen von diesen Datenbanken versteht, wofür sind sie da und wie ordne ich sie ein, dann kann ich sagen, okay, jetzt habe ich einen use case gefunden für diese Kombination von Eigenschaften, ich suche mir jetzt mal drei raus und dann schaue ich mir die im Detail an: Erfüllen die jetzt das, was ich will. Wenn wir einfach irgendeine Datenbank wählen, dann wählen wir gerade jetzt - ich glaube, das kommt auch so ein bisschen aus diesem Mindset, als Datenbanken viel ähnlicher zueinander waren, alle alten Datenbanken waren einfach immer konsistent, die waren nicht verteilt, fertig. Aber wir haben jetzt nicht einfach ein neues Zeitalter, wo wir auf einen Knopf drücken können und jetzt ist es verteilt, sondern wir müssen dann Trade-offs eingehen und die müssen wir bewusst eingehen. Deswegen finde ich es wichtig, das zu verstehen, was wir da tun.
Stefan Tilkov: Okay. Ich bin mir ganz sicher, dass wir noch ungefähr zwei Stunden weiter machen könnten. Das tun wir jetzt aber nicht, wir machen hier einfach einen Cut. Wir packen ganz, ganz viel in die Shownotes. Wir haben ganz, ganz viele Links; dazu gibt es unglaublich viel Material, man kann wochenlang Konferenzvideos anschauen.
Lucas Dohmen: So ist es.
Stefan Tilkov: Vielleicht machen wir bei Gelegenheit auch mal eine dritte Episode zum ganzen Thema. Ich danke dir auf jeden Fall vielmals, ich habe viel gelernt. Danke, Lucas, danke an die Zuhörer.
Lucas Dohmen: Und bis zum nächsten Mal.
Stefan Tilkov: Bis dann.
Beide: Tschüss.