MongoDB Erfahrungen / Tips und Tricks

veröffentlicht am : Mo, 17. 06. 2013 geändert am: Mo, 17. 06. 2013

Kategorie: Linux/Unix --> Computer

Schlagworte:


Noch ein paar Erfahrungen zum Thema mongodb... In den letzten Tagen habe ich wieder viel gelernt - teilweise mehr, als mir lieb sein kann...

Auf unserer Webseite de.holidayinsider.com nutzen wir MongoDB als primäres Data-Storage (wir haben PostgreSQL abgelöst). Wir waren schon kurz davor, die Entscheidung zu bereuen... aber eines nach dem anderen.

Installation

Die Installation von MongoDB ist denkbar leicht - einfach ein executeable ausführen und der Daemon läuft. Selbst als ReplicaSet (bzw. Cluster) ist MongoDB wirklich easy im Setup. Viel Logik, die sonst durch eigene Server gelöst wird, ist bei MongoDB in den Treiber gewandert. Der Treiber findet z.B. die aktiven Knoten raus und sendet Anfragen, je nach Konfiguration, an einen Sekundären oder Primären Knoten im Cluster.

Um das ganze automatisch beim Systemstart mit zu starten, muss man sich ein init-Script schreiben. Oder man verwendet die Pakete für die eigene distribution / das Betriebssystem der Wahl. Eventuell sind da auch schon startup-Skripte mit dabei. Wir haben alle mongod Parameter mit in das init-Script gepackt, man kann aber auch ein conf-file anlegen. Je nach gusto.

Die Firewalls sollten zwei Ports auf die MongoD zulassen - falls man den default nicht ändert: Port 27017 für den "binären" mongodb zugriff. Diesen Port nutzen die Treiber und die shell. Und Port 28017 (immer den Binär-Port +1000) für den HTTP-Zugriff (sofern man das möchte).

wir starten die Mongodb-Daemons folgendermaßen:

$MONGOHOME/bin/mongod --oplogSize $OPLOGSZ --rest --replSet REPL
  --pidfilepath /var/run/mongodb.pid --fork --cpu
  --dbpath $DATA --journal --logpath $LOG --logappend

Natürlich alles in einer Zeile. Kurze Erklärung zu den einzelnen Optionen:

--oplogsize: Damit legt man fest, wie viel Platz für das Operations-Log bereit gestellt werden soll. Dieses OpLog ist für die Replication wichtig und legt fest, wie weit dieser knoten hinter dem Rest "herhinken" darf.

--rest: Schaltet die Rest-API und den Zugriff via Webbrowser auf port 28017 ein.

--replSet REPL: spezifiziert den Namen des replicaset (s.u.)

--pidfilepath PATH: das file beinhaltet die PID des mongod nach dem Start

--fork: in den Hintergrund wechseln

--cpu: CPU Statistiken ins log schreiben

--dbpath: wo liegen die Daten

--journal: damit wird journalling eingeschaltet. Sollte man eigentlich immer anmachen, es sei denn, man benötigt noch mehr Performance

--logpath: wo liegt das logfile

--logappend: und hängen wir hinten an, oder wird überschrieben

Replicaset, ja oder nein? Sharding? oder was?

Im Zweifel: Ja! Denn sonst hat man mindestens eine Downtime, wenn man die Mongodatenverzeichnisse komprimieren will. Selbst wenn man keine zweite Maschine hat, ist man sogar noch besser dran, wenn man alles auf eine Maschine packt - das ist zwar nicht ausfallsicher, aber man hat eben den Vorteil, den ein oder anderen Knoten mal runter zu fahren und sich neu synchronisieren zu lassen.

Sharding ist auch so ein tolles Feature: im endeffekt werden dann die Daten auf grund von dem sog. Shard Key auf verschiedene Knoten verteilt. So könnte man z.B. die Daten auf Grund eine Timestamp verteilen. Oder abhängig vom Namen einer Person... oder oder oder.

An sich ne tolle Idee, so die last zu verteilen. (ihr hört vermutlich schon das ABER kommen, also: )

Aber leider ist das nicht immer sinnvoll. Denn z.B. auf Grund von einem Timestamp die Daten verteilen ist nicht unbedingt sinnig, da immer Chunks von 64 angelegt werden - d.h. es werden nicht die einzelnen requests verteilt. Außerdem müssen z.B. BulkInserts im Nachhinein evtl. noch "korrigiert" werden, d.h. die neu eingefügten Daten müssen evtl. auf andere Knoten verteilt werden.

Das hat bei uns zu Problemen geführt, die ich mittlerweile allerdings auf unser unzureichendes Storage und einige Konfigurationsfehler in den VMs zurückführe.

