21 Hands On: Installation eines MongoDB Replica Sets

Die theoretischen Grundlagen von Replica Sets sind wichtig, aber echtes Verständnis kommt durch praktische Erfahrung. In diesem Kapitel bauen wir ein vollständiges Replica Set von Grund auf – nicht auf einem einzelnen Host mit unterschiedlichen Ports wie im Einführungskapitel, sondern auf drei separaten Servern, wie es in Produktionsumgebungen der Fall wäre. Die Prinzipien sind identisch, aber die praktischen Herausforderungen – Netzwerk-Konfiguration, DNS, Firewalls – werden sichtbar.

Wir gehen davon aus, dass drei Server zur Verfügung stehen. In der Cloud könnten dies drei VMs sein, on-premise drei physische Server oder Bare-Metal-Hosts. Die Hostnamen sind mongo1.example.com, mongo2.example.com und mongo3.example.com. Diese Namen müssen von allen Servern auflösbar sein – entweder durch DNS oder durch Einträge in /etc/hosts. Die IP-Adressen sind 10.0.1.11, 10.0.1.12 und 10.0.1.13 in einem privaten Netzwerk.

21.1 Vorbereitung: MongoDB auf allen Nodes installieren

Bevor wir das Replica Set konfigurieren können, muss MongoDB auf allen drei Servern installiert sein. Der Prozess folgt dem in Kapitel 16 beschriebenen Ablauf, aber wir durchlaufen ihn kurz für den Kontext.

Auf jedem Server fügen wir das MongoDB-Repository hinzu und installieren das mongodb-org-Paket. Für Ubuntu 22.04:

# Auf mongo1, mongo2 und mongo3
wget -qO - https://www.mongodb.org/static/pgp/server-7.0.asc | sudo apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
sudo apt update
sudo apt install -y mongodb-org

Nach der Installation existiert MongoDB als installiertes Paket, läuft aber noch nicht. Der nächste Schritt ist Konfiguration – und hier weicht das Replica-Set-Setup vom Standalone ab.

21.2 Konfiguration: mongod.conf für Replica-Set-Betrieb

Jeder Node benötigt eine angepasste mongod.conf. Die Standard-Config, die die Installation erstellt, ist für Standalone-Betrieb gedacht. Wir müssen drei kritische Änderungen vornehmen: Die Bind-IP auf alle Interfaces setzen (oder spezifische IPs), den Replica-Set-Namen definieren und optional Authentifizierung aktivieren.

Die /etc/mongod.conf auf mongo1 könnte so aussehen:

# Network-Konfiguration
net:
  port: 27017
  bindIp: 0.0.0.0  # Lauscht auf allen Interfaces

# Storage
storage:
  dbPath: /var/lib/mongodb
  journal:
    enabled: true
  engine: wiredTiger

# Logging
systemLog:
  destination: file
  logAppend: true
  path: /var/log/mongodb/mongod.log

# Replication
replication:
  replSetName: "prodReplSet"

# Security (initial deaktiviert für Setup)
# security:
#   authorization: enabled
#   keyFile: /etc/mongodb-keyfile

Die wichtigsten Parameter für Replica-Set-Betrieb:

Die bindIp: 0.0.0.0-Einstellung bedeutet, dass MongoDB auf allen Netzwerk-Interfaces lauscht. Dies ist notwendig, damit andere Nodes verbinden können. Der Standalone-Default 127.0.0.1 würde nur localhost-Verbindungen erlauben. In Produktionsumgebungen mit mehreren Netzwerk-Interfaces sollte man spezifische IPs angeben: bindIp: 10.0.1.11 etwa, um nur auf dem privaten Netzwerk-Interface zu lauschen.

Der replSetName ist kritisch. Alle Nodes, die Teil desselben Replica Sets sein sollen, müssen denselben Namen haben. Hier ist es “prodReplSet”, aber der Name ist frei wählbar. Wichtig: Der Name ist Case-Sensitive und darf keine Leerzeichen enthalten. Konventionell nutzt man camelCase oder kebab-case.

