MySQL Master Master Synchronisation einrichten
Wenn ein Server der Last der Web-Anfragen nicht mehr Herr wird, oder man eine erhöhte Ausfallsicherheit benötigt, kommt man um die Dopplung der Datenbank nicht mehr herum.
Als Open-Source Fan hat man dazu mit MySQL eine Datenbank an der Hand die von Haus aus dafür schon einiges mitbringt, aber ohne eine gewisse Konfigurationsorgie geht es bei OpenSource eben meist nicht. Mit dieser Anleitung versuche ich diesen nervigen Stress zu minimieren und mir selbst eine Gedächtnisstütze zu schaffen, wenn ich das Ganze nochmals machen muss oder die Konfiguration updaten muss.
Diese Anleitung ist nichts für Anfänger und Einsteiger, da man seine Datenbanken bei diesen Experimenten sehr schnell kaputt machen kann.
Ziel:
Zwei Server sollen sowohl von den Webservern als auch von den Datenbanken synchron laufen, damit im Falle eines Ausfalls oder einer Lastspitze zwischen beiden Servern hin und her geschaltet werden kann, ohne das korrupte Datensätze entstehen. Es muss also parallel schreibend und lesend auf beide DBs zugegriffen werden können und die Datensätze müssen dabei gleichzeitig synchronisiert werden. Die gebräuchlichen Buzz-Words hierfür sind Datenbank-Cluster, Hochverfügbarkeitslösung und Ausfallsicherheit.
Um dies zu gewährleisten ist man mit einer Master-Master Synchronistation am Besten bedient.
Aufbau
Es existieren 2 Server:
DB1 oder Master1: Der erste Server, der seit längerem läuft und produktiv ist.
DB2 oder Master2: Der zweite neu gemietete Server mit einer leeren MySQL Datenbank.
DB1 und DB2 werden in der /etc/hosts Datei mit Namen und IP hinterlegt, damit später mit Namen anstatt IPs gearbeitet werden kann. Dadurch muss bei einem Austausch der Server weniger geändert werden.
Prinzip und Hintergrundwissen
MySQL kann keine direkte Master-Master Replikation. Im folgenden werden deshalb im Folgenden zwei Master-Slave Replikationen eingerichtet. Der Master des einen ist der Slave des anderen und vis versa.
Der zentrale Punkt dabei ist es Konflikte bei Primärschlüsseln um jeden Preis zu vermeiden.
MySQL zählt Primärschlüssel beim erstellen automatisch um den Wert eins hoch. Wenn einer der Server aus dem Takt gerät, weil die Sync-Verbindung für eine gewisse Zeit getrennt wird kann es sonst zu sehr unschönen und arbeitsintensiven Split Brain Situationen kommen. Um diese Situation von vornherein zu vermeiden wird die MySQL Konfiguration so angepasst, das DB1 nur gerade Primärschlüssel vergibt und DB2 nur ungerade. Falls jemand auf die (blöde) Idee gekommen ist Rechnungsnummern, Artikelnummern o.ä. als Primärschlüssel zu verwenden ist das natürlich ein Problem, da eine fortlaufende Nummerierung nicht mehr gewährleistet ist. CMS Systeme wie Typo3 oder WordPress haben damit kein Problem.
SSH-User erstellen
Der Replikations-User muss natürlich auf beiden Systemen vorhanden sein und muss von beiden Systemen vorautorisiert auf das jeweils andere System zugreifen können.
DB1:
adduser replicator su replicator ssh-keygen -i rsa -b 2048 ssh-copy-id -i ~/.ssh/id_rsa.pub replicator@DB2
DB2:
adduser replicator su replicator ssh-keygen -i rsa -b 2048 ssh-copy-id -i ~/.ssh/id_rsa.pub replicator@DB1
DB1 MySQL Vollbackup erstellen und auf Master2 einspielen
Als erstes muss mittels mysqldump ein Vollbackup der Daten von DB1 gemacht werden.
mysqldump -p --master-data --all-databases –result-file=DB1_fulldump.sql
Dieses Vollbackup muss nun auf DB2 kopiert und eingelesen werden.
mysql -u root -p source DB1_fulldump.sql;
Achtung! Da es sich um ein Vollbackup handelt wird auch die Benutzerdatenbank-Tabelle überschrieben.
Aus meiner Sicht hat das Verfahren den Vorteil, dass die DB direkt verwendet werden kann und ich mich nicht händisch darum kümmern muss, welcher User auf welche Tabelle mit welchen Rechten zugreifen durfte. Natürlich kann man auch jede Tabelle einzeln ex- und importieren und die Rechte anpassen. Man muss sich aber bewusst sein, dass dies dann bei jeder neuen Tabelle auf allen Cluster Servern von Hand gemacht werden muss. This is where the fun ends.
SSH Tunnel für Replikation einrichten
MySQL kommuniziert normal direkt über IPs und Port 3306 mit dem anderen Server. Diese Kommunikation findest normalerweise in eigenen Serverräumen statt und ist dadurch nach außen nicht sichtbar. Wenn das Hosting allerdings auf gemieteten Servern bei möglichst verschiedenen Providern statt findet ist die Klartext-Kommunikation von MySQL ein Problem, da alle Daten unverschlüsselt durch das Internet geschickt werden.
Um dieses Problem zu beheben gibt es im Grunde nur zwei Möglichkeiten: IPSec-VPN oder einen SSH-Tunnel (SSH-Port-Forwarding).
Ich habe dem SSH-Tunnel den Vorzug gegeben, weil diese Lösung im Vergleich zu VPN wesentlich leichter und schneller funktionsfähig ist.
Das Prinzip des SSH-Tunnels ist schnell erklärt. Man öffnet einen lokalen(!) Port auf DB1, SSH tunnelt diesen Port über das Netzwerk und gibt die Daten am anderen ende des Tunnels auf DB2 unter dem Zielport aus.
Eine normale SSH-Tunnel Syntax spiegelt auch genau das wieder:
ssh replicator@DB2 -L 3307:127.0.0.1:3306
Allerdings wird im aktuellen Fall auch der Rückkanal benötigt. Das kann man mit einem zweiten SSH Tunnel von DB2 nach DB1 realisieren, oder man benutzt einfach die eingebaute -R(everse) Option von SSH. Das hat den Vorteil, dass nur eine Seite überwacht und Aufgebaut werden muss.
Die komplette Syntax auf DB1:
ssh replicator@DB2 -L 3307:127.0.0.1:3306 -R 3307:127.0.0.1:3306
Ob der Tunnel funktionsfähig ist lässt sich sehr einfach testen. Man ruft auf einem DB-Server die MySQL-Shell mit dem Port 3307 auf.
mysql -u root -P 3307 -p
Im Prinzip reicht es, wenn „Enter password:“ angezeigt wird. Bekommt man diese Verbindung, steht der Tunnel und wir können (fast) mit der Replikation beginnen.
Automatierter Aufbau des SSH Tunnels mittels Crontab
Crontab vs. Inittab
Ich wurde jetzt schon mehrfach gefragt, warum Crontab und nicht Inittab.
(Für alle Linux Nutzer ist Inittab ein fester Begriff, für alle Anderen:) Inittab ist ein Systemdienst, der selbständig prüft, ob ein Dienst läuft. Falls er nicht mehr läuft startet Inittab den Dienst direkt neu. Genau da liegt das Problem: Wenn jetzt einer der Server neu gestartet wird, bricht die Verbindung und damit der Tunnel erwartungsgemäß zusammen. Inittab versucht sofort und permanent den Dienst neu zu starten. So weit, so gut, ABER inzwischen haben fast alle Rechenzentren Firewalls, Intrusion Prevention Systems (IPS) usw. etabliert. Wenn nun die Inittab im Sekundentakt versucht einen SSH-Dienst neu zu starten, vermuten IPS einen SSH-Angiff und blocken die Verbindung für einen unbekannten Zeitraum (von Minuten bis Tagen). Sehr schlecht, zumal die DBs die sofortige Wiederherstellung gar nicht benötigen (siehe oben).
Langer Rede, kurzer Sinn -> Crontab
Autossh
Damit der SSH-Tunnel nicht alle paar Minuten überprüft werden muss gibt es „autossh“
Ein kurzer Auszug aus der Feature-Liste: * Automatically restart SSH sessions and tunnels. * autossh is a program to start a copy of ssh and monitor it, restarting it as necessary should it die or stop passing traffic. The idea is from rstunnel (Reliable SSH Tunnel). Backs off on rate of connection attempts when experiencing rapid failures such as connection refused.
Autossh deckt damit genau unsere Anwendungsfälle ab.
Damit Autossh aus der Crontab sauber gestartet werden wird ein kurzes wrapper-script benötigt um sicherzustellen, dass autossh gestartet und ggf restartet wird.
autossh.sh
#!/bin/sh tunnelsite=DB2 if ! screen -ls | grep -F .$tunnelsite >/dev/null; then screen -d -m -S $tunnelsite autossh -n -N -T $tunnelsite -L 3307:127.0.0.1:3306 -R 3307:127.0.0.1:3306 fi
Dieses Script muss jetzt in der Crontab des Replikations-Users eingetragen werden.
Crontab -e
# m h dom mon dow command */15 * * * * /bin/bash /home/replicator/autossh.sh
DB Replikations User anlegen
Auf beiden Servern wird ein Replications User benötigt, der Daten vom jeweils anderen Server abholen darf.
Auf Master1 in der MySQL-Shell:
grant replication slave on *.* to replicator@'127.0.0.1' identified by 'a-secret-password';
Auf Master2 in der MySQL-Shell:
grant replication slave on *.* to replicator@'127.0.0.1' identified by 'a-secret-password';
Achtung! Immer IP-Adresse statt „localhost“ verwenden.
Anpassen der MySQL-Server-Konfigurationen
Anpassen der MySQL-Server-Konfigurationen
Achtung! Ich zeige nur die für die Anpassung wichtigen Zeilen der my.cnf!
Auf beiden Servern als root:
vi /etc/mysql/my.cnf
DB1 | DB2 |
server-id = 1 | server-id = 2 |
expire_logs_days = 21 | expire_logs_days = 21 |
max_binlog_size = 500M | max_binlog_size = 500M |
replicate-same-server-id = 0 | replicate-same-server-id = 0 |
auto-increment-increment = 2 | auto-increment-increment = 2 |
auto-increment-offset = 2 | auto-increment-offset = 1 |
Danach muss man beide MySQL Server neu starten und auf beiden den Slave in der MySQL-Shell starten:
change master to master_host='127.0.0.1', master_user='replicator', master_password='a-secret-password';
Test der Konfiguration
Die relevanten Kommandos sind:
show master status\g show slave status\g start slave;
Fertig!
Security und System Hardening
Zu guter Letzt noch ein Security Tipp: Damit man DB2 bzw. DB1 nicht mittels eines vorauthentifizieren Logins von Server zu Server springen kann, empfiehlt es sich den Shell zugang abzustellen und nur ein einfaches Port-Forwarding zu ermöglichen. Die hierzu nötigen Restriktionen kann man direkt in .ssh/authorized_keys eintragen:
Als root oder replicator-User:
vi /home/ replicator /.ssh/authorized_keys
command=“/bin/false“,no-agent-forwarding,no-X11-forwarding,no-pty ssh-rsa AAAA...
Hier steht: „Falls jemand auf die (blöde) Idee gekommen ist Rechnungsnummern, Artikelnummern o.ä. als Primärschlüssel zu verwenden ist das natürlich ein Problem, da eine fortlaufende Nummerierung nicht mehr gewährleistet ist. CMS Systeme wie Typo3 oder WordPress haben damit kein Problem.“
Was ist mit der Typo3-Tabelle fe_sessions und fe_session_data?
Das Sessionmanagement basiert soweit ich noch weiß auf generierten Session-IDs und diese sind eindeutig.
Die oben beschriebene Lösung funktioniert bei uns seit 2014 problemlos. Daher wage ich die Aussage das es einfach funktioniert. 😉
Ich hatte etwas Probleme, den Remote MySQL Server via SSH zu erreichen. Es lag daran, dass der mysql Client (mysql Ver 14.14 Distrib 5.5.49) per Default bei Localhost Verbindungen die Socket Verbindung nutzt. In der my.cnf, Abschnitt [client] zusätzlich den Parameter ‚protocol = TCP‘ löste das Problem:
[client]
port = 3306
socket = /var/run/mysqld/mysqld.sock
protocol = TCP
Gruß
Klaus
Ich habe das Problem, dass mein MariaDB-Server die my.cnf anscheinend nicht zur kentnis nimmt und keine server id setzt…