PostgreSQL => MongoDb => Morphia => Morphium

published : 2013-04-18 changed: 2017-05-16

category: Computer


no english version available yet

Das war ein langer Weg... Bei holidayinsider.com nutzen wir mittlerweile MongoDB (neben Lucene-SolR als primäre SearchEngine) als primäres BackEnd. Das brachte ein paar Probleme aber auch tolle Lösungen mit sich.

Zunächst mal, warum überhaupt eine NoSQL-Db und warum dann gerade Mongo? Die Frage ist natürlich berechtigt und wurde bei uns auch heiß diskutiert. Für das, was wir vor hatten, war PostgreSQL (so toll die Datenbank auch sonst ist) leider nicht ganz das richtige. Die Vorteile (Transaktionen, Berechtigungen und Relationen) wären für unseren Ansatz nur hinderlich gewesen.

Was haben wir gemacht? Wir loggen nahezu alles, was auf der Plattform passiert und werten diese Daten dann später in einem Offline-Prozess aus (wobei auch eine Menge Daten live in Graphite mit verfolgt werden können). Das war so mit PostgreSQL nicht möglich - wir hatten zwischenzeitlich Collections mit mehr als 100 Millionen Datensätzen deren Verarbeitung mit PostgreSQL mehr als 24h gedauert hätte. Da diese Daten keinen Relationen zuzuordnen sind, brachte eine Relationale Datenbank mit Transaktionen etc keine besonderen Vorteile. Deswegen der Gedanke (auch, weil das Mapping den Java-Objekten recht nahe kommt) auf einen Dokumentenbasierten Storage und in dem Fall Mongo zu setzen. Und: wir sind ein hippes Startup Unternehmen, da muss man auf neue Technologien setzen!

Warum Mongo? Wir wollten auf ein möglichst flottes und einfaches Modell setzen, Mongodb als primäre Datenbasis war das erklärte Ziel, PostgreSQL "nur" noch im Hintergrund für einige nicht lastabhängige Daten. Mongo schien uns als erste Wahl in dem Zusammenhang... da könnte man in der Retrospektive sicherlich noch mal drüber diskutieren ;-)

holidayinsider.com ist eine 100% pure Java Anwendung ohne viel Overhead. Deswegen war es auch wichtig, eine gute Java Anbindung der NoSQL-Datenbank zu haben. Für Mongo gab es 1. einen guten Java-Treiber, und 2. ein Mapping Framework namens Morphia.

Wir haben morphia einige Zeit lang produktiv in der Anwendung gehabt, aber insbesondere mit der Erweiterbarkeit hatten wir Probleme. Als dann die ersten Bugs in morphia zu Datenverlusten und DownTimes geführt hatten, mussten wir uns was einfallen lassen. Insbesondere, weil Morphia seit bestimmt einem Jahr auf der Version 0.99-1 festhängt und nicht mehr weiter entwickelt wird. Wir hätten bei dem Projekt ja sogar mitentwickelt, haben das auch angeboten, aber der einzige Entwickler von Morphia wollte nicht, dass irgendjemand mit macht.

Wir wollten das Projekt forken, haben es ausgecheckt und ich hab mir den Code mal genauer angesehen. Sagen wir es so - es war einfacher, es neu zu machen.