Diese Konfiguration muss auf alle drei Server kopiert werden, mit einer Ausnahme: Die bindIp sollte idealerweise auf jedem Server die jeweilige spezifische IP setzen. Auf mongo1: bindIp: 10.0.1.11, auf mongo2: bindIp: 10.0.1.12, usw. Dies ist sicherer als 0.0.0.0, aber 0.0.0.0 funktioniert und ist für Tests oder wenn man nicht um spezifische IPs kämpfen will, praktikabler.

Nach Anpassung der Config starten wir MongoDB auf allen drei Servern:

# Auf jedem Server
sudo systemctl start mongod
sudo systemctl enable mongod
sudo systemctl status mongod

Der Status sollte “active (running)” zeigen. Die Logs in /var/log/mongodb/mongod.log sollten eine Zeile enthalten wie:

{"t":{"$date":"2024-01-15T10:30:00.000Z"},"s":"I","c":"REPL","id":4615611,"ctx":"initandlisten","msg":"This node is a member of a replica set","attr":{"setName":"prodReplSet","config":"(empty)"}}

Dies bestätigt, dass MongoDB weiß, dass es Teil eines Replica Sets namens “prodReplSet” sein soll, aber das Set ist noch nicht initialisiert (config ist empty).

21.3 Netzwerk-Verifizierung: Können Nodes sich sehen?

Bevor wir das Replica Set initialisieren, sollten wir verifizieren, dass die Nodes sich gegenseitig erreichen können. Von mongo1 aus:

# DNS-Auflösung prüfen
nslookup mongo2.example.com
nslookup mongo3.example.com

# Ping-Test
ping -c 3 mongo2.example.com
ping -c 3 mongo3.example.com

# MongoDB-Port-Test
telnet mongo2.example.com 27017
telnet mongo3.example.com 27017

Alle diese Tests sollten erfolgreich sein. Schlägt nslookup fehl, ist DNS-Konfiguration falsch. Schlägt ping fehl, gibt es Netzwerk- oder Firewall-Probleme. Schlägt telnet fehl, lauscht MongoDB nicht auf dem erwarteten Port oder eine Firewall blockiert Port 27017.

Typische Firewall-Probleme: Viele Cloud-Provider haben Security Groups oder Network ACLs, die standardmäßig allen Traffic blocken. MongoDB Port 27017 muss explizit geöffnet werden. Auf Ubuntu mit ufw:

# Erlaube MongoDB-Port von den anderen Nodes
sudo ufw allow from 10.0.1.12 to any port 27017
sudo ufw allow from 10.0.1.13 to any port 27017
sudo ufw status

Dies erlaubt 10.0.1.12 (mongo2) und 10.0.1.13 (mongo3) den Zugriff auf Port 27017. Für bidirektionale Kommunikation muss dies auf allen Nodes konfiguriert werden.

21.4 Initialisierung: Das Replica Set zum Leben erwecken

Mit laufenden mongod-Prozessen auf allen Nodes und verifizierter Netzwerk-Connectivity können wir das Replica Set initialisieren. Wir verbinden uns mit einem der Nodes – typischerweise dem, der Primary werden soll:

mongosh --host mongo1.example.com --port 27017

Die Shell verbindet sich erfolgreich, zeigt aber noch keinen Replica-Set-Status im Prompt, weil das Set nicht initialisiert ist. Wir initiieren es nun:

rs.initiate({
  _id: "prodReplSet",
  members: [
    { _id: 0, host: "mongo1.example.com:27017" },
    { _id: 1, host: "mongo2.example.com:27017" },
    { _id: 2, host: "mongo3.example.com:27017" }
  ]
})

Dieser Command erstellt die Replica-Set-Konfiguration mit drei Members. Die _id des Sets muss mit dem replSetName in der mongod.conf übereinstimmen. Die Member-IDs (0, 1, 2) sind willkürlich, aber konventionell startet man bei 0 und zählt hoch.

Nach erfolgreichem rs.initiate() passiert intern viel:

