Ausfallsicheres MongoDB-Replica-Set in Ubuntu 20 erstellen
Heyho! Du willst also freiwillig das Abenteuer wagen, MongoDB ausfallsicher auf mehreren Servern zu installieren? Hut ab! Hier erkläre ich dir, wie das gehen könnte. Wie immer gilt: Niemand ist perfekt und du solltest deine eigenen Recherchen machen!
Diese Anleitung bezieht sich auf Version 4.4 (https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/). Es empfielt sich, immer eine spezifische Version zu installieren – sonst beschwert sich dein Nachbar und schellt mitten in der Nacht.
Achte auch darauf, bei der Installation das File-System XFS zu nutzen. Dies ist für die Nutzung von Journaling (wichtig bei Replication) empfohlen.
Also legen wir los! Zunächst installierst du die nötige Library, inklusive allem, was du so brauchst.
wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | sudo apt-key add - echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.4.list sudo apt update sudo apt-get install -y mongodb-org=4.4.0 mongodb-org-server=4.4.0 mongodb-org-shell=4.4.0 mongodb-org-mongos=4.4.0 mongodb-org-tools=4.4.0
SSL Zertifikat erstellen
Als nächstes kannst du (solltest du) SSL-Zertifikate erstellen. Das erlaubt dir eine verschlüsselte Kommunikation zwischen dir und den Servern. Das ist natürlich nicht nötig, wenn du ein eigenes Netzwerk hast, auf das keiner zugreift (oder VPN getunnelt). Kann aber nicht schaden.
Zunächst auf der ersten Maschine ein Arbeitsverzeichnis erstellen:
cd mkdir mongo-ssl cd mongo-ssl
Dann erstellst du das CA-Zertifikat, das für alle Server gilt; deshalb musst du es auch nur einmal machen. Die resultierende Datei mongo-ca.pem
sollte dann auch in allen Replikationen, bzw. Servern, benutzt werden. Die Eingabe des “Common-Name” ist egal!
openssl genrsa -out mongo-ca.key 2048 openssl req -x509 -new -nodes -key mongo-ca.key -sha256 -days 3650 -out mongo-ca.pem
Als nächstes erstellst du das Client-File für einen Server. Diese musst du für jeden Server erstellen. Die Datei mongo-server.pem
, die erzeugt wird, wird später in den Mongo-Einstellungen benutzt. Achtung: Der Common-Name muss die IP des Servers sein!
openssl genrsa -out mongo-server.key 2048 openssl req -new -key mongo-server.key -out mongo-server.csr openssl x509 -req -in mongo-server.csr -CA mongo-ca.pem -CAkey mongo-ca.key -CAcreateserial -out mongo-server.crt -days 3650 -sha256 cat mongo-server.key mongo-server.crt > mongo-server.pem
Als letztes kommt das Client-File für Anwendungen, z.B. um sich lokal mit dem Server zu verbinden oder über eine externe Applikation. Dazu benutzt du die Datei mongo-client.pem
. Der Common-Name ist egal. Um diese Datei zu erstellen, benötigst du die Dateien mongo-ca.pem
und mongo-ca.key
.
openssl genrsa -out mongo-client.key 2048 openssl req -new -key mongo-client.key -out mongo-client.csr openssl x509 -req -in mongo-client.csr -CA mongo-ca.pem -CAkey mongo-ca.key -CAcreateserial -out mongo-client.crt -days 3650 -sha256 cat mongo-client.key mongo-client.crt > mongo-client.pem
Anschließend kopierst du die Keys in ein eigenes Verzeichnis. Aus Gründen der Einfachheit kopieren wir hier alle (um die Verbindung später zu testen), aber auf einem Produktions-Server benötigst du nur die Dateien mongo-server.pem
und mongo-ca.pem
.
sudo mkdir /var/ssl sudo mkdir /var/ssl/mongo sudo cp * /var/ssl/mongo sudo chmod 777 /var/ssl/mongo/*.pem
Server absichern
MongoDB-Replikations-Server kommunzieren ohne dein Zutun unverschlüsselt miteinander, und das ist natürlich superdoof. Damit das nicht so ist, generierst du ein zusätzliches Keyfile, das du bei jeder Replikation verwendest (praktisch wie ein langes Passwort) (Quelle). Das wird so generiert:
cd # Keyfile erstellen openssl rand -base64 756 > mongo-keyfile # Verzeichnis erstellen sudo mkdir /opt/mongo # Keyfile verschieben sudo mv ~/mongo-keyfile /opt/mongo sudo chmod 400 /opt/mongo/mongo-keyfile # Ownership ändern sudo chown mongodb:mongodb /opt/mongo/mongo-keyfile
Achtung: Da wir später noch “authorization: enabled” einstellen (d.h. Anmeldung auf die MongoDB soll nur mit Passwort möglich sein), müssen Keyfiles eingestellt sein, sonst können die Server nicht miteinander kommunizieren. Dieser Schritt ist also nicht optional!
Ports freigeben
Darf nicht vergessen werden: Schalte Port 27017 frei. Dieser dient sowohl zur Kommunikation mit der Datenbank als auch zur Replikation der Server untereinander.
Config bearbeiten
Es geht ans Eingemachte! Bearbeite die Mongo-Config: sudo mcedit /etc/mongod.conf
net: port: 27017 bindIp: 0.0.0.0 tls: mode: preferTLS certificateKeyFile: /var/ssl/mongo/mongo-server.pem CAFile: /var/ssl/mongo/mongo-ca.pem allowInvalidCertificates: true security: # Die untenstehende Zeile noch auskommentiert lassen #authorization: enabled keyFile: /opt/mongo/mongo-keyfile replication: replSetName: "rs0"
Achtung: Unter bindIp
solltest du statt 0.0.0.0 alle Server, die Zugriff haben sollen, direkt angeben. In diesem Fall sorgt die MongoDB-Firewall später dafür, dass kein anderer Server Zugriff hat.
Die Zeile authorization
lässt du noch kommentiert, die aktivieren wir später, wenn alles läuft.
Jetzt den Service beim Startup enablen, dann starten und testen:
sudo systemctl enable mongod sudo systemctl start mongod sudo systemctl status mongod
Damit hast du MongoDB auch schon gestartet – als 1-Server-Replikation. Nun sollte der Angstschweiß ausbrechen, denn du verbindest dich jetzt lokal. Das geht noch einfach und ohne Passwort, da wir oben authentication
noch nicht enabled haben.
mongo
Wenn du keinen Fehler hast und in der Mongo-Console bist, beende diese mit exit
und versuche eine Verbindung per TLS aufzubauen:
mongo <IP SERVER> --tls --tlsCAFile /var/ssl/mongo/mongo-ca.pem --tlsCertificateKeyFile /home/ubuntu/mongo-ssl/mongo-client.pem
Hat alles geklappt? Wenn ja, super! Wenn nein, oh oh. Der einzige Tipp, den ich habe: wiederhole die Installation auf einer frischen Maschine und bete heftiger.
Da aber sicherlich alles geklappt hat, startest du die Mongo-Console udn gibst folgenden Befehl zum Starten der Replikation ein:
rs.initiate() exit
Jetzt ist deine 1-Server-Replikation wirklich gestartet.
User-Zugang einrichten
MongoDBs sind von Geburt an offen zugänglich und von jedermann zu benutzen. Das führt dazu, dass etliche Instanzen im Internet frei zugänglich sind. Suboptimal – und das will keiner haben. Neben einer Sicherung über die Firewall solltest du jetzt also auch einen Admin-User einrichten.
# Mongo-Shell starten mongo # Befehle zum Eintragen von adminUser use admin; db.createUser({ user: "adminUser", pwd:"<PASSWORD>", roles: [ { role: "userAdminAnyDatabase", db: "admin" }, { role: "dbAdminAnyDatabase", db: "admin" }, { role: "readWriteAnyDatabase", db:"admin" }, { role: "clusterAdmin", db: "admin" } ] }); exit # Verbinden mit mongo -u adminUser -p --authenticationDatabase admin
Quelle: https://medium.com/mongoaudit/how-to-enable-authentication-on-mongodb-b9e8a924efac
Anschließend den obigen authorization
Kommentar (/etc/mongod.conf
) entfernen. Damit kann sich niemand mehr auf den Cluster ohne Passwort einloggen.
Normalen User hinzufügen
Du möchtest bestimmt nicht den AdminUser nutzen, wenn der Webserver sich einloggen soll. Um einen normalen User anzulegen, benutze die folgenden Befehle; die Autorisierungsdatenbank ist admin
.
use admin; db.createUser({ user: "<BENUTZER>", pwd:"<PASSWORD>", roles: [ { role: "readWrite", db:"<DATENBANK>" } ] });
Um alle Benutzer anzuzeigen, nutzt du diesen Befehl:
use admin; db.getUsers();
Server klonen und hinzufügen
Um eine richtige Replikation aufzubauen, bedarf es mehr als einen Server. Mindestens benötigst du drei. Je mehr Server du hast, desto mehr dürfen ausfallen – niemals mehr oder genausoviel als die Hälfte.
3 Server: 1 darf ausfallen
4 Server: 1 darf ausfallen (macht also keinen Sinn)
5 Server: 2 dürfen ausfallen
Der einfachste Weg ist es, diesen Server zu klonen und den Mongo-Daemon zu starten.
sudo systemctl start mongod
Anschließend fügst du den neuen Server auf dem “Primary” (dein erster Server) als “Secondary” hinzu: Dazu auf Primary einloggen und folgende Befehle eintragen.
rs.add( { host: "<IP Server 2>:27017", priority: 2, votes: 1 } )
Die kompletten Options findest du hier.
An dieser Stelle ist es wichtig zu verstehen, wie das Voting bei MongoDB funktioniert: Fällt ein Primary aus (von dem es immer nur einen gibt), wird unter den verbliebenen Servern entschieden, wer der Neue wird. Das passiert aber nur, wenn die Mehrheit der Server im Cluster noch online ist! D.h. bei einem Cluster mit drei Servern darf nur einer ausfallen. Genauso verhält es sich bei vier Servern; deshalb macht ein Cluster am meisten in ungeraden Zahlen Sinn.
Fällt also bei drei Servern einer aus, entscheiden die beiden anderen (sofern voting
auf 1 gestellt ist), wer als nächster die Krone auf hat. Das geht dann nach priority
. Ist der alte Primary wieder online, ändert sich aber nichts mehr.
Der Vollständigkeit halber: Hast du nur zwei Server zur Verfügung, kannst du einen Arbiter erstellen. Das ist im Prinzip nichts anderes als eine MongoDB-Instanz, die nur abstimmen darf. Bei zwei Servern hat also einer noch einen Arbiter – fällt aber dieser aus, ist der ganze Cluster down.
Replikation prüfen
Hast du mindestens zwei Server am Start und diese dem Cluster hinzugefügt? Dann prüfen den Status der Replication:
rs.status()
Das Ergebnis sollte so aussehen:
{ "set": "rs0", "date": ISODate("2020-08-14T08:30:17.505Z"), "myState": NumberInt("1"), "term": NumberLong("1"), "syncSourceHost": "", "syncSourceId": NumberInt("-1"), "heartbeatIntervalMillis": NumberLong("2000"), "majorityVoteCount": NumberInt("1"), "writeMajorityCount": NumberInt("1"), "votingMembersCount": NumberInt("1"), "writableVotingMembersCount": NumberInt("1"), "optimes": { "lastCommittedOpTime": { "ts": Timestamp(1597393813, 1), "t": NumberLong("1") }, "lastCommittedWallTime": ISODate("2020-08-14T08:30:13.499Z"), "readConcernMajorityOpTime": { "ts": Timestamp(1597393813, 1), "t": NumberLong("1") }, "readConcernMajorityWallTime": ISODate("2020-08-14T08:30:13.499Z"), "appliedOpTime": { "ts": Timestamp(1597393813, 1), "t": NumberLong("1") }, "durableOpTime": { "ts": Timestamp(1597393813, 1), "t": NumberLong("1") }, "lastAppliedWallTime": ISODate("2020-08-14T08:30:13.499Z"), "lastDurableWallTime": ISODate("2020-08-14T08:30:13.499Z") }, "lastStableRecoveryTimestamp": Timestamp(1597393753, 1), "electionCandidateMetrics": { "lastElectionReason": "electionTimeout", "lastElectionDate": ISODate("2020-08-14T07:01:13.309Z"), "electionTerm": NumberLong("1"), "lastCommittedOpTimeAtElection": { "ts": Timestamp(0, 0), "t": NumberLong("-1") }, "lastSeenOpTimeAtElection": { "ts": Timestamp(1597388473, 1), "t": NumberLong("-1") }, "numVotesNeeded": NumberInt("1"), "priorityAtElection": 1, "electionTimeoutMillis": NumberLong("10000"), "newTermStartDate": ISODate("2020-08-14T07:01:13.335Z"), "wMajorityWriteAvailabilityDate": ISODate("2020-08-14T07:01:13.357Z") }, "members": [ { "_id": NumberInt("0"), "name": "<IP1>:27017", "health": 1, "state": NumberInt("1"), "stateStr": "PRIMARY", "uptime": NumberInt("5488"), "optime": { "ts": Timestamp(1597393813, 1), "t": NumberLong("1") }, "optimeDate": ISODate("2020-08-14T08:30:13.000Z"), "syncSourceHost": "", "syncSourceId": NumberInt("-1"), "infoMessage": "", "electionTime": Timestamp(1597388473, 2), "electionDate": ISODate("2020-08-14T07:01:13.000Z"), "configVersion": NumberInt("2"), "configTerm": NumberInt("1"), "self": true, "lastHeartbeatMessage": "" }, { "_id": NumberInt("1"), "name": "<IP2>:27017", "health": 1, "state": NumberInt("2"), "stateStr": "SECONDARY", "uptime": NumberInt("131"), "optime": { "ts": Timestamp(1597393813, 1), "t": NumberLong("1") }, "optimeDurable": { "ts": Timestamp(1597393813, 1), "t": NumberLong("1") }, "optimeDate": ISODate("2020-08-14T08:30:13.000Z"), "optimeDurableDate": ISODate("2020-08-14T08:30:13.000Z"), "lastHeartbeat": ISODate("2020-08-14T08:30:15.545Z"), "lastHeartbeatRecv": ISODate("2020-08-14T08:30:17.067Z"), "pingMs": NumberLong("0"), "lastHeartbeatMessage": "", "syncSourceHost": "<IP1>:27017", "syncSourceId": NumberInt("0"), "infoMessage": "", "configVersion": NumberInt("2"), "configTerm": NumberInt("1") } ], "ok": 1, "$clusterTime": { "clusterTime": Timestamp(1597393813, 1), "signature": { "hash": BinData(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAA="), "keyId": NumberLong("0") } }, "operationTime": Timestamp(1597393813, 1) }
Wenn du soweit bist, ist es Zeit, den Cluster zu testen. Was passiert, wenn ein paar Server ausfallen? Ausprobieren!
Was passiert, wenn man Server 1 (Primary) abschaltet?
S2 und S3 bleiben zunächst Secondaries, nach ein paar Sekunden wird dann der Server mit der höchsten Priority gewählt. Wenn man Server 1 wieder anmacht, bleibt er Secondary.
Was passiert, wenn man Server 3 (Secondary) abschaltet?
Dann wird rs.status()
bei Member 3 einen Fehler melden. Die anderen Server laufen aber ohne Probleme weiter. Wird Server 3 wieder aktiviert, synchronisiert er sich automatisch.
Was passiert, wenn man mehr als die Hälfte der Server ausschaltet?
Dann gibt es keine Mehrheit im Voting mehr, d.h. der Primary wird zum Secondary und es ist kein Schreiben in die Datenbank mehr möglich. Grundsätzlich gilt: Wenn es keinen Primary gibt, ist das System nicht mehr als Replication Set ansprechbar. Um mit dem letzten Server zu kommunizieren, muss man auf diesen direkt per SSH connecten. Was hilft: einen zweiten Server wieder aktivieren oder den Server als Einzelinstanz neu starten.
Was passiert, wenn man ein falsches SSL-Zertifikat nutzt?
Ändern wir doch einfach mal das Zertifikat von einem Server (sudo mcedit /var/ssl/mongo/mongo-server.pem)
. Schreibe hier ein paar Zeichen rein, die das Zertifikat ungültig machen. Anschließend die MongoDB neu starten (sudo systemctl restart mongod
).
Ergebnis: rs.status() gibt einen “unhealthy” Server aus. Das sieht man auch, wenn man die Log-Dateien auf dem betroffenen Server anschaut:
Andere Wege, neue Nodes hinzuzufügen
Der Replikationsstatus und auch die Administrations-User sind alle im Data-Verzeichnis (/var/lib/mongodb
) gespeichert. Löscht man dieses komplett (Service vorher beenden), ist der Server im “Null-Modus” und komplett resetted. Um einen neuen Server hinzuzufügen, kann man ganz einfach das Data-Dir kopieren; dann synchronisiert sich der Server schneller.
Ein paar Tipps
Hier ein paar Links für dich, um dich tiefer mit der Materie zu beschäftigen:
31. Oktober 2020
Alle meine Artikel entstehen mit bestem Wissen und Gewissen, sind aber nicht perfekt und sollten immer nur als Ausgangspunkt für deine eigenen Recherchen bilden.
Sollte dir etwas Fehlerhaftes auffallen, freue ich mich über deine Nachricht!