Einen kleinen Haken gibt es da auch noch: Das Setup wird ungleich komplizierter. Denn auch die Shards müssten eigentlich repliziert werden. D.h. die Daten müssten irgendwie redundant gehalten werden - 1. wegen der Ausfallsicherheit und 2. wg. dem oben genannten Komprimieren der Datenbank. Dann: einen Shard runter fahren alleine führt zu echt seltsamem Verhalten...

Wie man ein Replicaset aufsetzt kann man am besten auf der mongodb Seite nachlesen. Aber im Grunde sind nur folgende Dinge zu tun:

  • startet den Server mit der option -replset NAME, wobei NAME natürlich dem Namen in eurem Replicaset entspricht. Der muss auf allen Knoten des selben Sets gleich sein.
  • startet alle knoten im Replicaset
  • verbindet sich mit einem der Knoten mit der shell (mongo mongoserver/testdb). Dann erstellt die replicaset-Konfiguration in Java-Script. Das sieht in etwa so aus:
var config={
 "_id" : "REPLSETNAME",
 "version" : 38,
 "members" : [
 {
 "_id" : 0,
 "host" : "node1:27017",
 "priority" : 5
 },
 {
 "_id" : 1,
 "host" : "node2:27017",
 "priority" : 3
 },
 {
 "_id" : 10,
 "host" : "mongohidden:27017",
 "priority" : 0,
 "hidden" : true
 },
 {
 "_id" : 11,
 "host" : "arbiter:27017",
 "arbiterOnly" : true
 }
 ]
}
  • Natürlich müsst ihr die Rechnernamen und den Replicasetnamen korrigieren... in dem Beispiel ist auch ein Arbiter mit drin, der so gar nicht nötig wäre.
  • mit rs.initiate(config) wird das Replicaset initialisiert - voilá

Beim Aufsetzen eines Replicaset solltet ihr auf folgendes achten:

  • ungerade Anzahl an Knoten - falls das nicht geht, installiert einen sog. Arbiter. Das ist ein Mini-Mongodb-Daemon, der nur bei Wahlen zum Master mit macht. Da sich kein Knoten selbst wählen darf (er könnte ja auf Grund eines Netzproblems isoliert sein), gäbe es bei 2 Knoten sonst ein Problem, wenn einer ausfällt.
  • gebt dabei allen knoten eine Priority. Dann wird der Knoten mit der höchsten Priority bevorzugt master. Das mach es etwas vorhersagbarer
  • bei Bedarf kann man auch sog. "hidden" nodes anlegen (siehe Beispiel oben). Die sind ganz praktisch als "hot" backup (priority:0, hidden: true)

Viel Logik im Treiber

Der Treiber bei mongo ist relativ intelligent. Man startet ja nur die MongoD und teilt ihnen mit, dass sie sich in einem Replicaset befinden - that's it! Ein Treiber muss sich dessen "bewusst" sein und auf evtl. auftretende Fehler / Ausfälle reagiren. Klar, das ist nur kompliziert, wenn wir uns in einer replizierten / geclusterten Umgebung befinden. bei einem Single-Server-Setup ist ja alles simple. Aber wenn wir uns mit einem ReplicaSet "unterhalten" wollen, sieht das etwas anders aus.

So muss der Treiber als erstes mal rausfinden, welche Knoten gibt es überhaupt. Dafür übergibt man ihm einen sog. Seed. Das ist eine Liste von Serveradressen, da sollte tunlichst einer dabei sein, der auch antwortet. Diesen Fragt er dann nach dem ReplicaSet, welche Server gibt es, sind die alle da und wer ist der Master. Schreibzugriffe können nur auf dem Master-Server durchgeführt werden. Diese werden dann an alle anderen Knoten im Set repliziert (daher der Name).

Dummerweise kann sich der Master im Laufe der Zeit auch ändern - weil er z.B. runter gefahren wird oder sonst wie. Dann wird ein anderer Master gewählt und das muss der Treiber berücksichtigen.

Dieses Prinzip ist zunächst mal gewöhnungsbedürftig, aber es funktioniert - fast ;-) Das Problem ist, dass es auch im Treiber Fehler gab / gibt, die dann natürlich wie Replizierungsfehler oder so aussehen. Alles was ich hier sage bezieht sich auf die MonogDB-Version 2.4.x und die Programmiersprache Java. Und genau da gab es z.B. einen ziemlich nervigen Bug.

