18 Replica Set in MongoDB

Ein Replica Set ist MongoDBs Antwort auf die grundlegende Frage jeder produktiven Datenbank: Was passiert, wenn Hardware ausfällt? Eine Standalone-Instanz, wie im vorherigen Kapitel installiert, bietet keine Ausfallsicherheit. Stirbt der Server, ist die Datenbank nicht verfügbar. Für Entwicklung und Tests mag dies akzeptabel sein, für Produktion ist es inakzeptabel.

Replica Sets lösen dieses Problem durch Replikation und automatisches Failover. Mehrere MongoDB-Instanzen halten dieselben Daten. Fällt eine aus, übernehmen die anderen. Dieser Mechanismus ist nicht optional für produktive Systeme, sondern Standard. Selbst kleine Deployments sollten mindestens als Replica Set konfiguriert werden. Die operative Komplexität ist überschaubar, aber der Gewinn an Robustheit ist immens.

18.1 Architektur eines Replica Sets: Primary, Secondaries und das Oplog

Ein Replica Set besteht aus mehreren mongod-Prozessen, typischerweise drei oder mehr. Diese Prozesse sind keine gleichberechtigten Peers, sondern folgen einer klaren Hierarchie. Zu jedem Zeitpunkt gibt es genau einen Primary – den autoritativen Knoten, der alle Schreiboperationen entgegennimmt. Die anderen Knoten sind Secondaries, die Änderungen vom Primary replizieren.

Diese Asymmetrie ist bewusst gewählt. Alternative Architekturen wie Multi-Master-Systeme, wo mehrere Knoten Writes akzeptieren, haben fundamentale Probleme mit Konflikten und Konsistenz. MongoDB vermeidet diese Komplexität durch das Single-Primary-Modell. Alle Writes gehen an einen Knoten, der autoritativ entscheidet. Dies garantiert konsistente Schreibreihenfolge ohne komplizierte Konfliktauflösung.

Der Primary schreibt jede Operation in ein spezielles Log: das Oplog (Operations Log). Dieses Oplog ist eine spezielle Collection local.oplog.rs, die als Capped Collection implementiert ist – eine ringförmige Struktur mit fester Größe, wo alte Einträge automatisch überschrieben werden, wenn der Platz ausgeht. Jeder Eintrag im Oplog repräsentiert eine Operation: Insert, Update, Delete.

Secondaries lesen kontinuierlich das Oplog und wenden die Operationen auf ihre Kopie der Daten an. Dieser Prozess ist asynchron – es gibt eine kleine, aber unvermeidbare Verzögerung zwischen einer Operation auf dem Primary und ihrer Replikation zu Secondaries. Diese Replication Lag ist typischerweise Millisekunden, kann aber bei hoher Last oder langsamen Secondaries Sekunden betragen.

Die Oplog-Größe ist kritisch. Sie bestimmt, wie lange ein Secondary offline sein kann, ohne den Anschluss zu verlieren. Ist das Oplog 24 Stunden groß (kann mit großen Operations gefüllt werden in wenigen Stunden oder mit wenigen Operations in Tagen), kann ein Secondary 24 Stunden ausfallen und nach Rückkehr noch aufholen. Ist es kleiner und der Secondary war länger weg, hat der Primary die benötigten Oplog-Einträge bereits überschrieben. Der Secondary muss dann komplett neu initialisiert werden – ein teurer Prozess bei großen Datenbanken.

18.2 Elections: Automatisches Failover bei Ausfällen

Die wahre Stärke eines Replica Sets zeigt sich im Fehlerfall. Fällt der Primary aus – durch Hardware-Defekt, Netzwerkproblem oder geplante Wartung – initiieren die Secondaries automatisch eine Election. Innerhalb von Sekunden wählen sie einen neuen Primary. Die Anwendung verbindet sich über einen Connection String mit dem gesamten Replica Set, nicht mit einzelnen Nodes. Der MongoDB-Treiber erkennt den Primary-Wechsel automatisch und leitet Operationen zum neuen Primary um.

Elections basieren auf einem Mehrheitsprinzip. Ein Knoten wird Primary, wenn er die Mehrheit der Stimmen erhält. Dies erfordert eine ungerade Anzahl stimmberechtigter Mitglieder. Mit drei Nodes hat jeder eine Stimme – zwei Stimmen sind eine Mehrheit. Bei zwei Nodes würde ein Ausfall bedeuten, dass keine Mehrheit existiert. Das verbleibende Node darf nicht Primary werden, weil es nicht weiß, ob das andere Node wirklich tot ist oder nur die Netzwerkverbindung getrennt wurde (Split-Brain-Problem).

