MySQL-Replikation

Dieser Artikel ist der zweite Teil einer kleinen Gruppe von Artikeln über MySQL-Backups. Die weiteren Artikel werden hier verlinkt, sobald sie erscheinen:

Hier ist der erste Artikel: MySQL-Backups, aber wie?

Hier ist der dritte Artikel: Hinter der MySQL-Replikation aufräumen

Hier ist der vierte und letzte Artikel: MySQL mit daemontools

Wie im ersten Teil erläutert, nutzen wir gerne eine zweite MySQL-Instanz mit Replikation (einen sogenannten Slave), um MySQL-Backups zu realisieren. In diesem Teil geht es um die Einrichtung dieser Replikation.

Vorab ein paar Worte zu unserem MySQL-Setup:

  • Wir verwenden fast immer die Einstellungsoption skip-networking mit der die gesamte Erreichbarkeit des MySQL-Datenbanksystems über das Netzwerk abgeschaltet wird, inklusive der Möglichkeit das Datenbanksystem über das Loopback-Interface des Localhost anzusprechen.
  • Wenn die Verbindung über das Loopback-Interface aber benötigt wird, verwenden wir meist bind-address = 127.0.0.1, womit die Netzwerk-Erreichbarkeit auf die Loopback-Verbindung beschränkt wird.
  • Wir betreiben den Slave häufig auf demselben Server auf welchem auch der Master läuft. Die zusätzliche Last hält sich in Grenzen und es gibt weniger potentielle Fehlerquellen. Prinzipiell kann ein Slave aber natürlich auch auf einer anderen Maschine laufen.
  • Egal wo der Slave nun läuft, in jedem Fall muß man leider auf die Option skip-networking verzichten. Replikation kann leider nicht über Sockets erfolgen, sondern muß zwingend durch den TCP/IP Stack gehen. Bei Slaves auf demselben Server wie dem Master nutzen wir daher gezwungenermaßen bind-address = 127.0.0.1.

Nun zur Umsetzung:

Zunächst wird die /etc/my.cnf einfach kopiert und als /etc/my-slave.cnf gespeichert. In dieser Datei müssen dann einige Optionen angepaßt werden, die dafür sorgen daß die neue MySQL-Instanz (im weiteren Slave genannt) auf einen anderen Datenbestand zugreift und ein anderes Socket bereitstellt. Das wesentliche in Auszügen:

[mysqld]
port = 3307
socket = /var/lib/mysql-slave/mysql.sock
datadir = /var/lib/mysql-slave
server-id = 2

Auf den folgenden Teil kann man verzichten, wenn man MySQL nicht per init-Skript startet sondern mit den daemontools (so machen wir es fast immer):


[mysqld_safe]
err-log=/var/log/mysql/mysql-slave.log
pid-file=/var/run/mysql/mysq-slave.pid

Verzichten heißt in diesem Fall ggf. auskommentieren. Die ursprüngliche Form sollte man auf keinen Fall stehen lassen.

In einzelnen Punkten kann das Setup mal etwas abweichen. Wir versuchen uns meist an der Default-my.cnf des System zu orientieren, dementsprechend können sich die Pfade leicht ändern.

Zum Port ist zu sagen, daß er recht beliebig ist. Prinzipiell kann jeder Port genommen werden, den die bereits vorhandene MySQL-Instanz (der Master) nicht nutzt (bzw. den kein anderer Dienst nutzt).

Die Server-ID ist ebenfalls recht beliebig, es muß lediglich eine andere sein als beim Master: „For each server, you should pick a unique positive integer in the range from 1 to 232 – 1, and each ID must be different from every other ID.“ Quelle: MySQL Doku

Die /etc/my.cnf benötigt weniger Anpassungen:


[mysqld]
log-bin=mysql-bin
server-id = 1