Wenn man eine Verbindung zur MongoDb über den Treiber herstellt, muss der sich natürlich mit mindestens einem Knoten im Cluster verbinden. Das klappt so weit auch, jedoch gab es in der Version 2.10.1 des Treibers bei uns den Fehler, dass er, obwohl der Knoten nicht verfügbar war, immer wieder Anfragen dort hin geschickt hat. Das hat zu unnötigen Exceptions und Timeouts geführt - und Downtimes. Zum Glück wurde der Fehler relativ schnell wieder behoben, und die aktuelle Version 2.11.1 hat diesen Fehler nicht.

Es gibt kein Compact-DB

Das ist wirklich ein Problem. MongoDb erzeugt sozusagen ein virtuelles Filesystem auf dem Dateisystem des Betriebssystems. Dabei werden, je nach Bedarf, immer wieder neue Dateien angelegt (bis zu 2GB), die dann die eigentlichen Daten beinhalten. So weit, so gut, jedoch wird der Speicherplatz nie wieder freigegeben. D.h. der Belegte speicherplatz wächst stetig.

Ähnliche Probleme gibt es auch mit MySQL oder PostgreSQL, aber auch eine Lösung: Das sog. Vaccuuming der Datenbank. Diese Funktionalität gibt es in Mongodb allerdings nicht. Es gibt dabei nur diese Möglichkeiten, das Problem in den Griff zu bekommen:

  • so viel Speicherplatz bereit stellen, dass das nicht passiert (klar, ist etwas blauäugig, aber kann eine ganze Weile gut gehen). Vorteil: keine Downtime
  • das Kommando repairDatabase regelmäßig ausführen, bzw. den mongod mit der Option --repair starten. Dieses hat jedoch ein paar Tradeoffs: Während dieser Aktion ist der Knoten offline, nimmt keine Anfragen entgegen und es wird im endeffekt das gesamte Datenverzeichnis an Ort und Stelle kopiert => man benötigt doppelt so viel platz, damit das überhaupt möglich ist.
  • In regelmäßigen Abständen den mongod stoppen, das Datenverzeichnis komplett löschen und den Knoten neu synchronisieren lassen. Das ist zwar auch mit Downtime verbunden, aber braucht wesentlich weniger spare space wie die repairDatabase-Option - geht nur im Replicaset, klar, oder?
  • Die Datenbank auf einem Knoten mit einem der oberen beiden Varianten "komprimieren" und das Datenverzeichnis dann auf die anderen Knoten kopieren (z.B. mit rsync). Achtung: die MongoD auf den Zielknoten _müssen_ gestoppt sein! Der mongoD darf nicht laufen! Denn sonst wird das ganze Datenverzeichnis gelöscht. Auch nicht wünschenswert... Das löschen kann man erreichen, indem man einfach mit kill den mongod prozess beendet (Bitte nicht mit Kill -9!)

Wir haben uns für die vorletzte Variante entschieden. Jede Woche wird ein anderer Knoten komprimiert und seit der Treiber währenddessen keine Fehler mehr produziert, klappt das auch ohne Downtime.

Falls jemand noch eine Idee hat, was man tun kann, dann bitte per Kommentar unten hinzufügen.

Mongo ist extrem RAM- und IO-lastig

Eigentlich logisch, oder? Mongo nutzt Memory Mapped files, um schnell auf die Daten zugreifen zu können. Dazu müssen die aber auch ins RAM passen (bzw. zumindest die Files, die die Indices beinhalten).

Wenn man jetzt noch regelmäßige Updates auf den Daten fährt, ist es zwingend nötig, dass das Storage schnell genug ist. Bei uns gab es diesbezüglich einen Engpass, mit dem Effekt, dass mongo beim schreiben (und manchmal auch beim Lesen) Timeouts geliefert hat (die Zeitspanne kann konfiguriert werden). Das war super schwer zu finden, lag bei uns aber an einem überlastetem Storage.

Es wird auch empfohlen den Read-Ahead des Devices, auf dem die Mongo-Daten liegen, möglichst runter zu drehen. Empfohlen wird ein wert von 32 Blöcken / 16 KB. Unter linux kann man das recht einfach machen mit

blockdev --setra 32 /dev/sdb1

Mongo ist extrem abhängig von Indizes

Das ist das wichtigste überhaupt: Für alle Suchenzugriffe auf etwas größere oder große Collections (= Table in SQL-Nomenklatur) ist es nahezu zwingend nötig, einen brauchbaren Index definiert zu haben. Denn bei einem "FullTableScan" wartet man bei Mongo laaaaaaange!