Für gerade Anzahlen von Nodes bietet MongoDB eine Lösung: den Arbiter. Ein Arbiter ist ein spezieller Knoten, der an Elections teilnimmt, aber keine Daten speichert. Er existiert rein, um eine Stimme beizutragen. Ein Setup mit zwei Daten-Nodes plus einem Arbiter kann Elections durchführen (3 Stimmen, Mehrheit = 2), wobei der Arbiter minimal Ressourcen verbraucht – typischerweise läuft er auf einer VM mit 1 GB RAM.

Die Election-Logik berücksichtigt mehrere Faktoren. Prioritäten können Knoten bevorzugen – ein Node mit Priorität 2 wird über einen mit Priorität 1 gewählt, wenn beide verfügbar sind. Die Oplog-Position spielt eine Rolle – ein Node mit neueren Daten wird bevorzugt, um Datenverlust zu minimieren. Und die Netzwerk-Latenz zählt – ein Node, der schnell auf Heartbeats reagiert, ist bevorzugt.

Die Election dauert typischerweise 5-15 Sekunden. In dieser Zeit ist das Replica Set read-only – Secondaries akzeptieren weiterhin Reads (abhängig von Read Preference), aber keine Writes, weil kein Primary existiert. Nach erfolgreicher Election normalisiert sich der Betrieb. Der neue Primary akzeptiert Writes, die Secondaries replizieren von ihm.

18.3 Read Preferences: Lastverteilung und Konsistenz-Trade-offs

Standardmäßig gehen alle Operationen – Reads und Writes – an den Primary. Dies garantiert maximale Konsistenz: Jeder Read sieht alle vorherigen Writes. Aber es bedeutet auch, dass der Primary die gesamte Last trägt. Secondaries replizieren nur, bedienen keine Queries.

Read Preferences ändern dies. Sie erlauben es, Reads auf Secondaries zu verteilen. Dies hat zwei Vorteile: Der Primary wird entlastet, und Reads können skalieren, indem man mehr Secondaries hinzufügt. Der Trade-off: Reads von Secondaries können veraltete Daten sehen, wegen Replication Lag.

MongoDB bietet fünf Read Preference Modi:

primary (Standard): Alle Reads vom Primary. Maximale Konsistenz, keine Skalierung von Reads.

primaryPreferred: Primary bevorzugt, aber wenn nicht verfügbar (etwa während Election), dann Secondary. Praktisch für Hochverfügbarkeit, akzeptiert temporäre Staleness.

secondary: Nur von Secondaries lesen. Primary wird komplett entlastet. Akzeptiert Staleness bewusst.

secondaryPreferred: Secondary bevorzugt, aber Primary als Fallback. Nützlich für Analytics-Workloads, die nicht aktuellste Daten brauchen.

nearest: Liest vom Node mit niedrigster Netzwerk-Latenz, egal ob Primary oder Secondary. Optimiert für Latenz über Konsistenz.

Die Wahl hängt vom Use-Case ab. Transaktionale Anwendungen, wo Konsistenz kritisch ist, nutzen primary. Analytics-Dashboards, die Latenz 10-20 Sekunden tolerieren können, nutzen secondary oder secondaryPreferred. Geo-verteilte Anwendungen nutzen nearest, um lokale Nodes zu bevorzugen.

// Connection String mit Read Preference
mongodb://localhost:27017,localhost:27018,localhost:27019/?replicaSet=myrs&readPreference=secondaryPreferred

// Oder programmatisch in der Shell
db.getMongo().setReadPref("secondary")

// Oder per-Query
db.users.find({...}).readPref("nearest")

Ein wichtiger Punkt: Auch mit secondary Read Preference gehen Writes immer an den Primary. Read Preferences betreffen nur Reads. Dies bedeutet, dass ein Write gefolgt von einem Read die geschriebenen Daten nicht sehen könnte, wenn der Read zu einem Secondary mit Replication Lag geroutet wird. Anwendungen müssen dies handhaben – etwa durch Verwendung von Read Concerns oder explizites Lesen vom Primary nach Writes.

18.4 Hands-On: Ein lokales Replica Set aufsetzen

