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.
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-orgNach 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.
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-keyfileDie 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 mongodDer 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).
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 27017Alle 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 statusDies 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.
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 27017Die 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.
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 27017In 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=falseDieser 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.
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 mongodDies 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 mongodTritt 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.
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-keyfileDieser 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-keyfileUnd starten MongoDB auf allen Nodes neu:
# Auf allen Nodes
sudo systemctl restart mongodNach dem Restart erfordern Verbindungen Credentials:
mongosh --host mongo1.example.com --port 27017 -u admin -p "SecurePassword123!" --authenticationDatabase adminOder 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.
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" }])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.