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)