Die Theorie ist verständlich, aber ein funktionierendes Replica Set selbst aufzusetzen vermittelt das Verständnis besser als jede Erklärung. Wir bauen ein lokales 3-Node-Replica-Set auf einem einzelnen Host. In Produktion würden diese Nodes auf separaten Servern laufen, aber für Lernen und Tests funktioniert dies perfekt.

Jede MongoDB-Instanz braucht ein eigenes Datenverzeichnis und einen eigenen Port. Wir erstellen drei Verzeichnisse und starten drei mongod-Prozesse:

# Verzeichnisse erstellen
mkdir -p ~/mongodb-replica/node1
mkdir -p ~/mongodb-replica/node2
mkdir -p ~/mongodb-replica/node3

# Node 1 starten (Port 27017)
mongod --port 27017 \
       --dbpath ~/mongodb-replica/node1 \
       --replSet myReplSet \
       --bind_ip localhost \
       --fork \
       --logpath ~/mongodb-replica/node1/mongod.log

# Node 2 starten (Port 27018)
mongod --port 27018 \
       --dbpath ~/mongodb-replica/node2 \
       --replSet myReplSet \
       --bind_ip localhost \
       --fork \
       --logpath ~/mongodb-replica/node2/mongod.log

# Node 3 starten (Port 27019)
mongod --port 27019 \
       --dbpath ~/mongodb-replica/node3 \
       --replSet myReplSet \
       --bind_ip localhost \
       --fork \
       --logpath ~/mongodb-replica/node3/mongod.log

Die entscheidenden Parameter: --replSet myReplSet teilt jedem Node mit, dass er Teil eines Replica Sets namens “myReplSet” ist. Alle Nodes eines Sets müssen denselben Namen haben. Der --fork-Parameter forkt den Prozess in den Hintergrund, --logpath spezifiziert, wo Logs geschrieben werden.

Nach dem Start laufen drei unabhängige MongoDB-Instanzen. Sie wissen, dass sie Teil eines Replica Sets sein sollen, aber das Set ist noch nicht initialisiert. Wir verbinden uns mit einem der Nodes und initialisieren:

mongosh --port 27017

In der Shell:

rs.initiate({
  _id: "myReplSet",
  members: [
    { _id: 0, host: "localhost:27017" },
    { _id: 1, host: "localhost:27018" },
    { _id: 2, host: "localhost:27019" }
  ]
})

Der rs.initiate()-Befehl konfiguriert das Replica Set. Das Konfigurationsdokument definiert den Set-Namen (_id) und die Members. Jedes Member hat eine eindeutige _id (numerisch, startet bei 0) und einen host (Hostname:Port).

Nach der Initiierung passiert innerhalb von Sekunden eine Election. Ein Node wird Primary, die anderen Secondaries. Der Shell-Prompt ändert sich, um die Rolle anzuzeigen:

myReplSet [direct: primary] test>

Das [direct: primary] zeigt, dass wir mit dem Primary verbunden sind. Wären wir mit einem Secondary verbunden, würde es [direct: secondary] zeigen.

Der Status des Replica Sets ist abfragbar mit rs.status():

rs.status()

Dieses Command gibt ein umfangreiches Dokument zurück mit Details zu jedem Member: Zustand (PRIMARY, SECONDARY, ARBITER), Oplog-Position, Replication Lag, Heartbeat-Status. Für ein gerade gestartetes Set sollten alle Members healthy sein, der Primary sollte Oplog-Einträge haben (auch wenn noch keine Daten geschrieben wurden), und die Secondaries sollten synchron sein.

// Auszug aus rs.status()
{
  set: 'myReplSet',
  members: [
    {
      _id: 0,
      name: 'localhost:27017',
      health: 1,
      state: 1,  // PRIMARY
      stateStr: 'PRIMARY',
      uptime: 245,
      optime: { ts: Timestamp(...), t: Long("1") },
      optimeDate: ISODate("2024-01-15T10:30:00.000Z")
    },
    {
      _id: 1,
      name: 'localhost:27018',
      health: 1,
      state: 2,  // SECONDARY
      stateStr: 'SECONDARY',
      uptime: 245,
      syncSourceHost: 'localhost:27017',
      optime: { ts: Timestamp(...), t: Long("1") }
    },
    // ...
  ]
}