Plant eure Datenstruktur etc entsprechend, legt sinnvolle Indizes an. Bei der Datenstruktur sind auch Redundanzen kein Problem (ist ja etwas, was man bei SQL tunlichst vermeidet). Aber lieber etwas zu doppelt gespeichert aber die Zugriffszeiten gezehntelt, als am Platz gespart.

Mongo benötigt Struktur

Ja ja, NoSQL-Datenbanken sind ja so schlecht, weil sie keine Struktur haben. Falsch: sie erzwingen keine. Benötigt wird sie aber dennoch! Ist sogar nahezu zwingend erforderlich. Achtet darauf, dass Indizes auf euren Collections sind - nur so könnt ihr performant mit der Mongo arbeiten.

Der große Vorteil der Schemalosigkeit kann auch schnell zum Nachteil werden, wenn man nicht aufpasst. Da ist Programmierdisziplin gefragt.

Wenn möglich sollte man einen Object-Mapper verwenden, damit man nicht direkt auf der Mongo umprogrammiert. Wir nutzen Morphium als Mapper, da das auch Caching, Deklarative Validation und einige andere Features mitbringt.

Es gibt keine "Joins"

Das wird gerne mal vergessen, mongoDb kann nicht joinen. D.h. man muss seine Datenstruktur auch entsprechend designen. Joins sind in Mongo deswegen nur möglich, wenn man sie ausprogrammiert, oder in MapReduce oder dem aktuellen Aggregation-Framework abbildet. Das ist alles viel mächtiger als reine Joins - allerdings auch viel langsamer!

Falls man in seinem Programm an eine Stelle kommt, wo man zwingend joinen muss, sollte man sich evtl. fragen, ob die Datenbank korrekt designed ist.

Mongo ist schnell

Das ist wirklich ein wichtiges Kriterium: Mehrere 10.000 Schreibzugriffe pro Minute sind kein Problem, zeitgleiches Lesen auch nicht. Jedoch muss man auf die obigen Hinweise achten, sonst kann das nicht funktionieren.

Achtung: auch hier gilt wieder, wenn das storage lahm ist, kann Mongo nicht schnell sein! Das mussten wir am eigenen Leib erfahren, das unser Storage leider doch so seine Tücken hat:

 

Das ist übrigens ein Beispiel dafür, wie wir Graphite einsetzen und was wir alles messen. In diesem Fall sind des die LAufzeiten der Lese und Schreibzugriffe. Das konnten wir recht leicht realisieren, da morphium eine Schnittstelle für sog. Profiling Listener bietet. Dort mussten wir die Daten dann nur noch nach Graphite schicken.

Überwachung der Mongo-Knoten

Da gibt es auch verschiedene Möglichkeiten. Die Mongo Installation liefert schon einige Tools, mit denen man was tun kann:

  • mongotop zeigt an, wie lange Schreib- und Lesezugriffe auf bestimmten Daten dauern.
  • mongostat zeigt mehr über technischen Hintergrund, wie viel ram etc.
  • mongosniff zeigt den von mongo verursachten traffik an, ähnlich tcpdump

Wer ein wenig scripten kann, kann natürlich auch selbst was bauen, oder man nutzt den kostenfreien Service von 10Gen namens MMS.

Leider frisst der MMS-Agent bei uns binnen kürzester Zeit 80% RAM, so dass nix anderes mehr läuft. Eine Lösung konnten wir bisher noch nicht finden.

Die Überwachung der Mongo geht aber auch "zu Fuß", d.h. mit ein wenig Bash- oder sonstwie Skripterei. Der Mongo client kann alle möglichen Informationen über das Replicatset auslesen. z.B. der ReplicationLag, d.h. wie viel ein Knoten hinter dem Master-Knoten her hinkt. das ist eine wichtige Kennzahl, die viel darüber aussagt, ob z.B. das storage passt.

So kann man den Status des Replicaset inkl des ReplicationLag (also wie weit welcher Knoten hinter dem Master her hinkt) mit dem Kommando:

mongo host:port/datenbank --eval 'rs.status()'

die Ausgabe ist sehr ausführlich und liefert den Status eines jeden Mongo-Knoten:

