Thomas Bandt

Über mich | Kontakt | Archiv

NoSQL, no pain? Not at all.

Ich habe heute auf Twitter zwei Tweets gepostet, die zu reichlich Kommentaren geführt haben:

"Das denormalisierte Speichern mag für ein Gästebuch cool und easy sein, bei einem komplexen Modell wird es schnell nervig. Nervig bedeutet: fehleranfällig und enorm aufwändig."

Nun ist Twitter nicht gerade das Medium, über das man ausführlich über bestimmte Themen diskutieren kann, weshalb ich an dieser Stelle noch ein paar Worte nachreiche.

Ich arbeite seit geraumer Zeit an einem Projekt, das sehr agil entstanden ist. Heute sind wir fast "Feature complete" - die genaue Funktionsweise, die Zusammenhänge und die konkrete Implementierung waren bei Projektstart aber nur sehr wage oder noch gar nicht definiert.

Bei jenem Start haben wir uns aus dem Bauch heraus für den Verzicht auf ein RDBMS entschieden und zu RavenDB gegriffen. Über den Projektverlauf wechselten wir zu MongoDB, heute sind wir aber wieder bei RavenDB angelangt.

Gründe für NoSQL im Allgemeinen waren primär die (einfache) horizontale Skalierbarkeit, sowie die Tatsache, dass wir mit deutlich mehr Lese- als Schreibzugriffen rechneten (etwas, das sich wahrscheinlich nicht ganz halten lassen wird, wie wir heute wissen.).

Nehmen wir an, es geht um ein öffentliches Blogging-System, über welches registrierte Nutzer eigene Blogs anlegen und mit Artikeln befüllen können. Diese Artikel können kommentiert werden, primär von auf der Plattform registrierten Nutzern. Nutzerschnittstellen sind einmal der normale Webbrowser, als auch Apps für Smartphones. Die Apps können jedoch nicht nur zum Veröffentlichen von Blog-Beiträgen benutzt werden, sondern auch zum Lesen abonnierter Blogs, Kommentieren und Vernetzen mit anderen Plattform-Nutzern. Das Ganze ist also durchaus ein Stück weit "social".

Die Anwendung besteht im Kern aus folgenden Komponenten:

Sowohl Website als auch Apps beziehen ihre Daten ausschließlich über die API, welche für die Datenhaltung allein zuständig ist. Wie das ganze skaliert und ab welchem Punkt Caching oder CQRS relevant werden, soll hier nicht das Thema sein.

Was uns zunächst zum Wechsel von MongoDB zurück zu RavenDB bewegte, war die Tatsache, wie wir Daten in Dokumentform ablegen. Denn wir haben derzeit nur zwei Dokument-Typen: Blogs und User.

Ein User hat n Blogs und n andere User als Kontakte. Ein Blog wiederum hat n Autoren, n Artikel, welche wiederum n Kommentare haben.

Beim Lesen ist das derzeit tatsächlich sehr bequem, weil die einzelnen Dokumente rasend schnell ausgelesen sind und alle relevanten Informationen bereits beinhalten, die auf dem "klassischen Weg" erst über teure Joins zusammengepackt werden müssten.

Damit das aber funktioniert, müssen viele Daten stark denormalisiert gespeichert werden. So sind zum Beispiel alle von einem Nutzer abonnierten Blogs in einer Collection auf ihm gespeichert, in Form eines BlogBriefs, das die wesentlichen Informationen bereithält, die beim Auflisten aller Abonnements benötigt werden (Titel, BildID).

Umgekehrt hat zum Beispiel jeder Kommentar, der einem Artikel zugeordnet ist, welcher wiederum in einer Collection in einem Blog beheimatet ist, einen UserBrief. Dieser enthält gleichfalls den Namen des Autors und die ID seines Profilbildes.

Der Vorteil liegt auf der Hand: man lädt zur Darstellung eines Blogs bspw. genau dieses eine Blog-Dokument und kann ohne weitere Datenbankzugriffe auf einen Schlag alle Artikel mit allen Kommentaren darstellen. Und zu jedem Kommentar kann man Namen und Bild des Autors anzeigen.

Spannend wird die Sache, wenn nun ein User seinen Namen ändert. In einem RDBMS wäre das ein Update auf die Users-Tabelle und die Sache wäre gegessen (gut, der Cache müsste invalidiert werden … aber egal).