Das war die Geburtsstunde von Morphium (http://code.google.com/p/morphium) - der Name war an Morphia angelegt, ich wollte so auch die Vorarbeit von Scott Hernandes würdigen. Das Projekt wird rege weiter entwickelt und ist mittlerweile in der Version 2.0.6 verfügbar. Entweder von der Projektseite, oder von SonatypeOSS / Maven.

Morphium ist ein ObjectMapper für Java mit einer Menge Features (die meiner Meinung nach in Morphia gefehlt haben). Hierbei bezeichnen Entities Objekte (POJOs), die in Mongo gespeichert werden sollen.:

  • sehr robustes, flexibles und performantes Mapping von Entities/POJOs zu mongo Objekten: So ist es in Morphium möglich, seinen eigenen Mapper zu definieren. Das ist z.B. sinnvoll, wenn man das Mapping mocken muss für z.B. JUnit-Tests oder dergleichen.
  • Flexible API: Es ist möglich, fast alle Kernkomponenten selbst zu implementieren bzw. die Default-Implementierungen abzuleiten und um eigene Funktionalitäten erweitern. so können ObjectMapper, Cache, Writer und Query durch eigene Implementierungen ersetzt werden (zur Laufzeit).
  • Unterstützung von Vererbung und Objekthierarchien: Morphium untersucht für das Mapping den gesamten Ableitungsbaum, wodurch es möglich ist, Entities von anderen abzuleiten und entsprechend zu erweitern.
  • Deklarative definition von Indices: Mongo ist nur dann performant, wenn indices verwendet werden. Diese sollten gut zu den Suchanfragen passen. Ohne Index hit ist eine Query um leicht das 10-100 fache langsamer (query auf eine Collection mit ca. 500.000 Entries ohne Index hit dauerte ca. 45 Sek - mit < 1Sek). In Morphium können die Indices mit Hilfe von Annotations definiert werden.
  • Deklaratives mapping: Feld- und Collectionnamen können deklarativ festgelegt werden. Es ist auch möglich, Aliase anzugeben, wordurch ein Feld evtl. unter mehreren Namen ansprechbar ist. Das ist insbesondere bei Datenmigration hilfreich. Es ist auch möglich, daten von verschiedenen Typen in eine Collection zu speichern - insbesondere bei Vererbung sinnvoll, weshalb das Flag auch @polymorph heißt.
  • transparente Referenzen (werden von Morphium aufgelöst), incl. lazy loading: In Morphium kann man sehr leicht objekt-Referenzen definieren. Das funktioniert über die Java-Annotation @Reference. Diese wird direkt auf dem entsprechenden Feld angewendet, wodurch nicht das ganze
  • deklaratives Caching: jedes entity bestimmt, ob es per default gecachted werden soll oder nicht. Das bedeutet, dass Ergebnisse von Suchanfragen an diese Entity im Ram gehalten werden. Dabei wird auch bestimmt, wie groß dieser Cache sein soll, ob er bei Schreibzugriffen geleert wird, wie lange Cache-Einträge gültig sind und was passieren soll, wenn der Cache "voll" ist.
  • asynchrones und gepuffertes Schreiben: gerade die Schreibzugriffe im Cluster (wenn man darauf wartet, dass die Knoten die einzelnen Schreiboperationen bestätigen) können länger dauern. Das kann zu Problemen mit der Performance führen. Eine Asynchrone API war da sehr sinnvoll. Gepuffertes Schreiben legt zunächst einen Puffer im RAM an und schreibt die Daten dann erst nach einer gewissen Zeit, oder wenn eine Mindestanzahl erreicht ist. Das ist insbesondere deswegen sinnvoll, weil Bulk-Inserts deutlich effizienter funktionieren als einzelne.
  • Validation: Validieren der Daten vor jedem Schreibzugriff mit javax.validation
  • Partial updates: gerade bei großen Dokumenten sinnvoll - sendet Änderungen an die Datenbank, nicht das gesamte Objekt
  • Threadsafety und Cluster awareness bzw. Clusterfähigkeit: Wir nutzen Mongo im cluster von mehreren Knoten. Es ist wichtig, das Morphium Unterstützung dafür hat. Einerseits um die Sicherheit zu gewährleisten (warte darauf, dass alle verfügbaren Knoten den Schreibzugriff bestätigen) andererseits um z.B. richtig zu reagieren (wenn ein knoten ausfällt etc.). Das ganze wird innerhalb Morphiums auch überwacht
  • Montoring und profiling: durch ein cleveres Listener-Konzept ist es möglich, jeden Schreib- oder Lesezugriff zu messen und die Daten für spätere Auswertung abzulegen (z.B. in Graphite).
  • Lifecycle Methods: Es ist möglich, Callbacks innerhalb eines Entity zu definieren für "preStore" oder "postLoad". Des Weiteren ist es noch möglich, globale Listener für diese Events zu definieren, d.h. bei jedem Schreibzugriff bzw. Lesezugriff unbabhängig vom Typ informiert werden.
  • automatische Werte: wie z.B. lastChanged oder lastAccessed - Timestamps
  • einfaches Messaging: messaging über MongoDB. Die Messages werden persistiert und in einer Transaktions-ähnlichen Art und Weise verarbeitet.
  • Unterstützung für Geospacial-Suche: selbsterklärend, oder?
  • Unterstützung von Map-Reduce bzw. dem Aggregation Framework: Das ist die Antwort von Mongo auf Joins und komplexe Anfragen in der "Relationalnen" Welt. Kurz gesagt: damit lassen sich auch schwierigere Auswertungen einigermaßen performant lösen. Das Konzept dahinter ist aber dennoch etwas schwer zu verstehen.
all diese Features sind zur Zeit in der aktuellen Version von Morphium verfügbar und sind auch schon produktiv in einigen Projekten im Einsatz.

Weitere Doku ist auf der Projektseite oder unter www.caluga.de/morphium zu finden.

Aber zurück zum Thema mongoDB. So einfach, wie ich es hier geschildert habe, war es auch nicht. Wir hatten auch ein paar (u.a. schmerzhafte) learnings.

Wir hatten 10Gen zu uns eingeladen, uns die Vorteile von Mongo mal zu erklären und uns das in ihren Augen korrekte Setup zu empfehlen. Wir nutzten zu diesem Zeitpunkt mongo schon für einige kleinere Logs, hatten noch keinen Cluster. Die Empfehlung ging auch von 10gen eindeutig in Richtung Sharding und jeden Shard in einem replicaSet zu Clustern. An sich ja eine gute Idee, allerdings nicht ganz seiteneffektfrei.

Wir sind ziemlich schnell drauf gekommen, dass Sharding auch overhead produziert - nicht zu knapp! So ist die Wahl des shard Key von essentieller Bedeutung beim Einsatz von Sharding. Der Einsatz von timestamps führte bei uns leider nicht zum gewünschten Effekt (das führt nämlich auch dazu, dass quasi minutenweise der Shard gewechselt wird, da nicht timestamp modulo Anzahl Maschinen benutzt wird um den Knoten rauszufinden, sondern immer 64K-Blöcke auf den selben Knoten landen). Außerdem war es so, dass requests auf eine geshardete Collection Extreme Last produziert hat. Teilweise wurden dadurch globale locks angelegt -> downtime!!!! Das war untragbar. Mal abgesehen davon, dass das setup extrem kompliziert ist (mongod für jeden Knoten, der daten hält, mit entsprechender Konfiguration, mongos auf jedem Knoten, der auf den shard zugreifen will und dann noch mongo-config-Server, die auch erreichbar sein müssen. Bei uns führte das zu mehr Problemen, als die, die es lösen sollte. So wurde immer mal wieder die Last auf den mongo Knoten extrem hoch nur durch irgendwelche Sharding- oder ReplicaSet-Sync-Operationen. Dummerweise haben die dazu geführt, dass alle anderen requests auch lahm wurden, teilweise so langsam, dass sie in Timeouts gelaufen sind.

Zu allem Überfluss lief auch eine ganze zeit lang der Failover nicht - zumindest nicht in Java. Wenn ein mongo Knoten ausgefallen ist, oder zur Wartung runtergefahren wurde, versuchte der Java Treiber dennoch darauf zuzugreifen.... Untragbar! Wir haben der mongo dennoch eine Chance gegeben, eine Menge von den learnings in Morphium einfließen lassen. Im Moment sind wir wieder auf dem Stand: Simplicity wins! Ein Cluster von 4 mongo Knoten, im replicaSet konfiguriert, kein Sharding. Und momentan keine Probleme!

Dabei ist auch wichtig, die Knoten korrekt zu dimensionieren. Wichtig sind: flotte Festplatte und genug RAM. Da ist man schnell etwas verwirrt, da der MongoD selbst nicht wirklich viel RAM nutzt, aber er nutzt "Memory Mapped files", d.h. der RAM-Bedarf des Systems steigt deutlich an. Wichtig ist, dass die Indices ins RAM passen. Sobald das nicht mehr der Fall ist, sollte man über Sharding noch mal nachdenken. Wir sind da aber noch weit von entfernt bei unseren Datenmengen.

En Problem mit den Datenmengen hatten wir dennoch: die eine der primären Datenbanken ist zwischenzeitlich auf eine Größe von mehr als 250GB im Filesystem angewachsen, obwohl reine Nutzdaten nur in etwa 100GB umfassten. Das liegt daran, wie mongo das Storage organisiert: einzelne Dateien werden angelegt, je nach dem, was gerade benötigt wird - aber nie gelöscht, selbst, wenn man die Collections entfernt. Der einmal belegt Platz wird wieder verwendet. Allerdings scheint es da zumindest bei unserem Setup ein Problem gegeben zuhaben, denn der Platzbedarf im Filesystem lag deutlich über den wirklichen Daten, obwohl nicht so viel gelöscht wurde... Die einzige Lösung für dieses Problem ist ein repairDatabase auszuführen. Das führt aber dazu, dass der entsprechende Knoten für die Dauer der Reparatur nicht erreichbar ist - Super, wenn auch der Failover nicht richtig funktioniert. Außerdem darf das Datenverzeichnis nur max. zu 50% belegt sein, will sagen: es muss noch genug Platz auf dem Filesystem sein, damit die Daten ein mal komplett umkopiert werden können.

Das konnten wir erst in letzter Zeit lösen, sind dann auch einen anderen Weg gegangen:

  • alle 2 Wochen wird ein Knoten im Cluster runtergefahren. Wenn es der Primary node war, wartet man zudem noch auf den Failover
  • auf diesem Knoten wird das Datenverzeichnis gelöscht
  • der Knoten wird im replicaset wieder hochgefahren
  • er synchronisiert alle Daten neu, minimaler platzbedarf auf der Platte.
Das funktioniert mittlerweile recht problemlos (der Failover funktioniert, und Morphium überwacht ja den Cluster Status). So haben wir den Platzbedarf auf den Platten um ca. 50% senken können.

Also als Tip: Jeder, der plant mongoDB einzusetzen, sollte auch diese Downtime für Repairdatabase oder das Syncen im Replicaset einplanen! Ich würde aus diesem Grund Mongo immer in einem ReplicaSet einsetzen!

Ein anderes Problem mit mongo hat zu grösseren hickups geführt, ein mal zu einem kompletten Datenbankcrash. Das muss man etwas ausführlicher erklären:

In mongo ist es möglich, komplett asynchron zu schreiben. D.h. Der schreibzugriff ist beendet, obwohl kein Knoten die Operation schon ausgeführt hat. Das ist natürlich etwas sub-optimal, wenn man diese Daten wieder einlesen möchte (z.B. Für Messaging oder Session Replikation). Deswegen ist es in mongo und in allen zugehörigen Treibern möglich, anzugeben,

  • darauf zu warten, dass der Knoten die Operation bekommen hat
  • ob darauf gewartet wird, dass die Schreiboperation auf der Platte persistiert wurde (fsync)
  • und auf wie viele Knoten man denn da warten will. Da kann man mittlerweile auch so was wie majority angeben.
  • beim lesen vomier mongo kann Management, welche Knoten im replicaSet benutzt werden sollen. Ob primärknoten, sekundärknoten oder irgendeiner.
Wir dachten natürlich, ok, bei wichtigen Daten warte auf alle Knoten. Das hat immer wird zu hängenden schreibprozessen und timeout exceptions in der Anwendung geführt. Auch nachklänge Diskussion mit 10Gen konnte mir keiner erklären, was da los ist. Die hi verwendete Option ist bestimmt, wie viele schreibvorgänge auf verschiedenen Knoten erfolgreich sein müssen. Wenn ich das auf die Anzahl der Knoten stelle, hängt der request (auch in der mongo Shell) reproduzierbar. Für alle Werte kleiner als die maximale Anzahl an Knoten funktioniert es.

in der Shell sieht dass dann so aus:

hi1:PRIMARY> db.testreplication.insert({_id:new Date()});printjson(db.runCommand({getLastError:1, w:4,  wtimeout:180000}));
{
        "n" : 0,
        "lastOp" : Timestamp(1347957897000, 17),
        "connectionId" : 431946,
        "wtimeout" : true,
        "waited" : 180000,
        "err" : "timeout",
        "ok" : 1
}

 

Das bedeutet, der Zugriff auf dieses Replicaset mit 4 Knoten (ohne Arbiter, eigentlich sind es 5) dauerte 180 Sekunden(!) in lief dann in einen Timeout. Sobald man irgend einen Wert kleiner 4 angibt, funktioniert es wunderbar. heute, 18.04.2013 ist dieser saublöde Fehler wieder aufgetreten, diesmal war allerdings nicht eingestellt, schreib auf alle Knoten, sondern nimm die Majority (also etwa die Hälfte). Sowas ist wirklich unnötig

Obwohl ich von 10Gen keine Bestätigung bekommen habe, denke ich, es werden bei diesen Schreibvorgängen, Hidden nodes (solche nodes, die nur die Daten bekommen, aber von denen nicht gelesen wird) dabei nicht mit gezählt werden. Ist nur eine Theorie, funktioniert aber.... Morphium unterstützt eine deklarative Festlegung sowohl der ReadPreference, also welche Cluster Knoten für den Lesezugriff erlaubt sind, als auch auf wie viele Knoten gewartet werden sollte bei Schreibzugriffen. Da das mit dem Schreiben auf alle knoten nicht funktioniert hat und die Anwendung immer wieder Dirty Reds bekommen hat (ich musste mir anhören, dass es ja keine Dirty Reds geben kann, da es das Konzept auf mongo gar nicht gibt - klugsch... Dann eben stale Data!) haben wir momentan alles auf primär umgestellt.. D.h. Wichtige Collections werden nur auf den primären Knoten geschrieben, und auch nur von dem gelesen... Widerspricht irgendwie dem lastverteilungsgedanken der replicasets.

Aber ok... Momentan haben wir noch keine Probleme mit der Last, sollte das kommen, muss sich 10Gen da was einfallen lassen.... Der Bug ist übrigens auch in der aktuellen V2.4.1 von mongodb vorhanden... (wenn man von einem Bug reden kann, evtl. ist es nur ein Fehler in der Doku)

Fazit: Nichts desto Troz kann ich MongoDb weiterempfehlen, wenn auch nicht uneingeschränkt. Ich habe Mongo jetzt mit Hilfe von Morphium auch in anderen Projekten eingesetzt (z.B. CalugaMed). Wenn man weiß, was man tut, kann mongo wirklich einen Großen Vorteil bringen. Da man nicht so fix an Strukturen gebunden ist, wie in einer Relationalen Datenbank, hat man viel mehr Möglichkeiten. Als "Datenmülleimer" ungeschlagen! Allerdings kann man auf Grund des fehlenden Zwangs zur Struktur auch mehr falsch machen! Datenmigrationen sind relativ leicht zu machen, die Anfragesprache (JavaScript) ist gut und mächtig (auch wenn immernoch ein brauchbares FrontEnd fehlt) und mit Morphium gibt es auch ein recht gutes Mapping-Framework (ich weiß, Eigenlob stinkt ;-) ).

Dennoch würde ich Mongo nicht für alles und immer benutzen. Man muss sich überlegen, was man benötigt. Wenn man z.B. Joins benötigt bzw. die Daten eben nicht so document based ablegen kann, so sind diese Anfragen in Mongo eher schwierig bis unmöglich abzubilden. Abhilfe schaft da evtl. auch der "Mut zur Redundanz". D.h. evtl. daten mehrfach ablegen. Komplexe Anfragen funktionieren zwar, können aber zu echten Problemen und Downtimes führen, wenn man nicht aufpasst (Indizes!!! Indizes!!! Indizes!!!).

Alles in Allem ist Mongo eine echte Alternative zu SQL-Datenbanken und kann in bestimmten Fällen echte Performance und flexibilitätsvorteile bringen. Einen Blick ist es auf jeden Fall wert.

mongo gibt es hier: www.mongodb.org

Morphium Projektseite: code.google.com/p/morphium

Morphium deutsch: www.caluga.de/morphium.html

created Stephan Bösebeck (stephan)