La réplication retardée permet à un esclave de réplication d'être délibérément en retard sur le maître d'au moins une durée spécifiée. Avant d'exécuter un événement, l'esclave attendra d'abord, si nécessaire, que le temps imparti se soit écoulé depuis la création de l'événement sur le maître. Le résultat est que l'esclave reflétera l'état du maître il y a quelque temps dans le passé. Cette fonctionnalité est prise en charge depuis MySQL 5.6 et MariaDB 10.2.3. Cela peut s'avérer utile en cas de suppression accidentelle de données et devrait faire partie de votre plan de reprise après sinistre.
Le problème lors de la configuration d'un esclave à réplication retardée est le délai à appliquer. Trop peu de temps et vous risquez que la mauvaise requête atteigne votre esclave retardé avant que vous ne puissiez y accéder, perdant ainsi le point d'avoir l'esclave retardé. En option, vous pouvez faire en sorte que votre temps de retard soit si long qu'il faut des heures à votre esclave retardé pour rattraper l'endroit où se trouvait le maître au moment de l'erreur.
Heureusement avec Docker, l'isolation des processus est sa force. L'exécution de plusieurs instances MySQL est assez pratique avec Docker. Cela nous permet d'avoir plusieurs esclaves retardés au sein d'un seul hôte physique pour améliorer notre temps de récupération et économiser les ressources matérielles. Si vous pensez qu'un délai de 15 minutes est trop court, nous pouvons avoir une autre instance avec un délai de 1 heure ou 6 heures pour un instantané encore plus ancien de notre base de données.
Dans cet article de blog, nous allons déployer plusieurs esclaves retardés MySQL sur un seul hôte physique avec Docker et montrer quelques scénarios de récupération. Le schéma suivant illustre notre architecture finale que nous souhaitons construire :
Notre architecture consiste en une réplication MySQL à 2 nœuds déjà déployée s'exécutant sur des serveurs physiques (bleu) et nous aimerions configurer trois autres esclaves MySQL (vert) avec le comportement suivant :
- 15 minutes de retard
- 1 heure de retard
- 6 heures de retard
Prenez note que nous allons avoir 3 copies des mêmes données exactes sur le même serveur physique. Assurez-vous que notre hôte Docker dispose de l'espace de stockage requis, allouez donc suffisamment d'espace disque au préalable.
Maîtrise MySQL
Tout d'abord, connectez-vous au serveur maître et créez l'utilisateur de réplication :
mysql> GRANT REPLICATION SLAVE ON *.* TO [email protected]'%' IDENTIFIED BY 'YlgSH6bLLy';
Créez ensuite une sauvegarde compatible PITR sur le maître :
$ mysqldump -uroot -p --flush-privileges --hex-blob --opt --master-data=1 --single-transaction --skip-lock-tables --skip-lock-tables --triggers --routines --events --all-databases | gzip -6 -c > mysqldump_complete.sql.gz
Si vous utilisez ClusterControl, vous pouvez facilement effectuer une sauvegarde compatible PITR. Allez dans Sauvegardes > Créer une sauvegarde et sélectionnez "Compatible PITR complet" dans la liste déroulante "Type de vidage" :
Enfin, transférez cette sauvegarde vers l'hôte Docker :
$ scp mysqldump_complete.sql.gz [email protected]:~
Ce fichier de sauvegarde sera utilisé par les conteneurs esclaves MySQL pendant le processus d'amorçage de l'esclave, comme indiqué dans la section suivante.
Déploiement retardé de l'esclave
Préparez nos répertoires de conteneurs Docker. Créez 3 répertoires (mysql.conf.d, datadir et sql) pour chaque conteneur MySQL que nous allons lancer (vous pouvez utiliser loop pour simplifier les commandes ci-dessous) :
$ mkdir -p /storage/mysql-slave-15m/mysql.conf.d
$ mkdir -p /storage/mysql-slave-15m/datadir
$ mkdir -p /storage/mysql-slave-15m/sql
$ mkdir -p /storage/mysql-slave-1h/mysql.conf.d
$ mkdir -p /storage/mysql-slave-1h/datadir
$ mkdir -p /storage/mysql-slave-1h/sql
$ mkdir -p /storage/mysql-slave-6h/mysql.conf.d
$ mkdir -p /storage/mysql-slave-6h/datadir
$ mkdir -p /storage/mysql-slave-6h/sql
Le répertoire "mysql.conf.d" stockera notre fichier de configuration MySQL personnalisé et sera mappé dans le conteneur sous /etc/mysql.conf.d. "datadir" est l'endroit où nous voulons que Docker stocke le répertoire de données MySQL, qui correspond à /var/lib/mysql du conteneur et le répertoire "sql" stocke nos fichiers SQL - fichiers de sauvegarde au format .sql ou .sql.gz à mettre en scène l'esclave avant la réplication ainsi que des fichiers .sql pour automatiser la configuration et le démarrage de la réplication.
Esclave retardé de 15 minutes
Préparez le fichier de configuration MySQL pour notre esclave retardé de 15 minutes :
$ vim /storage/mysql-slave-15m/mysql.conf.d/my.cnf
Et ajoutez les lignes suivantes :
[mysqld]
server_id=10015
binlog_format=ROW
log_bin=binlog
log_slave_updates=1
gtid_mode=ON
enforce_gtid_consistency=1
relay_log=relay-bin
expire_logs_days=7
read_only=ON
** La valeur d'ID de serveur que nous avons utilisée pour cet esclave est 10015.
Ensuite, sous le répertoire /storage/mysql-slave-15m/sql, créez deux fichiers SQL, un pour RESET MASTER (1reset_master.sql) et un autre pour établir le lien de réplication à l'aide de l'instruction CHANGE MASTER (3setup_slave.sql).
Créez un fichier texte 1reset_master.sql et ajoutez la ligne suivante :
RESET MASTER;
Créez un fichier texte 3setup_slave.sql et ajoutez les lignes suivantes :
CHANGE MASTER TO MASTER_HOST = '192.168.55.171', MASTER_USER = 'rpl_user', MASTER_PASSWORD = 'YlgSH6bLLy', MASTER_AUTO_POSITION = 1, MASTER_DELAY=900;
START SLAVE;
MASTER_DELAY=900 est égal à 15 minutes (en secondes). Ensuite, copiez le fichier de sauvegarde extrait de notre maître (qui a été transféré dans notre hôte Docker) dans le répertoire "sql" et renommez-le en 2mysqldump_complete.sql.gz :
$ cp ~/mysqldump_complete.tar.gz /storage/mysql-slave-15m/sql/2mysqldump_complete.tar.gz
L'aspect final de notre répertoire "sql" devrait ressembler à ceci :
$ pwd
/storage/mysql-slave-15m/sql
$ ls -1
1reset_master.sql
2mysqldump_complete.sql.gz
3setup_slave.sql
Notez que nous préfixons le nom de fichier SQL avec un entier pour déterminer l'ordre d'exécution lorsque Docker initialise le conteneur MySQL.
Une fois que tout est en place, exécutez le conteneur MySQL pour notre esclave retardé de 15 minutes :
$ docker run -d \
--name mysql-slave-15m \
-e MYSQL_ROOT_PASSWORD=password \
--mount type=bind,source=/storage/mysql-slave-15m/datadir,target=/var/lib/mysql \
--mount type=bind,source=/storage/mysql-slave-15m/mysql.conf.d,target=/etc/mysql/mysql.conf.d \
--mount type=bind,source=/storage/mysql-slave-15m/sql,target=/docker-entrypoint-initdb.d \
mysql:5.7
** La valeur MYSQL_ROOT_PASSWORD doit être identique au mot de passe racine MySQL sur le maître.
Les lignes suivantes sont ce que nous recherchons pour vérifier si MySQL fonctionne correctement et connecté en tant qu'esclave à notre maître (192.168.55.171) :
$ docker logs -f mysql-slave-15m
...
2018-12-04T04:05:24.890244Z 0 [Note] mysqld: ready for connections.
Version: '5.7.24-log' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server (GPL)
2018-12-04T04:05:25.010032Z 2 [Note] Slave I/O thread for channel '': connected to master '[email protected]:3306',replication started in log 'FIRST' at position 4
Vous pouvez ensuite vérifier l'état de la réplication avec la déclaration suivante :
$ docker exec -it mysql-slave-15m mysql -uroot -p -e 'show slave status\G'
...
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
SQL_Delay: 900
Auto_Position: 1
...
À ce stade, notre conteneur esclave retardé de 15 minutes se réplique correctement et notre architecture ressemble à ceci :
Esclave retardé d'une heure
Préparez le fichier de configuration MySQL pour notre esclave retardé d'une heure :
$ vim /storage/mysql-slave-1h/mysql.conf.d/my.cnf
Et ajoutez les lignes suivantes :
[mysqld]
server_id=10060
binlog_format=ROW
log_bin=binlog
log_slave_updates=1
gtid_mode=ON
enforce_gtid_consistency=1
relay_log=relay-bin
expire_logs_days=7
read_only=ON
** La valeur d'ID de serveur que nous avons utilisée pour cet esclave est 10060.
Ensuite, sous le répertoire /storage/mysql-slave-1h/sql, créez deux fichiers SQL, un pour RESET MASTER (1reset_master.sql) et un autre pour établir le lien de réplication à l'aide de l'instruction CHANGE MASTER (3setup_slave.sql).
Créez un fichier texte 1reset_master.sql et ajoutez la ligne suivante :
RESET MASTER;
Créez un fichier texte 3setup_slave.sql et ajoutez les lignes suivantes :
CHANGE MASTER TO MASTER_HOST = '192.168.55.171', MASTER_USER = 'rpl_user', MASTER_PASSWORD = 'YlgSH6bLLy', MASTER_AUTO_POSITION = 1, MASTER_DELAY=3600;
START SLAVE;
MASTER_DELAY=3600 est égal à 1 heure (en secondes). Ensuite, copiez le fichier de sauvegarde extrait de notre maître (qui a été transféré dans notre hôte Docker) dans le répertoire "sql" et renommez-le en 2mysqldump_complete.sql.gz :
$ cp ~/mysqldump_complete.tar.gz /storage/mysql-slave-1h/sql/2mysqldump_complete.tar.gz
L'aspect final de notre répertoire "sql" devrait ressembler à ceci :
$ pwd
/storage/mysql-slave-1h/sql
$ ls -1
1reset_master.sql
2mysqldump_complete.sql.gz
3setup_slave.sql
Notez que nous préfixons le nom de fichier SQL avec un entier pour déterminer l'ordre d'exécution lorsque Docker initialise le conteneur MySQL.
Une fois que tout est en place, exécutez le conteneur MySQL pour notre esclave retardé d'une heure :
$ docker run -d \
--name mysql-slave-1h \
-e MYSQL_ROOT_PASSWORD=password \
--mount type=bind,source=/storage/mysql-slave-1h/datadir,target=/var/lib/mysql \
--mount type=bind,source=/storage/mysql-slave-1h/mysql.conf.d,target=/etc/mysql/mysql.conf.d \
--mount type=bind,source=/storage/mysql-slave-1h/sql,target=/docker-entrypoint-initdb.d \
mysql:5.7
** La valeur MYSQL_ROOT_PASSWORD doit être identique au mot de passe racine MySQL sur le maître.
Les lignes suivantes sont ce que nous recherchons pour vérifier si MySQL fonctionne correctement et connecté en tant qu'esclave à notre maître (192.168.55.171) :
$ docker logs -f mysql-slave-1h
...
2018-12-04T04:05:24.890244Z 0 [Note] mysqld: ready for connections.
Version: '5.7.24-log' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server (GPL)
2018-12-04T04:05:25.010032Z 2 [Note] Slave I/O thread for channel '': connected to master '[email protected]:3306',replication started in log 'FIRST' at position 4
Vous pouvez ensuite vérifier l'état de la réplication avec la déclaration suivante :
$ docker exec -it mysql-slave-1h mysql -uroot -p -e 'show slave status\G'
...
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
SQL_Delay: 3600
Auto_Position: 1
...
À ce stade, nos conteneurs esclaves retardés MySQL de 15 minutes et 1 heure se répliquent à partir du maître et notre architecture ressemble à ceci :
Esclave retardé de 6 heures
Préparez le fichier de configuration MySQL pour notre esclave retardé de 6 heures :
$ vim /storage/mysql-slave-15m/mysql.conf.d/my.cnf
Et ajoutez les lignes suivantes :
[mysqld]
server_id=10006
binlog_format=ROW
log_bin=binlog
log_slave_updates=1
gtid_mode=ON
enforce_gtid_consistency=1
relay_log=relay-bin
expire_logs_days=7
read_only=ON
** La valeur d'ID de serveur que nous avons utilisée pour cet esclave est 10006.
Ensuite, sous le répertoire /storage/mysql-slave-6h/sql, créez deux fichiers SQL, un pour RESET MASTER (1reset_master.sql) et un autre pour établir le lien de réplication à l'aide de l'instruction CHANGE MASTER (3setup_slave.sql).
Créez un fichier texte 1reset_master.sql et ajoutez la ligne suivante :
RESET MASTER;
Créez un fichier texte 3setup_slave.sql et ajoutez les lignes suivantes :
CHANGE MASTER TO MASTER_HOST = '192.168.55.171', MASTER_USER = 'rpl_user', MASTER_PASSWORD = 'YlgSH6bLLy', MASTER_AUTO_POSITION = 1, MASTER_DELAY=21600;
START SLAVE;
MASTER_DELAY=21600 est égal à 6 heures (en secondes). Ensuite, copiez le fichier de sauvegarde extrait de notre maître (qui a été transféré dans notre hôte Docker) dans le répertoire "sql" et renommez-le en 2mysqldump_complete.sql.gz :
$ cp ~/mysqldump_complete.tar.gz /storage/mysql-slave-6h/sql/2mysqldump_complete.tar.gz
L'aspect final de notre répertoire "sql" devrait ressembler à ceci :
$ pwd
/storage/mysql-slave-6h/sql
$ ls -1
1reset_master.sql
2mysqldump_complete.sql.gz
3setup_slave.sql
Notez que nous préfixons le nom de fichier SQL avec un entier pour déterminer l'ordre d'exécution lorsque Docker initialise le conteneur MySQL.
Une fois que tout est en place, exécutez le conteneur MySQL pour notre esclave retardé de 6 heures :
$ docker run -d \
--name mysql-slave-6h \
-e MYSQL_ROOT_PASSWORD=password \
--mount type=bind,source=/storage/mysql-slave-6h/datadir,target=/var/lib/mysql \
--mount type=bind,source=/storage/mysql-slave-6h/mysql.conf.d,target=/etc/mysql/mysql.conf.d \
--mount type=bind,source=/storage/mysql-slave-6h/sql,target=/docker-entrypoint-initdb.d \
mysql:5.7
** La valeur MYSQL_ROOT_PASSWORD doit être identique au mot de passe racine MySQL sur le maître.
Les lignes suivantes sont ce que nous recherchons pour vérifier si MySQL fonctionne correctement et connecté en tant qu'esclave à notre maître (192.168.55.171) :
$ docker logs -f mysql-slave-6h
...
2018-12-04T04:05:24.890244Z 0 [Note] mysqld: ready for connections.
Version: '5.7.24-log' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server (GPL)
2018-12-04T04:05:25.010032Z 2 [Note] Slave I/O thread for channel '': connected to master '[email protected]:3306',replication started in log 'FIRST' at position 4
Vous pouvez ensuite vérifier l'état de la réplication avec la déclaration suivante :
$ docker exec -it mysql-slave-6h mysql -uroot -p -e 'show slave status\G'
...
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
SQL_Delay: 21600
Auto_Position: 1
...
À ce stade, nos conteneurs esclaves retardés de 5 minutes, 1 heure et 6 heures se répliquent correctement et notre architecture ressemble à ceci :
Scénario de reprise après sinistre
Disons qu'un utilisateur a accidentellement laissé tomber une mauvaise colonne sur une grande table. Considérez que l'instruction suivante a été exécutée sur le maître :
mysql> USE shop;
mysql> ALTER TABLE settings DROP COLUMN status;
Si vous avez la chance de le réaliser immédiatement, vous pouvez utiliser l'esclave retardé de 15 minutes pour rattraper le moment avant que la catastrophe ne se produise et le promouvoir pour devenir maître, ou exporter les données manquantes et les restaurer sur le maître.
Tout d'abord, nous devons trouver la position du journal binaire avant que la catastrophe ne se produise. Saisissez l'heure maintenant() sur le maître :
mysql> SELECT now();
+---------------------+
| now() |
+---------------------+
| 2018-12-04 14:55:41 |
+---------------------+
Ensuite, récupérez le fichier journal binaire actif sur le maître :
mysql> SHOW MASTER STATUS;
+---------------+----------+--------------+------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+---------------+----------+--------------+------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| binlog.000004 | 20260658 | | | 1560665e-ed2b-11e8-93fa-000c29b7f985:1-12031,
1b235f7a-d37b-11e8-9c3e-000c29bafe8f:1-62519,
1d8dc60a-e817-11e8-82ff-000c29bafe8f:1-326575,
791748b3-d37a-11e8-b03a-000c29b7f985:1-374 |
+---------------+----------+--------------+------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
En utilisant le même format de date, extrayez les informations souhaitées du journal binaire, binlog.000004. Nous estimons l'heure de début de la lecture du binlog il y a environ 20 minutes (2018-12-04 14:35:00) et filtrons la sortie pour afficher 25 lignes avant l'instruction "drop column" :
$ mysqlbinlog --start-datetime="2018-12-04 14:35:00" --stop-datetime="2018-12-04 14:55:41" /var/lib/mysql/binlog.000004 | grep -i -B 25 "drop column"
'/*!*/;
# at 19379172
#181204 14:54:45 server id 1 end_log_pos 19379232 CRC32 0x0716e7a2 Table_map: `shop`.`settings` mapped to number 766
# at 19379232
#181204 14:54:45 server id 1 end_log_pos 19379460 CRC32 0xa6187edd Write_rows: table id 766 flags: STMT_END_F
BINLOG '
tSQGXBMBAAAAPAAAACC0JwEAAP4CAAAAAAEABnNidGVzdAAHc2J0ZXN0MgAFAwP+/gME/nj+PBCi
5xYH
tSQGXB4BAAAA5AAAAAS1JwEAAP4CAAAAAAEAAgAF/+AYwwAAysYAAHc0ODYyMjI0NjI5OC0zNDE2
OTY3MjY5OS02MDQ1NTQwOTY1Ny01MjY2MDQ0MDcwOC05NDA0NzQzOTUwMS00OTA2MTAxNzgwNC05
OTIyMzM3NzEwOS05NzIwMzc5NTA4OC0yODAzOTU2NjQ2MC0zNzY0ODg3MTYzOTswMTM0MjAwNTcw
Ni02Mjk1ODMzMzExNi00NzQ1MjMxODA1OS0zODk4MDQwMjk5MS03OTc4MTA3OTkwNQEAAADdfhim
'/*!*/;
# at 19379460
#181204 14:54:45 server id 1 end_log_pos 19379491 CRC32 0x71f00e63 Xid = 622405
COMMIT/*!*/;
# at 19379491
#181204 14:54:46 server id 1 end_log_pos 19379556 CRC32 0x62b78c9e GTID last_committed=11507 sequence_number=11508 rbr_only=no
SET @@SESSION.GTID_NEXT= '1560665e-ed2b-11e8-93fa-000c29b7f985:11508'/*!*/;
# at 19379556
#181204 14:54:46 server id 1 end_log_pos 19379672 CRC32 0xc222542a Query thread_id=3162 exec_time=1 error_code=0
SET TIMESTAMP=1543906486/*!*/;
/*!\C utf8 *//*!*/;
SET @@session.character_set_client=33,@@session.collation_connection=33,@@session.collation_server=8/*!*/;
ALTER TABLE settings DROP COLUMN status
Dans les dernières lignes de la sortie mysqlbinlog, vous devriez avoir la commande erronée qui a été exécutée à la position 19379556. La position que nous devons restaurer est une étape avant cela, qui est en position 19379491. C'est la position binlog où nous voulons que notre esclave retardé jusqu'à.
Ensuite, sur l'esclave retardé choisi, arrêtez l'esclave de réplication retardée et redémarrez l'esclave jusqu'à une position finale fixe que nous avons déterminée ci-dessus :
$ docker exec -it mysql-slave-15m mysql -uroot -p
mysql> STOP SLAVE;
mysql> START SLAVE UNTIL MASTER_LOG_FILE = 'binlog.000004', MASTER_LOG_POS = 19379491;
Surveillez l'état de la réplication et attendez que Exec_Master_Log_Pos soit égal à la valeur Until_Log_Pos. Cela pourrait prendre du temps. Une fois rattrapé, vous devriez voir ceci :
$ docker exec -it mysql-slave-15m mysql -uroot -p -e 'SHOW SLAVE STATUS\G'
...
Exec_Master_Log_Pos: 19379491
Relay_Log_Space: 50552186
Until_Condition: Master
Until_Log_File: binlog.000004
Until_Log_Pos: 19379491
...
Vérifiez enfin si la donnée manquante que nous recherchions s'y trouve (la colonne "statut" existe toujours) :
mysql> DESCRIBE shop.settings;
+--------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| sid | int(10) unsigned | NO | MUL | 0 | |
| param | varchar(100) | NO | | | |
| value | varchar(255) | NO | | | |
| status | int(11) | YES | | 1 | |
+--------+------------------+------+-----+---------+----------------+
Exportez ensuite la table depuis notre conteneur esclave et transférez-la sur le serveur maître :
$ docker exec -it mysql-slave-1h mysqldump -uroot -ppassword --single-transaction shop settings > shop_settings.sql
Supprimez la table problématique et restaurez-la sur le maître :
$ mysql -uroot -p -e 'DROP TABLE shop.settings'
$ mysqldump -uroot -p -e shop < shop_setttings.sql
Nous avons maintenant récupéré notre table dans son état d'origine avant l'événement désastreux. Pour résumer, la réplication différée peut être utilisée à plusieurs fins :
- Pour se protéger contre les erreurs de l'utilisateur sur le maître. Un administrateur de base de données peut restaurer un esclave retardé à l'heure juste avant le sinistre.
- Pour tester le comportement du système en cas de décalage. Par exemple, dans une application, un décalage peut être causé par une forte charge sur l'esclave. Cependant, il peut être difficile de générer ce niveau de charge. La réplication différée peut simuler le décalage sans avoir à simuler la charge. Il peut également être utilisé pour déboguer les conditions liées à un esclave en retard.
- Pour inspecter à quoi ressemblait la base de données dans le passé, sans avoir à recharger une sauvegarde. Par exemple, si le retard est d'une semaine et que le DBA a besoin de voir à quoi ressemblait la base de données avant les derniers jours de développement, l'esclave retardé peut être inspecté.
Réflexions finales
Avec Docker, l'exécution de plusieurs instances MySQL sur un même hôte physique peut être effectuée efficacement. Vous pouvez utiliser des outils d'orchestration Docker tels que Docker Compose et Swarm pour simplifier le déploiement multi-conteneurs par opposition aux étapes présentées dans cet article de blog.