An dieser Stelle müssen jedoch sämtliche Blogs geöffnet, alle darin befindlichen Artikel und alle wiederum darin enthaltenen Kommentare durchgeschaut, und die passenden UserBriefs aktualisiert werden.

Da das Wahnsinn ist, speichert man also auf den User noch die ID jedes Blogs, in welchem er einen Kommentar geschrieben hat - somit muss man nur die Blogs aktualisieren, die wirklich betroffen sind.

Wird nun ein Blog gelöscht, müsste man wiederum alle mit ihm in Verbindung stehenden User durchgehen und die ID des Blogs aus deren Collections entfernen. Man kann natürlich auch die ID erst dann löschen, wenn mal wieder ein Profilupdate erfolgt. Auch wenn an dieser Stelle der kleine RDBMS-Administrator fürchterlich weint.

Das alles ist nervig, aber irgendwie beherrschbar (falls es für diese Denormalisierungs-Geschichten übrigens Best Practices gibt, nur her mit den Vorschlägen - momentan ist das alles Marke Eigenbau).

Nun kommt aber noch etwas dazu, ich schulde ja noch die Begründung für den Wechsel weg von MongoDB hin zu RavenDB. RavenDB beherrscht Transactions! Ja ja, Deadlocks und so weiter und so fort. Aber lieber einen Deadlock als Inkonsistenz.

Denn: bei so gut wie jedem Update müssen - schon wg. der starken Denormalisierung - mehrere Dokumente, oft sogar User und Blogs, aktualisiert werden. Und geht beim Aktualisieren eines UserBriefs zu einem Kommentar etwas schief, sollte auch das Aktualisieren des Userprofils nicht durchgehen - sonst hat der Nutzer nachher zwei Namen im System.

Man muss sehen, wie sich das in der Praxis auswirkt, wenn einmal viele Blogs mit noch mehr Kommentaren vorhanden sind und ein Update innerhalb des Transaction-Scope dutzende oder hunderte Dokumente blockiert.

Aber es erscheint derzeit als bester Kompromiss. Schade, dass wir nicht in einer perfekten Welt leben können ;-).

PS: Das Buch NoSQL Distilled von Martin Fowler ist schon auf dem Kindle und als Bettlektüre für die nächsten Tage fest eingeplant.

PPS: Alles Feedback ist willkommen.