Das binary log benötigt man bei Replikation immer, man kommt nicht drum herum. Hier bei sei angemerkt, daß man es unter Umständen später mit Logrotation oder ähnlichen Mechanismen wegräumen kann. Das ist aber nicht ganz einfach, einerseits muß man darauf achten dem Master nicht das aktive Log wegzurotieren, andererseits könnte man die alten binary logs unter Umständen benötigen, falls man in der Zukunft weitere Slaves anlegt. (Diese kann man aber auch von einem späteren Stand des binary logs beginnen.)

Nun muß man dafür sorgen, daß Master und Slave auch beide gestartet werden. Wie man hier vorgeht hängt sehr davon ab, wie man seine MySQL-Instanzen startet, also ob man z.B. selber was skriptet, init verwendet oder lieber auf daemontools oder runit setzt. Wir bevorzugen daemontools.

Bei daemontools muß man lediglich ein run-Skript schreiben, mit dem der Slave mit der /etc/my-slave.cnf gestartet wird. Im einfachsten Fall sieht das so aus:


#!/bin/sh
exec /usr/bin/mysqld_safe --defaults-file=/etc/my-slave.cnf

Wer auf init setzt, muß entweder ein neues init-Skript nur für den Slave schreiben oder das vorhandene so anpassen, daß es beide Instanzen startet. Je nach verwendeter Distribution kann das unterschiedlich aufwendig werden.

An dieser Stelle sollte man den Master schonmal neustarten, damit er die Änderungen in der /etc/my.cnf übernimmt und vor allem anfängt, das binary log zu führen.

Jetzt muß man noch dafür sorgen, daß der Slave auch einen eigenen Datenbestand hat. Wie man dabei vorgeht hängt stark davon ab, wie viel einmalige Downtime man sich beim Einrichten des Slaves erlauben kann und wie groß der Datenbestand ist.

Falls der Datenbestand in der MySQL-Instanz bereits groß ist und eine lange Downtime vermieden werden soll, kann zunächst mit rsync eine Kopie des MySQL-Verzeichnisses eingefertigt werden. Diese Kopie ist zwar nicht gut genug um davon die neue Instanz zu starten, aber da meist ein Großteil des Datenbestands selten oder gar nicht verändert wird, kann man hier schon viel Vorarbeit leisten und so später Zeit sparen.

# mkdir /var/lib/mysql-slave
# rsync -vaH /var/lib/mysql/ /var/lib/mysql-slave/

Wie immer bei rsync ist der Slash am Ende des Pfades jeweils wichtig. Hier wird rsync befohlen, nicht den Ordner /var/lib/mysql/ selbst, sondern seinen Inhalt in den Ordner /var/lib/mysql-slave/ zu kopieren.

Wenn eine spürbare Verlangsamung des Systems vermieden werden soll, kann man das übrigens auch mit nice oder bei neueren Kerneln ionice machen:

# nice -n19 rsync -vaH /var/lib/mysql/ /var/lib/mysql-slave/

bzw.:

# ionice -c3 rsync -vaH /var/lib/mysql/ /var/lib/mysql-slave/

Wie gesagt ist der so duplizierte Datenbestand nicht konsistent! Man kann den Slave noch nicht damit starten. Um dies nun möglich zu machen muß man den Master dazu bringen alle Daten konsistent auf die Platte zu schreiben und ihn kurzzeitig mit einem READ LOCK sperren und dann während die Sperre besteht erneut rsync verwenden. Bei der Gelegenheit kann man gleich die Zugriffsrechte für den Slave einräumen.

Man verbindet sich also mit dem Master und befiehlt:

mysql> GRANT REPLICATION SLAVE ON *.* TO slave@'%localhost' IDENTIFIED BY 'PASSWORD';
mysql>FLUSH TABLES WITH READ LOCK;
mysql>SHOW MASTER STATUS;
+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000001 | 2730869 | | |
+------------------+----------+--------------+------------------+
1 row in set (0.00 sec)