Das syncSourceHost-Feld zeigt, von welchem Node der Secondary repliziert. Typischerweise vom Primary, aber Secondaries können auch von anderen Secondaries replizieren, um Primary-Last zu reduzieren.

18.5 Failover testen: Den Primary simuliert töten

Die wahre Bewährungsprobe eines Replica Sets ist das Failover. Wir simulieren einen Primary-Ausfall, indem wir seinen Prozess killen:

# Primary-PID finden
ps aux | grep "mongod.*27017"

# Prozess töten
kill -9 <pid>

Das -9 Signal terminiert den Prozess sofort, ohne Chance für Cleanup. Dies simuliert einen Hard-Crash wie Stromausfall. Innerhalb weniger Sekunden erkennen die verbliebenen Nodes den Ausfall (fehlende Heartbeats) und starten eine Election.

Verbinden wir uns mit einem der überlebenden Nodes:

mongosh --port 27018

Und prüfen den Status:

rs.status()

Wir sehen, dass localhost:27017 als “not reachable/healthy” markiert ist, und einer der anderen Nodes (27018 oder 27019) ist nun PRIMARY. Die Election dauerte Sekunden, keine manuelle Intervention war nötig.

Wir können den ausgefallenen Node wieder starten:

mongod --port 27017 \
       --dbpath ~/mongodb-replica/node1 \
       --replSet myReplSet \
       --bind_ip localhost \
       --fork \
       --logpath ~/mongodb-replica/node1/mongod.log

Nach dem Start tritt er automatisch dem Replica Set bei, repliziert verpasste Operationen aus dem Oplog der anderen Nodes, und wird Secondary. Er wird nicht automatisch wieder Primary – der aktuelle Primary bleibt Primary, bis er ausfällt. Dies ist bewusst so, um Flapping zu vermeiden.

18.6 Write Concerns und Read Concerns: Konsistenz-Garantien

Replica Sets sind asynchron, aber Anwendungen können Garantien erzwingen. Write Concerns spezifizieren, wie viele Nodes eine Operation bestätigen müssen, bevor sie als erfolgreich gilt. Read Concerns spezifizieren, welche Konsistenz-Garantien Reads haben.

Der Standard-Write-Concern ist w: 1 – der Write gilt als committed, sobald der Primary ihn persistiert hat. Secondaries replizieren asynchron später. Dies ist schnell, aber bedeutet: Fällt der Primary unmittelbar nach dem Write aus, bevor Replikation erfolgte, ist der Write verloren. Der neue Primary hat ihn nicht.

Für kritische Writes ist w: "majority" sicherer:

db.orders.insertOne(
  { customer: "Alice", total: 99.99 },
  { writeConcern: { w: "majority", wtimeout: 5000 } }
)

Dies wartet, bis eine Mehrheit der Nodes (bei 3 Nodes also 2) den Write persistiert hat. Erst dann returned das Command erfolgreich. Falls der Primary jetzt ausfällt, hat mindestens ein Secondary den Write, und der wird zum neuen Primary. Der Write ist garantiert nicht verloren.

Der wtimeout Parameter setzt ein Timeout. Kann die Mehrheit nicht innerhalb 5 Sekunden erreicht werden (etwa weil ein Secondary down oder langsam ist), schlägt der Write fehl. Dies ist wichtig, damit Anwendungen nicht ewig warten.

Read Concerns arbeiten analog. Der Standard local liest die neuesten Daten vom jeweiligen Node, ohne Garantien über Replikation. majority garantiert, dass gelesene Daten von einer Mehrheit bestätigt wurden – sie werden nicht bei Rollback verloren gehen.

db.orders.find({ customer: "Alice" })
  .readConcern("majority")

Die Kombination von writeConcern: { w: "majority" } und readConcern: "majority" garantiert Linearizability – eine starke Konsistenz-Garantie, die man normalerweise mit asynchronen Systemen nicht assoziiert. Der Preis: Latenz. Majority-Writes und -Reads sind langsamer als defaults.

Replica Sets sind die Basis jedes produktiven MongoDB-Deployments. Sie eliminieren Single Points of Failure, automatisieren Failover und ermöglichen Lastverteilung. Die operative Komplexität ist moderat – drei Nodes statt einem, etwas Konfiguration, aber keine fundamentale Architektur-Änderung. Der Gewinn an Robustheit ist immens. Eine Standalone-Instanz ist für Tests, ein Replica Set ist für Produktion. Diese Regel hat keine Ausnahmen.