Kommentare

  1. vandango schrieb am Donnerstag, 4. April 2013 21:12:00 Uhr:

    warum nicht eine klassische db mit nem vernünftigem server und ner sauberen schnittstelle?
    ähnliche geschwindigkeit, aber normalisiert.
  2. Alex schrieb am Donnerstag, 4. April 2013 21:12:00 Uhr:

    Schon gesehen?
    http://daniellang.net/joining-documents-in-ravendb-2-0/
  3. Robert Mühsig schrieb am Donnerstag, 4. April 2013 21:26:00 Uhr:

    Ich hab selber nur kleinere Demos gebaut, daher kann ich nicht von grossen System sprechen, aber "gefühlt" wollt ihr zu viele Daten denormalisieren (Blogs in dem der User kommentiert hat mit in sein Dokument). Solche Abfragen könnten vermutlich wesentlich eleganter durch eine RavenDb Index gelöst werden - siehe was Alex verlinkt hat.
  4. Stephan schrieb am Donnerstag, 4. April 2013 21:33:00 Uhr:

    So aus dem Bauch, ohne Rücksicht auf Paradigmen ;)
    Die Dokumente in NOSQL lassen aber statt Userinfo nur noch die ID speichern.
    Anzeigeinformationen des User als separate Tabelle speichern und "manuell" zu den Dokumenten "joinen" .
    Wenn ein Username geändert wird geschieht dies in der sep. Tabelle für die Dokumente erledigt das Dein "Join".
    Wenn ein User gelöscht wird, musst Du auch nichts machen und ersetzt die nicht gefundene ID durch "gelöschter User"
    So bleiben auch Kommentarfolgen lesbar.
  5. Alex schrieb am Donnerstag, 4. April 2013 21:37:00 Uhr:

    Dein Blog mag keine spitzen Klammern ;-)

    Unter 1.0 hat man ja sowas gemacht: DenormalizedReference<T>
    https://gist.github.com/AlexZeitler/5314120

    Dann mit einem Index betroffene Daten holen und per PatchRequest aktualisieren.

    Seit 2.0 gibts den ScriptedPatchRequest, da kannst Du mit JavaScript auf dem JSON herumklimpern:
    http://ayende.com/blog/157185/awesome-ravendb-feature-of-the-day-evil-patching

    Der AdvancedPatchRequest heißt in der 2.0 Stable ScriptedPatchRequest.
  6. Thomas schrieb am Donnerstag, 4. April 2013 21:59:00 Uhr:

    Danke für euer Feedback - klasse :-).

    @Alex: Volltreffer, http://daniellang.net/joining-documents-in-ravendb-2-0/ sieht großartig aus.
  7. Mariusz schrieb am Donnerstag, 4. April 2013 22:35:00 Uhr:

    Kurzfassung: Ich würde die Updates des Benutzernamens nicht online durchführen, sondern eine Art UpdateMessageQueue aufbauen, die von vielen Workern abgearbeitet wird.
    Details: Wenn jeder ca. 10 Kommentare am Tag generiert, wären das pro Jahr Plattformnutzung 3650 Kommentare, die mit einem Schlag verändert werden müssen. Ich würde hierfür einen eigenen Dienst schreiben, der im Hintergrund solche Updates durchführt. Dieser Dienst könnte z. b. aktuelle Kommentare priorisiert updaten als ältere, da diese der Wahrscheinlichkeit nach nicht sofort dargestellt werden müssen. Programmiert man diesen Dienst clever, kann man beliebig viele Worker parallel arbeiten lassen, ggf. verteilt auf vielen Serverknoten. Eine Transaktion ist in meinen Augen nicht notwendig, da ein Update eigentlich nur aus technischen Gründen schief gehen kann (DB verschluckt sich – kein Festplatenplatz, etc). Fachlich gibt es ja keine Regel, die einen Update im Dokument A zulassen würde, im Dokument B aber nicht.
  8. Werner Mairl schrieb am Freitag, 5. April 2013 06:50:00 Uhr:

    >Aber lieber einen Deadlock als Inkonsistenz.

    Bin jetzt kein NoSql Experte, aber könnte es sein, dass du mit obiger Annahme/Anforderung im Prinzip gegen eines der Grundkonzepte von NoSQL/CQRS verstösst (eventually consistent)

    ich habe nämlich den Eindruck, dass man durch diese Anforderung mehr oder weniger gezwungen ist das Verhalten eines RDBMS im NoSQL nach zu bauen, was ja nicht im Sinne des Erfinders sein kann

    just my 2 cents...

    lg
    Werner
  9. Thomas schrieb am Freitag, 5. April 2013 09:36:00 Uhr:

    Es muss nichts nachgebaut werden, RavenDB unterstützt Transactions out of the box. Und Eventual Consistency bezieht sich auf etwas anderes, siehe auch CAP (http://de.wikipedia.org/wiki/CAP-Theorem).
  10. Hannes Preishuber schrieb am Freitag, 5. April 2013 15:19:00 Uhr:

    das NoPain tool suche ich noch immer. So Subjektiv: die Probleme werden nicht kleiner sondern nur anders und die Produktivität bei der Entwicklung stagniert seit Jahren
  11. Steve schrieb am Freitag, 12. April 2013 15:27:00 Uhr:

    Ich denke für so was braucht man trotzdem keine Transaktionen.

    Was gar nicht geht in so einem Scenario ist die Änderungen an den Denormalisierten Daten direkt überall im Code (z.B. Controllern) denn dann verlierst du schnell den Überblick. Anbieten würde sich CQRS (Aber ohne Eventstore, also die nur die Trennung zwischen Schreib- und Leseseite). Die Controller schicken dann ne Änderungsnachricht an ne spezifischen Teil der Anwendung der sich nur um das Schreiben der Daten kümmert und wo alles an einem Fleck ist. Um jetzt das Problem mit den Transaktionen zu umgehen könntest du z.B. die Änderungsnachricht an nen RabbitMQ Exchange schicken. Dann machst du für jede denormalisierte Tabelle einen "handler" der dann jeweils eine eigene Queue bekommt. Die bindest du dann alle beim starten an den Exchange. Dann musst du nur noch dafür sorgen das der Ack (Nachricht verarbeitet) erst nach dem Abarbeiten der Nachricht gesetzt wird. Dadurch ist gesichert das dir keine Änderung verloren geht. Der Exchange bewirkt das die Nachricht dann jeweils einmal in jede Queue gelegt wird. Und der Ack stellt sicher das die Nachricht erst dann aus der Queue verschwindet wenn die Daten geändert werden.


« Zurück  |  Weiter »