MongoDB propagiert die Konfiguration zu allen Members. Jeder Node speichert sie in seiner lokalen local.system.replset Collection. Die Nodes beginnen, Heartbeats zu senden – alle 2 Sekunden pingt jeder Node die anderen. Eine Election wird initiiert. Innerhalb von Sekunden wird ein Primary gewählt – typischerweise der erste Member in der Liste, aber garantiert ist das nicht.

Der Shell-Prompt ändert sich:

prodReplSet [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.

Wir verifizieren den Status:

rs.status()

Die Output ist umfangreich, aber die wichtigen Teile:

{
  set: 'prodReplSet',
  date: ISODate("2024-01-15T10:35:00.000Z"),
  myState: 1,  // 1 = PRIMARY
  members: [
    {
      _id: 0,
      name: 'mongo1.example.com:27017',
      health: 1,
      state: 1,  // PRIMARY
      stateStr: 'PRIMARY',
      uptime: 300,
      optime: { ts: Timestamp(1705315500, 1), t: Long("1") },
      optimeDate: ISODate("2024-01-15T10:35:00.000Z"),
      electionTime: Timestamp(1705315210, 1),
      electionDate: ISODate("2024-01-15T10:30:10.000Z"),
      self: true
    },
    {
      _id: 1,
      name: 'mongo2.example.com:27017',
      health: 1,
      state: 2,  // SECONDARY
      stateStr: 'SECONDARY',
      uptime: 295,
      optime: { ts: Timestamp(1705315500, 1), t: Long("1") },
      optimeDate: ISODate("2024-01-15T10:35:00.000Z"),
      syncSourceHost: 'mongo1.example.com:27017',
      lastHeartbeat: ISODate("2024-01-15T10:35:00.500Z")
    },
    {
      _id: 2,
      name: 'mongo3.example.com:27017',
      health: 1,
      state: 2,  // SECONDARY
      stateStr: 'SECONDARY',
      uptime: 295,
      optime: { ts: Timestamp(1705315500, 1), t: Long("1") },
      optimeDate: ISODate("2024-01-15T10:35:00.000Z"),
      syncSourceHost: 'mongo1.example.com:27017',
      lastHeartbeat: ISODate("2024-01-15T10:35:00.500Z")
    }
  ],
  ok: 1
}

Alle drei Members sind health: 1 (erreichbar) und haben sinnvolle States. mongo1 ist PRIMARY, die anderen zwei sind SECONDARY. Die optimeDate ist identisch oder sehr nah beieinander, was bedeutet, dass alle synchron sind – kein Replication Lag.

Das syncSourceHost-Feld bei den Secondaries zeigt, dass sie vom Primary replizieren. Dies ist normal für ein frisch initialisiertes Set. Mit Chaining könnten Secondaries später von anderen Secondaries replizieren.

21.5 Erste Operationen: Daten schreiben und replizieren sehen

Mit dem Replica Set funktionsfähig können wir erste Operationen durchführen. Wir sind noch mit dem Primary (mongo1) verbunden:

// Eine Test-Datenbank verwenden
use testdb

// Ein Dokument einfügen
db.testcollection.insertOne({
  message: "Hello from Replica Set",
  timestamp: new Date(),
  host: "mongo1"
})

// Das Dokument sollte sichtbar sein
db.testcollection.find()

Der Insert erfolgt auf dem Primary. MongoDB schreibt ihn ins Oplog, und die Secondaries replizieren ihn asynchron. Wir können dies verifizieren, indem wir uns mit einem Secondary verbinden:

# In neuem Terminal
mongosh --host mongo2.example.com --port 27017

In dieser Shell:

// Wir sind mit einem Secondary verbunden
// Der Prompt zeigt: prodReplSet [direct: secondary] test>

// Reads von Secondaries sind standardmäßig nicht erlaubt
use testdb
db.testcollection.find()
// Error: not primary and secondaryOk=false

Dieser Fehler ist erwartbar. MongoDB erlaubt standardmäßig keine Reads von Secondaries, weil sie potenziell stale Daten zurückgeben. Wir müssen explizit sagen, dass wir Reads vom Secondary akzeptieren:

db.getMongo().setReadPref("secondaryPreferred")

// Jetzt funktioniert der Read
db.testcollection.find()

Das Dokument, das wir auf dem Primary eingefügt haben, ist nun auf dem Secondary sichtbar. Die Replikation hat funktioniert – typischerweise in Millisekunden.

21.6 Failover testen: Primary-Ausfall simulieren

Der Acid-Test für jedes Replica Set ist Failover. Wir simulieren einen Primary-Ausfall und beobachten, wie das Set reagiert. Auf mongo1 (dem aktuellen Primary):

sudo systemctl stop mongod

Dies stoppt den mongod-Prozess sauber. Innerhalb von Sekunden (typischerweise 10-15 Sekunden, abhängig von electionTimeoutMillis) erkennen die verbliebenen Nodes den Ausfall und starten eine Election.

In der Shell, die mit mongo2 oder mongo3 verbunden ist, sehen wir nach kurzer Zeit:

rs.status()

Die Output zeigt, dass einer der Secondaries (mongo2 oder mongo3) nun PRIMARY ist. Der Prompt ändert sich entsprechend, wenn wir mit dem neuen Primary verbunden sind. mongo1 erscheint als state: 8 (DOWN) oder ist gar nicht mehr in der Liste, weil er nicht erreichbar ist.

Wir können weiterhin Daten schreiben:

use testdb
db.testcollection.insertOne({
  message: "Written after failover",
  timestamp: new Date(),
  host: "mongo2 or mongo3"
})

Der Write erfolgt auf dem neuen Primary. Wenn wir mongo1 wieder starten:

sudo systemctl start mongod

Tritt mongo1 dem Set als Secondary bei. Er repliziert die Operationen, die während seines Ausfalls passierten, aus dem Oplog der anderen Nodes. Er wird nicht automatisch wieder Primary – der aktuelle Primary bleibt Primary, bis er ausfällt oder explizit heruntergestuft wird.

21.7 Authentifizierung aktivieren: Sicherheit für das Set

Ein produktives Replica Set ohne Authentifizierung ist ein massives Sicherheitsrisiko. Jeder, der Netzwerk-Zugriff hat, kann verbinden und beliebige Operationen durchführen. Wir aktivieren jetzt Authentifizierung.

Der Prozess erfordert zwei Schritte: Einen Admin-User erstellen und dann Authentifizierung aktivieren. Wir erstellen den User, während Authentifizierung noch deaktiviert ist:

// Mit Primary verbunden
use admin
db.createUser({
  user: "admin",
  pwd: "SecurePassword123!",
  roles: [ { role: "root", db: "admin" } ]
})

Dieser User hat die root-Rolle, was alle Rechte gibt. Für produktive Systeme sollte man spezifischere Rollen nutzen, aber für initiales Setup ist root pragmatisch.

Als nächstes erstellen wir den KeyFile – die shared secret für Inter-Member-Authentifizierung. Auf einem der Nodes:

openssl rand -base64 756 > /etc/mongodb-keyfile
chmod 400 /etc/mongodb-keyfile
chown mongodb:mongodb /etc/mongodb-keyfile

Dieser KeyFile muss auf alle Nodes kopiert werden. Mit scp:

scp /etc/mongodb-keyfile mongo2:/etc/mongodb-keyfile
scp /etc/mongodb-keyfile mongo3:/etc/mongodb-keyfile

# Auf mongo2 und mongo3
ssh mongo2 "sudo chmod 400 /etc/mongodb-keyfile && sudo chown mongodb:mongodb /etc/mongodb-keyfile"
ssh mongo3 "sudo chmod 400 /etc/mongodb-keyfile && sudo chown mongodb:mongodb /etc/mongodb-keyfile"

Jetzt aktivieren wir Authentifizierung in der mongod.conf auf allen Nodes:

security:
  authorization: enabled
  keyFile: /etc/mongodb-keyfile

Und starten MongoDB auf allen Nodes neu:

# Auf allen Nodes
sudo systemctl restart mongod

Nach dem Restart erfordern Verbindungen Credentials:

mongosh --host mongo1.example.com --port 27017 -u admin -p "SecurePassword123!" --authenticationDatabase admin

Oder mit Connection String:

mongosh "mongodb://admin:SecurePassword123!@mongo1.example.com:27017/?authSource=admin"

Ohne Credentials schlägt die Verbindung fehl oder erlaubt nur limitierte Operationen.

21.8 Advanced Setup: Prioritäten und Tags setzen

Mit einem funktionierenden, gesicherten Replica Set können wir fortgeschrittene Konfigurationen vornehmen. Prioritäten steuern, welcher Node bevorzugt Primary wird:

var config = rs.conf()
config.members[0].priority = 2  // mongo1 bevorzugt
config.members[1].priority = 1  // mongo2 normal
config.members[2].priority = 0.5  // mongo3 weniger bevorzugt
rs.reconfig(config)

Mit dieser Konfiguration wird mongo1 bevorzugt Primary. Fällt mongo1 aus und wird später wieder gestartet, wird eine Election ausgelöst und mongo1 wird wieder Primary (weil höchste Priorität).

Tags ermöglichen geographische oder workload-basierte Selektierung:

var config = rs.conf()
config.members[0].tags = { dc: "east", ssd: "true" }
config.members[1].tags = { dc: "west", ssd: "true" }
config.members[2].tags = { dc: "east", ssd: "false" }
rs.reconfig(config)

Anwendungen können nun Reads basierend auf Tags routen:

db.users.find({...}).readPref("nearest", [{ dc: "east" }])

21.9 Monitoring und Troubleshooting

Ein produktives Replica Set benötigt kontinuierliches Monitoring. Key-Metriken sind Replication Lag, Member Health und Oplog Window. Wir können diese in der Shell abfragen:

// Replication Lag für alle Members
rs.printReplicationInfo()

// Status aller Members
rs.status().members.forEach(m => {
  print(`${m.name}: ${m.stateStr}, lag: ${m.optimeDate}`)
})

Für automatisiertes Monitoring sollte ein Tool wie Prometheus mit MongoDB Exporter eingesetzt werden. Alerts bei Replication Lag über 10 Sekunden oder Member Health unter 1 (down) sind essentiell.

Typische Probleme und ihre Lösung:

Member bleibt RECOVERING: Initial Sync schlägt fehl. Logs prüfen mit tail -f /var/log/mongodb/mongod.log. Oft sind Netzwerk-Probleme oder Disk-Space-Mangel die Ursache. Lösung: Problem beheben und Member mit rs.remove() entfernen, dann mit rs.add() neu hinzufügen.

Replication Lag steigt kontinuierlich: Der Secondary hält nicht mit. Ursache oft unterdimensionierte Hardware oder extrem hohe Write-Last. Lösung: Hardware upgraden oder Write-Pattern optimieren.

Häufige Elections: Netzwerk-Instabilität oder zu aggressives electionTimeoutMillis. Lösung: Netzwerk stabilisieren oder Timeout in config erhöhen: config.settings.electionTimeoutMillis = 15000

Die folgende Tabelle fasst die wichtigsten Verwaltungs-Commands zusammen:

Command Zweck Typischer Use-Case
rs.status() Status aller Members Health-Check, Lag-Monitoring
rs.conf() Aktuelle Konfiguration Vor Änderungen inspizieren
rs.reconfig(cfg) Konfiguration ändern Prioritäten setzen, Members hinzufügen
rs.add("host:port") Member hinzufügen Cluster erweitern
rs.remove("host:port") Member entfernen Defekten Node entfernen
rs.stepDown() Primary herunterstufen Geplante Wartung
rs.printReplicationInfo() Oplog-Info Oplog Window prüfen

Ein funktionierendes Replica Set, wie wir es hier aufgebaut haben, ist production-ready für viele Anwendungen. Die Grundlagen – drei Nodes, Authentifizierung, sinnvolle Konfiguration – sind vorhanden. Für höchste Anforderungen würde man zusätzlich TLS/SSL aktivieren, Monitoring integrieren, automatisierte Backups einrichten und möglicherweise auf geografisch verteilte Nodes erweitern. Aber das Fundament steht, und das ist der kritische erste Schritt.