19 Konfiguration eines MongoDB Replica Sets

Die Initialisierung eines Replica Sets im vorherigen Kapitel war der erste Schritt, aber die Konfiguration eines produktionsreifen Sets erfordert mehr. Prioritäten müssen gesetzt werden, um zu steuern, welcher Node Primary wird. Read Preferences müssen an Workload-Charakteristiken angepasst werden. Hidden Members für Backups oder Analytics könnten nötig sein. Die Konfiguration eines Replica Sets ist kein einmaliger Vorgang, sondern ein kontinuierlicher Prozess, der sich mit den Anforderungen entwickelt.

MongoDB speichert die Replica-Set-Konfiguration selbst in der Datenbank – in der Collection local.system.replset auf jedem Member. Diese Konfiguration ist ein Dokument, das alle Members, ihre Rollen, Prioritäten und Set-weite Einstellungen beschreibt. Änderungen an dieser Konfiguration erfolgen über spezielle Commands, die MongoDB automatisch über alle Members propagiert. Dies garantiert Konsistenz – alle Nodes haben dieselbe Konfiguration, niemand ist out-of-sync.

19.1 Das Konfigurationsdokument: Anatomie eines Replica Sets

Das Herz der Replica-Set-Konfiguration ist ein JSON-ähnliches Dokument. Wir können es jederzeit abrufen und inspizieren:

var config = rs.conf()
printjson(config)

Ein typisches Konfigurationsdokument für ein 3-Node-Set sieht so aus:

{
  _id: "myReplSet",
  version: 3,
  term: 1,
  members: [
    {
      _id: 0,
      host: "server1.example.com:27017",
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: { dc: "east", usage: "production" },
      secondaryDelaySecs: 0,
      votes: 1
    },
    {
      _id: 1,
      host: "server2.example.com:27017",
      priority: 1,
      tags: { dc: "west", usage: "production" }
    },
    {
      _id: 2,
      host: "server3.example.com:27017",
      priority: 0.5,
      tags: { dc: "east", usage: "analytics" }
    }
  ],
  settings: {
    chainingAllowed: true,
    heartbeatIntervalMillis: 2000,
    heartbeatTimeoutSecs: 10,
    electionTimeoutMillis: 10000,
    catchUpTimeoutMillis: -1,
    getLastErrorModes: {},
    getLastErrorDefaults: { w: 1, wtimeout: 0 }
  }
}

Dieses Dokument ist komplex, aber jedes Feld hat einen Zweck. Das _id-Feld ist der Replica-Set-Name – “myReplSet” in diesem Fall. Dieser Name muss mit dem --replSet-Parameter übereinstimmen, mit dem die mongod-Prozesse gestartet wurden. Stimmt er nicht überein, verweigern Nodes die Teilnahme am Set.

Die version ist ein Zähler, der bei jeder Konfigurationsänderung inkrementiert wird. MongoDB nutzt dies, um zu erkennen, welche Konfiguration aktueller ist. Wenn ein Node eine Konfiguration mit Version 5 hat und ein anderer mit Version 3, weiß MongoDB, dass Version 5 die neuere ist. Der term ist ein weiterer Zähler, der bei jeder Election inkrementiert wird – er hilft, Election-Zyklen zu tracken.

19.2 Members: Knoten mit Rollen und Eigenschaften

Das members-Array ist die Kernkomponente. Jedes Objekt repräsentiert einen Node mit seinen Eigenschaften. Die _id ist eine eindeutige numerische ID innerhalb des Sets. Sie startet typischerweise bei 0 und wird sequentiell vergeben. Diese ID ist persistent – selbst wenn ein Node offline geht und zurückkommt, behält er dieselbe ID.

Das host-Feld spezifiziert Hostname und Port. MongoDB nutzt diese Information, um Members zu kontaktieren. Wichtig: Der Hostname muss von allen Members auflösbar sein. “localhost” funktioniert nur, wenn alle Members auf demselben Host laufen (wie in unseren lokalen Tests). In Produktionsumgebungen müssen FQDNs oder IP-Adressen verwendet werden.

Die priority steuert die Wahrscheinlichkeit, Primary zu werden. Der Default ist 1.0. Ein Node mit Priorität 2 wird über einen mit Priorität 1 gewählt, wenn beide verfügbar sind. Priorität 0 bedeutet, der Node kann niemals Primary werden – ein “passive member”. Dies ist nützlich für Nodes, die nur für Backups oder Analytics gedacht sind und nie produktive Writes bedienen sollen.