(Das Passwort kann man selbstverständlich selbst wählen.)

Die Ausgabe von SHOW MASTER STATUS sollte man sich notieren, wir benötigen sie gleich, genau genommen den Dateinamen und die Position (den Offset) im binary log, in unserem Bespiel ist das die 2730869. (Eventuell benötigt man sie später nochmal, wenn man weitere Slaves einrichten sollte, die auch ab dieser Position die Replikation beginnen sollen. In diesem Fall darf man später auch das binary log nicht wegrotieren. Insgesamt scheint dieser Anwendungsfall aber eher selten zu sein.)

Nun muß man den Master kurz anhalten und die Datenverzeichnisse synchronisieren. Hierbei sollte man ausnahmsweise mysqladmin benutzen, damit die Sperre erhalten bleibt. (Normalerweise werden Sperren beim Beenden aufgehoben.)

Update:

# mysqladmin shutdown
rsync -vaH /var/lib/mysql/ /var/lib/mysql-slave/

Es muß mysqladmin shutdown verwendet werden, mysqladmin stop war ein Tippfehler. /Update.

Sobald das erledigt ist, kann man den Master wieder starten und die Sperre aufheben, letzteres mit: mysql>UNLOCK TABLES;

(Wer später weitere Slaves aufsetzen möchte, die an dieser Stelle im Log mit der Replikation beginnen, sollte sich jetzt ein Backup von /var/lib/mysql-slave ziehen, z.B. einen Tarball.)

Jetzt kann der Slave gefahrlos gestartet werden, denn sein Datenverzeichnis enthält konsistente Daten. Wenn er sauber startet, kann man sich auch gleich mit ihm verbinden.

Achtung: Wenn man Master- und Slave-Instanz auf dem selben Computer am Laufen hat, gilt es bei der Verbindung zur Slave-Instanz folgendes zu beachten: Man kann nicht wie gewohnt einfach mysql auf der Kommandozeile ausführen, da das eine Verbindung zur Master-Instanz öffnen würde. Auch die Angabe des Ports mit mysql -P 3307 hilft nicht weiter, da MySQL in diesem Fall, trotz der expliziten Portangabe eine Verbindung zur Master-Instanz öffnen würde (dafür mal ein verdientes #fail an die Adresse der Programmierer). Das gleiche ärgerliche Verhalten würde auch bei mysql -P 3307 -H localhost auftreten.
Damit das funktioniert muß man mysql -P 3307 -H 127.0.0.1 (IPv4) oder mysql -P -H ::1 (IPv6) benutzen. Eine wie ich finde noch elegantere Lösung ist mysql -S /var/lib/mysql-slave/mysql.sock. Wozu den Networkstack bemühen, wenn man schon eine Shell offen hat?

Nun muß dem Slave noch mitgeteilt werden, daß er Änderungen an seinem Datenbestand vom Master replizieren sollt.

mysql> CHANGE MASTER TO
-> MASTER_HOST='locahost',
-> MASTER_USER='slave',
-> MASTER_PASSWORD='PASSWORD.',
-> MASTER_LOG_FILE='mysql-bin.000001',
-> MASTER_LOG_POS='2730869';
mysql> START SLAVE;

Der Slave beginnt jetzt ausgehend von der angegebenen Position (2730869) im binary log seinen Datenbestand zu synchronisieren. Den Fortschritt kann man mit mysql> SHOW SLAVE STATUS\G am besten einsehen. Die letzte Position ist dabei das entscheidende. Seconds_Behind_Master gibt an, ob der Slave mit dem Master synchron ist oder nicht. Steht hier der Wert NULL, ist der Slave angehalten und synchronisiert sich nicht mit dem Master, steht hier ingegen eine Zahl größer 0, gibt sie an wie viele Sekunden der Slave hinter dem Master zurückliegt (diese Zahl sollte schnell kleiner werden), steht hier genau 0 dann ist der Slave synchron zum Master.