{
 "set" : "tst",
 "date" : ISODate("2013-06-18T18:56:58Z"),
 "myState" : 2,
 "syncingTo" : "127.0.0.1:27018",
 "members" : [
 {
 "_id" : 0,
 "name" : "127.0.0.1:27017",
 "health" : 1,
 "state" : 2,
 "stateStr" : "SECONDARY",
 "uptime" : 96,
 "optime" : {
 "t" : 1370329384,
 "i" : 1
 },
 "optimeDate" : ISODate("2013-06-04T07:03:04Z"),
 "errmsg" : "syncing to: 127.0.0.1:27018",
 "self" : true
 },
 {
 "_id" : 1,
 "name" : "127.0.0.1:27018",
 "health" : 1,
 "state" : 1,
 "stateStr" : "PRIMARY",
 "uptime" : 93,
 "optime" : {
 "t" : 1370329384,
 "i" : 1
 },
 "optimeDate" : ISODate("2013-06-04T07:03:04Z"),
 "lastHeartbeat" : ISODate("2013-06-18T18:56:57Z"),
 "lastHeartbeatRecv" : ISODate("1970-01-01T00:00:00Z"),
 "pingMs" : 0
 },
 {
 "_id" : 2,
 "name" : "127.0.0.1:27019",
 "health" : 1,
 "state" : 2,
 "stateStr" : "SECONDARY",
 "uptime" : 93,
 "optime" : {
 "t" : 1370329384,
 "i" : 1
 },
 "optimeDate" : ISODate("2013-06-04T07:03:04Z"),
 "lastHeartbeat" : ISODate("2013-06-18T18:56:57Z"),
 "lastHeartbeatRecv" : ISODate("1970-01-01T00:00:00Z"),
 "pingMs" : 0,
 "lastHeartbeatMessage" : "syncing to: 127.0.0.1:27018",
 "syncingTo" : "127.0.0.1:27018"
 }
 ],
 "ok" : 1
}

Wir senden diese Daten dann noch in Graphite um diese bei bedarf auch historisch auswerten zu können.

Des weiteren bekommt man so auch gleich mit, ob der Knoten noch up ist, oder nicht. Da wir Graphite auch für viele andere Werte benutzen, war es für uns naheliegend, das so zu implementieren.

Interaktiv kann man sich das ganze natürlich einerseits über die Shell oder über das Web-Interface ansehen. Wenn man auf Port 28017 des Servers geht (mit dem Webbrowser) so erhält man da noch weit mehr Informationen, wie z.B. wie viele und welche Verbindungen sind offen, welche Requests laufen gerade etc.

Mongo kann keine Transaktionen

ACID sucht man bei Mongo vergeblich, schon gar nicht verteilt. Standardmäßig ist jede Schreiboperation ein "Fire and forget"-Vorgang. Der Schrieibzugriff wird weiter gegeben, und das wars.

Man kann da zwar ein wenig Sicherheit erreichen, in dem man dem Treiber mitteilt, er soll gefälligst darauf warten, dass die Daten geschrieben wurden oder von anderen Knoten bestätigt... aber das ist weit von Transaktionen entfernt.

Für das Messaging von morpium habe ich eine Art "Transaktion-Light" auf Mongo eingerichtet. Die Vorgehensweise basiert darauf, dass Update-Prozesse atomar ablaufen und die ACID Bedingungen erfüllen. In Pseudo-Code sieht es so aus:

  1. Blocke ein Entry für die Bearbeitung, indem eine eigene eindeutige ID an den Eintrag ran geschrieben wird.
  2. Lese alle mit meiner ID geblockten Einträge und bearbeite sie
  3. Entferne meine ID von allen Objekten

Das ist nicht 100% fehlerfrei, hat in meinen Tests aber ganz gut funktioniert. Das würde natürlich noch zu Dirty-Reads führen, man müsste halt alle Lesezugriffe um den Zusatz "blockedID==null" erweitern... dann würde es relativ einfach funktionieren.

Wenn man noch ein Blocking auf Collection-Ebene einführen wollte, würde das auch gehen.

ABER: man muss quasi das Transaktion-Handling komplett selbst implementieren. Und dann noch an "falscher" Stelle im Application Stack. Denn egal wie man es dreht, die Logik wäre Teil der Application und nicht teil der Datenbank. Das kann also keinesfalls so sicher sein, wie eine "echte" Transaktion.

Das Kann ein K.O.-Kriterium sein, das gegen die Verwendung von Mongo spricht!

Fazit:

wir hatten so unsere Schwierigkeiten mit der MongoDB, wobei die (fast) völlig unschuldig war. Es lag am Storage oder am Java-Treiber für Mongo. Jetzt läuft wieder alles stabil und flott und zusammen mit Morphium haben wir ein richtig gutes Setup erreicht.

Ich kann mongo nicht uneingeschränkt empfehlen, man sollte sich schon klar werden, ob es einem Vorteile bringt, ob die Nachteile evtl. nicht überwiegen.

Ich hab jetzt mehrere Projekte mit MongoDB realisiert und in diesen Fällen ist es auf jeden Fall richtig gewesen.

erstellt Stephan Bösebeck (stephan)