Das votes-Feld bestimmt, ob ein Member bei Elections abstimmen darf. Der Default ist 1 – jeder Node hat eine Stimme. Votes können auf 0 gesetzt werden, um einen Node von Elections auszuschließen, während er trotzdem Daten repliziert. Dies ist nützlich, um die Anzahl stimmberechtigter Members ungerade zu halten. Ein Set mit 4 Daten-Nodes könnte einem 0 Votes geben, sodass effektiv 3 Stimmen existieren.

19.3 Hidden Members und Delayed Secondaries

Hidden Members sind eine mächtige Funktion für spezielle Workloads. Ein Hidden Member repliziert Daten normal, aber ist unsichtbar für Clients. Applikationen sehen ihn nicht, Treiber routen keine Queries dorthin, selbst mit readPreference: "secondary". Er existiert ausschließlich für interne Zwecke – typischerweise Backups oder Analytics.

// Member 2 als Hidden konfigurieren
var config = rs.conf()
config.members[2].hidden = true
config.members[2].priority = 0  // Hidden Members müssen Priority 0 haben
rs.reconfig(config)

Ein Hidden Member muss immer Priority 0 haben, weil er niemals Primary werden darf. Wäre er Primary, wäre er nicht mehr hidden – der Primary ist per Definition sichtbar. MongoDB erzwingt diese Regel.

Der Use-Case für Hidden Members: Ein Analytics-Team führt komplexe, ressourcenintensive Queries aus. Diese würden produktive Workloads beeinträchtigen, wenn sie auf den normalen Secondaries liefen. Ein dedizierter Hidden Member absorbiert diese Last, ohne produktive Queries zu tangieren.

Delayed Secondaries sind eine Variante – sie replizieren mit künstlicher Verzögerung. Die secondaryDelaySecs-Einstellung definiert, wie viele Sekunden der Secondary hinter dem Primary zurückliegt:

// Member 2 mit 1-Stunden-Delay
var config = rs.conf()
config.members[2].secondaryDelaySecs = 3600
config.members[2].priority = 0
config.members[2].hidden = true
rs.reconfig(config)

Ein Delayed Secondary ist automatisch hidden – es ergibt keinen Sinn, verzögerte Daten für produktive Reads zu nutzen. Der Use-Case: Schutz vor logischen Fehlern. Löscht eine Applikation versehentlich Millionen Dokumente, passiert dies sofort auf dem Primary und repliziert zu normalen Secondaries innerhalb von Millisekunden. Aber der Delayed Secondary hat die Daten noch für eine Stunde. Ein Admin kann ihn nutzen, um gelöschte Daten zu recovern, bevor sie auch dort verschwinden.

19.4 Tags und Tag Sets: Geo-Awareness und Workload-Isolation

Tags sind Key-Value-Paare, die Members annotieren. Sie dienen keinem technischen Zweck für MongoDB selbst, aber erlauben Applikationen, Members basierend auf Charakteristiken zu selektieren. Typische Use-Cases sind Geo-Awareness und Workload-Isolation.

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

Applikationen können nun Read Preferences mit Tag Sets kombinieren:

db.users.find({...}).readPref("nearest", [
  { dc: "us-east" },   // Bevorzuge us-east
  { ssd: "true" },     // Fallback: Irgendein SSD-Node
  {}                   // Final Fallback: Irgendein Node
])

Diese Query bevorzugt Members im us-east Datacenter. Sind keine verfügbar, nimmt sie irgendeinen Node mit SSD. Sind auch die nicht verfügbar, nimmt sie irgendwas. Diese Flexibilität erlaubt sophisticated Routing-Strategien.

Ein weiterer Use-Case für Tags: Write Concerns mit Geo-Awareness. Man kann verlangen, dass ein Write in mindestens zwei Datacenters bestätigt wird, bevor er als committed gilt:

// Konfiguration: Definiere "datacenters"-Constraint
var config = rs.conf()
config.settings.getLastErrorModes = {
  multiDC: { dc: 2 }  // Mindestens 2 verschiedene "dc"-Tag-Werte
}
rs.reconfig(config)

// Write mit multiDC-Concern
db.orders.insertOne(
  { customer: "Alice", total: 99.99 },
  { writeConcern: { w: "multiDC", wtimeout: 5000 } }
)

Dieser Write committed erst, wenn mindestens zwei Members mit unterschiedlichen dc-Tags ihn bestätigt haben. Dies garantiert Geo-Redundanz selbst bei einem kompletten Datacenter-Ausfall.

19.5 Election Tuning: Timeouts und Prioritäten

Elections sind normalerweise automatisch und erfordern keine Intervention. Aber manchmal ist Tuning nötig. Die settings-Sektion des Config-Dokuments steuert Election-Verhalten.

