Kinder, wie die Zeit vergeht. Vor anderthalb Jahren habe ich die Entscheidung getroffen, dass es für mein neues Projekt an der Zeit ist, statt der bekannten und geschätzten relationalen Datenbanken NoSQL zu verwenden.
Die primäre Motivation neben der Entdeckungslust eines Techies: Größenwahn und die dafür erforderliche horizontale Skalierbarkeit. Wenige große Server sind teuer (*), viele kleine Server billig, so die Grundannahme. Und RDBMS wie SQL Server skalieren faktisch nur vertikal, auch wenn das wie im Fall von stackoverflow ebenso gut funktionieren kann - aber eben nur mit entsprechenden Investitionen in Hardware.
So begab es sich, dass wir uns nach der Evaluation verschiedener Systeme für RavenDB entschieden, womit wir, nur unterbrochen von einer kurzen Episode MongoDB, auch heute noch arbeiten.
Naiv, unerfahren und wagemutig begannen wir mit zu wenigen Aggregaten und totaler Denormalisierung, was uns schnurgerade in die Update-Hölle führte.
Angeregt durch die Kommentare rund um diesen Blogpost fanden wir dann aber eine wirklich schöne Lösung, die das Beste aus beiden Welten verbindet.
Um bei dem Beispiel aus diesem Post zu bleiben: Statt wie zuvor nur Blogs und User in einzelnen Dokumenten abzulegen, haben wir heute eigene Dokumente für Blogs, Artikel, Kommentare und User. Anstatt beispielsweise Autoren-Infos für Kommentare direkt in den Kommentar zu schreiben, legen wir dort nur noch die Referenz zum User in Form seiner ID ab.
An der Stelle kommt der Charme von RavenDB ins Spiel: über Indizes, die fortlaufend asynchron im Hintergrund gepflegt werden, werden die einzelnen Dokumente wieder so zusammengesetzt, wie wir sie nachher auslesen wollen.
Beispiel: ein Blog hat sowohl Autoren als auch Abonnenten innerhalb der Plattform. Im Blog-Dokument befindet sich nun jeweils eine Liste mit den IDs aller Autoren und eine Liste mit den IDs aller Abonnenten. Über den passenden Index erzeugten wir uns eine View auf das Blog, die die vollständigen und aktuellen Nutzerdaten wie Name oder die ID des Profilbildes anstatt der blanken ID enthält. So ist nur noch die Abfrage des einen Views auf das Blog notwendig, anstatt der 1+n Abfragen für das 1 Blog + n Autoren + n Abonnenten. Sobald nun ein Nutzer seine Daten ändert, wird der Index automatisch von RavenDB im Hintergrund aktualisiert.
An der Stelle darf man übrigens nicht den Fehler machen und das Ganze mit Views aus RDBMS verwechseln - denn über Joins bekommt man diese Daten (1 Blogpost + n Autoren + n Abonnenten) nicht über eine "einzelne Abfrage" heraus. Das ist natürlich auch für den Index eine komplexere Angelegenheit, da jedoch im Hintergrund indiziert wird, ist der Abruf vergleichsweise billig.
Apropos Generierung im Hintergrund: RavenDB bietet verschiedene Optionen um mitzubekommen, ob abgerufene Daten aktuell sind, oder nicht. Daraufhin lässt sich dann entsprechend reagieren, wenn man z.B. zwingend aktuelle Daten benötigt.
Damit waren wir nun vermeintlich der Update-Hölle entronnen - denn es mussten bei bloßer Änderung des Namens eines Nutzers nicht mehr 1+n Dokumente aktualisiert werden, sondern nur noch eines. Den Rest erledigte der Index.
Das alles ist in seinem Aufbau wirklich eine schöne Sache, es hat Spaß gemacht damit zu entwickeln.
Das endete abrupt, als sich die Entwicklung der ersten Phase dem Ende zuneigte und die Menge an Daten auf unserem Alpha-Server zunahm. Über Last-Tests hatten wir zwar mal gesprochen, aber es gab immer noch andere Aufgaben, die wichtiger waren ... tja, hätten wir der Intuition vorher mal ihren Lauf gelassen.
Denn es ist so: RavenDB verweigert sich per default Operationen, bei denen mehr als 30 Dokumente abgerufen werden. Man kann diesen Wert zwar hochsetzen, aber alles über 1024 Dokumente wird spätestens am Server nicht mehr unterstützt.
Das wird spätestens beim Löschen problematisch. Nehmen wir an, ein User möchte seinen Account löschen. Das bedeutet für uns, dass all seine Daten restlos entfernt werden, ohne Kompromisse.
Wenn er nun in 10 Blogs zu jeweils 5 Posts im Schnitt 2 Kommentare geschrieben hat, müssen diese 100 Kommentare aus den 50 Posts entfernt werden - bzw. nicht die Kommentare selbst, sondern die Referenzen auf die Kommentare. Das bedeutet: 50 Post-Dokumente öffnen, die ID aus der Comments-Collection entfernen, speichern. Und das alles sinnvollerweise in einer Transaktion, um keine Inkonsistenzen zu riskieren.
Aber das geht nicht, denn per default ist wie gesagt beim Anfordern des 31. Dokumentes Schluss, konkret steigt der RavenDB-Client mit einer Exception aus.
Sicher, es gibt Workarounds. Man kann das Limit hochsetzen und darauf wetten, dass User unsere Plattform so scheiße finden werden, dass sie sie nicht nutzen ;-). Man kann auch auf Transaktionen verzichten und inkonsistente Daten riskieren.
Aber will man das? Ich nicht. Ich muss aber auch gestehen, dass ich zu diesem Zeitpunkt mit der Erfahrung aus knapp anderthalb Jahren RavenDB auch nicht mehr ganz unvoreingenommen war.
Hier eine lose Liste meiner Erinnerungen:
- Integer-IDs für Dokumente erfordern Workarounds.
- Das Entwickeln der Indizes ist eine einzige Katastrophe. Man kann sie zwar in C# definieren, jedoch läuft da irgendein krummer Parser im Hintergrund, bei dem wir heute noch in bestimmten Konstellationen nach Mustern suchen, wie bestimmte Konstrukte interpretiert und ausgeführt werden. Pures Trial & Error. Wenn die Indizes einmal laufen, sind sie ein Traum, aber anfassen möchte sie nachher niemand mehr.
- Wir haben mehrfach Bugs gefunden, mal mehr, mal weniger schwerwiegend. Z.B. haben wir es geschafft, die Indizes mit der Verwendung von longs statt ints zu zerschießen.
- Das asynchrone Handling der Indizes ermöglicht eine gute Lese-Performance, aber wie verhält es sich unter echt harten Bedingungen, in denen das System auch skalieren muss? Ist man nicht auf aktuelle Ergebnisse angewiesen ist das gut, wenn man die jedoch braucht (und wir brauchen sie zwingend), dürften Indizes über kurz oder lang nicht mehr die geeigneten Werkzeuge sein.
- Das Management Studio läuft noch immer auf Silverlight und ist schlicht und ergreifend erbärmlich gegenüber allem Tooling, was für andere Datenbanksysteme da draußen existiert. Das ist kein riesen Problem, aber eben doch in manchen Situationen ein bisschen ärgerlich.
- RavenDB bietet Dutzende Features, wenn man Ayendes Blog verfolgt ist auch ein Austausch des Unterbaus geplant. Es ist natürlich super, wenn an einer Software aktiv gearbeitet wird. Aber wenn ich es schaffe, mit longs statt ints ein zentrales Feature zu zerschießen, sollte vielleicht doch mehr Wert auf "Reliability" gelegt werden, als auf neue Funktionen?
- Der Umgangston im Support-Forum ist rau. Nun bin ich selbst kein Kind von Traurigkeit und kann damit umgehen. Wenn ich aber einen krassen Bug wie den mit den 64-bit-Integern melde, erwarte ich etwas Eigeninitiative in der Reproduktion und Behebung - stattdessen wollte man von mir einen laufenden Failing-Test haben. Bei Kleinigkeiten ist das ok, aber bei so etwas ...
Das sind jedenfalls die Eindrücke, die bei mir in Summe dazu geführt haben, für dieses Projekt nicht weiter auf RavenDB zu setzen.
So sind wir jetzt wieder auf Los - und haben uns für PostgreSQL als Datenbanksystem entschieden. Einerseits, weil sich "die Wahrheit in einem relationalen System" für uns schon immer besser aufgehoben angefühlt hat, andererseits weil wir tendenziell ohnehin in Richtung CQRS unterwegs sind und wir damit den potentiellen Performance-Nachteil durch Joins & Co. problemlos durch den Einsatz von (ggf. anderen) NoSQL-Tools beim Lesen wieder ausgleichen können werden.
(*) Bei Hetzner kostet ein DELL-Server mit Hexacore-CPU, gespiegelter SSD und 192 GB RAM momentan 300 EUR netto pro Monat. Das ist nichts im Vergleich zu den Anschaffungskosten für Hardware in diesen Dimensionen von vor ein paar Jahren.