Das electionTimeoutMillis definiert, wie lange ein Node wartet, bevor er eine Election startet, wenn Heartbeats vom Primary fehlen. Der Default ist 10 Sekunden. Dies bedeutet: Fällt der Primary aus, dauert es mindestens 10 Sekunden, bis ein neuer gewählt ist. Für Anwendungen mit strengen Uptime-Anforderungen könnte man dies reduzieren:

var config = rs.conf()
config.settings.electionTimeoutMillis = 5000  // 5 Sekunden
rs.reconfig(config)

Der Trade-off: Kürzere Timeouts bedeuten schnellere Failovers, aber auch höheres Risiko für unnötige Elections bei temporären Netzwerkproblemen. Verliert der Primary für 2 Sekunden Netzwerk-Connectivity (etwa wegen eines Switch-Reboots), ist das mit 10-Sekunden-Timeout kein Problem – er bleibt Primary. Mit 5-Sekunden-Timeout könnte eine Election getriggert werden, nur um festzustellen, dass der Primary wieder da ist.

Das heartbeatIntervalMillis steuert, wie oft Members Heartbeats senden. Der Default ist 2 Sekunden. Kürzere Intervalle bedeuten schnellere Fehler-Detection, aber mehr Netzwerk-Traffic. In lokalen Setups ist 2 Sekunden fine. In Wide-Area-Networks mit langsamer Connectivity könnte man es auf 5 oder 10 Sekunden erhöhen.

Die priority-Werte in members steuern, wer Primary wird, aber es gibt Nuancen. Priorität allein garantiert nichts – sie ist ein Gewicht, kein Anspruch. Ein Node mit Priority 2 wird über einen mit Priority 1 gewählt, wenn beide Kandidaten sind. Aber “Kandidat” heißt: verfügbar, synchron (hat aktuelle Daten) und erreichbar von einer Mehrheit. Ein Node mit Priority 2, der 10 Sekunden Replication Lag hat, verliert gegen einen mit Priority 1, der synchron ist.

19.6 Chaining: Replication-Topologien

Standardmäßig replizieren Secondaries direkt vom Primary. Bei vielen Secondaries kann dies den Primary belasten – jeder Secondary zieht Oplog-Einträge, was Netzwerk-Bandwidth und CPU kostet. Chaining erlaubt es Secondaries, von anderen Secondaries zu replizieren, was den Primary entlastet.

var config = rs.conf()
config.settings.chainingAllowed = true  // Default
rs.reconfig(config)

Mit Chaining aktiv wählt MongoDB automatisch Sync-Sources. Ein Secondary könnte vom Primary replizieren, aber auch von einem anderen Secondary, der “näher” ist (geringere Latenz). Dies ergibt eine Baum-Struktur statt eines Sterns.

Der Vorteil: Skalierbarkeit. Ein Primary kann 50+ Secondaries bedienen, wenn Chaining genutzt wird. Ohne Chaining wird der Primary zum Bottleneck. Der Nachteil: Replication Lag kann sich addieren. Wenn Secondary 1 Lag von 2 Sekunden hat und Secondary 3 von ihm repliziert, hat Secondary 3 mindestens 2 Sekunden Lag, plus eigene Delays.

Chaining ist typischerweise sinnvoll, sollte aber monitored werden. Replication Lag über 10 Sekunden ist ein Warnsignal. rs.status() zeigt Sync-Sources und Lag pro Member.

19.7 Reconfiguration: Änderungen sicher durchführen

Änderungen an der Replica-Set-Konfiguration erfolgen über rs.reconfig(). Dieser Command nimmt das gesamte Config-Dokument, validiert es und propagiert es zu allen Members. Der Prozess ist atomar – entweder alle Members bekommen die neue Config, oder keine.

Ein typischer Workflow: Config abrufen, in Variable speichern, modifizieren, reconfiguren:

// Config abrufen
var config = rs.conf()

// Member hinzufügen
config.members.push({
  _id: 3,
  host: "server4.example.com:27017",
  priority: 1
})

// Increment version (automatisch, aber gut zu wissen)
config.version++

// Anwenden
rs.reconfig(config)

MongoDB inkrementiert die version automatisch bei rs.reconfig(), also manuelles Inkrement ist nicht nötig, aber schadet nicht. Nach erfolgreichem Reconfig repliziert der neue Member automatisch vom Set und holt Daten auf.

Vorsicht: Manche Änderungen erzwingen eine Election. Ändert man Prioritäten so, dass der aktuelle Primary keine höchste Priorität mehr hat, tritt er zurück und ein anderer wird gewählt. Dies bedeutet kurze Downtime für Writes (Sekunden). Solche Änderungen sollten während Wartungsfenstern erfolgen oder wenn die Applikation kurze Write-Ausfälle toleriert.

Das Entfernen eines Members ist elegant:

var config = rs.conf()
// Filter Member mit Host "server2.example.com:27017" raus
config.members = config.members.filter(m => m.host !== "server2.example.com:27017")
rs.reconfig(config)

Der entfernte Node erhält die neue Config und erkennt, dass er nicht mehr Member ist. Er stoppt Replikation und bleibt als Standalone-Instanz erreichbar. Seine Daten bleiben intakt – man könnte ihn später wieder hinzufügen.

19.8 Arbiter: Stimmrecht ohne Daten

Arbiter sind spezielle Members, die an Elections teilnehmen, aber keine Daten replizieren. Sie existieren rein, um eine ungerade Anzahl von Stimmen sicherzustellen. Ein Setup mit zwei Daten-Nodes plus Arbiter kann Elections durchführen (3 Stimmen, Mehrheit = 2), wobei der Arbiter minimal Ressourcen verbraucht.

Das Hinzufügen eines Arbiters ist explizit:

rs.addArb("server3.example.com:27017")

Alternativ manuell in der Config:

var config = rs.conf()
config.members.push({
  _id: 2,
  host: "server3.example.com:27017",
  arbiterOnly: true
})
rs.reconfig(config)

Ein Arbiter hat automatisch Priority 0 und Votes 1. Er kann niemals Primary werden (weil er keine Daten hat), aber seine Stimme zählt bei Elections. Arbiter sollten auf separater Hardware laufen – typischerweise auf einem App-Server oder Monitoring-Server, nicht auf Datenbank-Hardware.

Die Verwendung von Arbitern ist umstritten. Manche Admins meiden sie, weil ein 3-Daten-Node-Set robuster ist als 2-Daten + Arbiter. Fällt bei letzterem ein Daten-Node aus, gibt es nur noch eine Daten-Kopie – kein Redundanz mehr. Bei 3 Daten-Nodes sind nach einem Ausfall noch zwei Kopien da. Arbiter sind ein Budget-Kompromiss, kein Best-Practice.

19.9 Monitoring und Troubleshooting

Die Gesundheit eines Replica Sets zu überwachen ist kritisch. rs.status() ist die primäre Anlaufstelle:

rs.status()

Wichtige Felder in der Output:

Das myState-Feld des eigenen Members zeigt dessen Zustand: 1 = PRIMARY, 2 = SECONDARY, 7 = ARBITER, 8 = DOWN. Für jeden anderen Member zeigt das stateStr-Feld den Zustand als String.

Das optimeDate-Feld zeigt den Zeitstempel der letzten replizierten Operation. Vergleich zwischen Members offenbart Replication Lag. Liegt ein Secondary 10 Sekunden zurück, ist sein optimeDate 10 Sekunden älter als das des Primary.

Das syncSourceHost-Feld zeigt, von wo der Member repliziert. Normalerweise vom Primary, aber bei Chaining könnte es ein Secondary sein.

Das health-Feld ist 1 wenn erreichbar, 0 wenn down. Ein Member mit health: 0 ist aus Sicht dieses Monitors nicht erreichbar – könnte tot sein oder Netzwerk-isoliert.

Typische Probleme und ihre Diagnose:

Replication Lag über 10 Sekunden: Der Secondary hält nicht mit. Mögliche Ursachen: Unterdimensionierte Hardware, langsames Netzwerk, komplexe Writes die schwer zu replizieren sind. Lösung: Hardware upgraden, Netzwerk verbessern, oder Write-Patterns optimieren.

Häufige Elections: Das Set wählt ständig neue Primaries. Ursache: Instabile Netzwerk-Connectivity oder zu aggressives electionTimeoutMillis. Lösung: Netzwerk stabilisieren oder Timeout erhöhen.

Member bleibt RECOVERING: Ein Member kommt nicht zu SECONDARY-Status. Ursache: Initial Sync schlägt fehl, oder verpasste Oplog-Einträge. Lösung: Logs prüfen für spezifische Fehler. Oft muss der Member neu initialisiert werden mit rs.remove() und rs.add().

Die Logs in /var/log/mongodb/ sind essentiell für tiefes Troubleshooting. Grep für “election”, “replication”, “oplog” liefert relevante Einträge:

grep -i "election" /var/log/mongodb/mongod.log | tail -20
grep -i "replication lag" /var/log/mongodb/mongod.log

Replica-Set-Konfiguration ist kein “set it and forget it”. Anforderungen ändern sich, Hardware wird ausgetauscht, Netzwerk-Topologien evolvieren. Regelmäßiges Review der Konfiguration – Prioritäten, Tags, Timeouts – stellt sicher, dass das Set optimal für die aktuelle Workload konfiguriert ist. Monitoring ist nicht optional, sondern essentiell, um Probleme zu erkennen, bevor sie kritisch werden.