La replicación retrasada permite que un esclavo de replicación se retrase deliberadamente con respecto al maestro por al menos una cantidad de tiempo específica. Antes de ejecutar un evento, el esclavo primero esperará, si es necesario, hasta que haya pasado el tiempo dado desde que se creó el evento en el maestro. El resultado es que el esclavo reflejará el estado del maestro algún tiempo atrás en el pasado. Esta función es compatible desde MySQL 5.6 y MariaDB 10.2.3. Puede resultar útil en caso de eliminación accidental de datos y debe formar parte de su plan de recuperación ante desastres.
El problema al configurar un esclavo de replicación retrasada es cuánto retraso debemos poner. Demasiado poco tiempo y corre el riesgo de que la consulta incorrecta llegue a su esclavo retrasado antes de que pueda llegar a él, desperdiciando así el punto de tener el esclavo retrasado. Opcionalmente, puede hacer que su tiempo de retraso sea tan largo que su esclavo retrasado tarde horas en alcanzar el lugar donde estaba el maestro en el momento del error.
Afortunadamente con Docker, el aislamiento de procesos es su punto fuerte. Ejecutar múltiples instancias de MySQL es bastante conveniente con Docker. Nos permite tener múltiples esclavos retrasados dentro de un solo host físico para mejorar nuestro tiempo de recuperación y ahorrar recursos de hardware. Si cree que un retraso de 15 minutos es demasiado corto, podemos tener otra instancia con un retraso de 1 hora o 6 horas para una instantánea aún más antigua de nuestra base de datos.
En esta publicación de blog, implementaremos varios esclavos retrasados de MySQL en un solo host físico con Docker y mostraremos algunos escenarios de recuperación. El siguiente diagrama ilustra nuestra arquitectura final que queremos construir:
Nuestra arquitectura consta de una replicación MySQL de 2 nodos ya implementada que se ejecuta en servidores físicos (azul) y nos gustaría configurar otros tres esclavos MySQL (verde) con el siguiente comportamiento:
- 15 minutos de retraso
- 1 hora de retraso
- Retraso de 6 horas
Tenga en cuenta que vamos a tener 3 copias de exactamente los mismos datos en el mismo servidor físico. Asegúrese de que nuestro host Docker tenga el almacenamiento necesario, así que asigne suficiente espacio en disco de antemano.
Preparación maestra de MySQL
En primer lugar, inicie sesión en el servidor maestro y cree el usuario de replicación:
mysql> GRANT REPLICATION SLAVE ON *.* TO [email protected]'%' IDENTIFIED BY 'YlgSH6bLLy';
Luego, cree una copia de seguridad compatible con PITR en el maestro:
$ 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 está utilizando ClusterControl, puede realizar fácilmente una copia de seguridad compatible con PITR. Vaya a Copias de seguridad -> Crear copia de seguridad y elija "Completamente compatible con PITR" en el menú desplegable "Tipo de volcado":
Finalmente, transfiera esta copia de seguridad al host de Docker:
$ scp mysqldump_complete.sql.gz [email protected]:~
Este archivo de copia de seguridad será utilizado por los contenedores esclavos de MySQL durante el proceso de arranque del esclavo, como se muestra en la siguiente sección.
Despliegue esclavo retrasado
Prepare nuestros directorios de contenedores de Docker. Cree 3 directorios (mysql.conf.d, datadir y sql) para cada contenedor de MySQL que vamos a lanzar (puede usar loop para simplificar los comandos a continuación):
$ 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
El directorio "mysql.conf.d" almacenará nuestro archivo de configuración MySQL personalizado y se asignará al contenedor en /etc/mysql.conf.d. "datadir" es donde queremos que Docker almacene el directorio de datos MySQL, que se asigna a /var/lib/mysql del contenedor y el directorio "sql" almacena nuestros archivos SQL:archivos de copia de seguridad en formato .sql o .sql.gz para el escenario el esclavo antes de la replicación y también archivos .sql para automatizar la configuración y el inicio de la replicación.
Esclavo retrasado de 15 minutos
Prepare el archivo de configuración de MySQL para nuestro esclavo retrasado de 15 minutos:
$ vim /storage/mysql-slave-15m/mysql.conf.d/my.cnf
Y agregue las siguientes líneas:
[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
** El valor de ID del servidor que usamos para este esclavo es 10015.
A continuación, en el directorio /storage/mysql-slave-15m/sql, cree dos archivos SQL, uno para RESET MASTER (1reset_master.sql) y otro para establecer el enlace de replicación mediante la declaración CHANGE MASTER (3setup_slave.sql).
Cree un archivo de texto 1reset_master.sql y agregue la siguiente línea:
RESET MASTER;
Cree un archivo de texto 3setup_slave.sql y agregue las siguientes líneas:
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 es igual a 15 minutos (en segundos). Luego copie el archivo de copia de seguridad tomado de nuestro maestro (que ha sido transferido a nuestro host Docker) al directorio "sql" y renómbrelo como 2mysqldump_complete.sql.gz:
$ cp ~/mysqldump_complete.tar.gz /storage/mysql-slave-15m/sql/2mysqldump_complete.tar.gz
El aspecto final de nuestro directorio "sql" debería ser algo como esto:
$ pwd
/storage/mysql-slave-15m/sql
$ ls -1
1reset_master.sql
2mysqldump_complete.sql.gz
3setup_slave.sql
Tenga en cuenta que anteponemos el nombre del archivo SQL con un número entero para determinar el orden de ejecución cuando Docker inicializa el contenedor MySQL.
Una vez que todo esté en su lugar, ejecute el contenedor MySQL para nuestro esclavo retrasado de 15 minutos:
$ 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
** El valor de MYSQL_ROOT_PASSWORD debe ser el mismo que la contraseña raíz de MySQL en el maestro.
Las siguientes líneas son las que buscamos para verificar si MySQL se está ejecutando correctamente y conectado como esclavo a nuestro maestro (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
Luego puede verificar el estado de la replicación con la siguiente declaración:
$ 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
...
En este punto, nuestro contenedor esclavo retrasado de 15 minutos se está replicando correctamente y nuestra arquitectura se parece a esto:
Esclavo retrasado de 1 hora
Prepare el archivo de configuración de MySQL para nuestro esclavo retrasado de 1 hora:
$ vim /storage/mysql-slave-1h/mysql.conf.d/my.cnf
Y agregue las siguientes líneas:
[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
** El valor de ID del servidor que usamos para este esclavo es 10060.
A continuación, en el directorio /storage/mysql-slave-1h/sql, cree dos archivos SQL, uno para RESET MASTER (1reset_master.sql) y otro para establecer el enlace de replicación mediante la instrucción CHANGE MASTER (3setup_slave.sql).
Cree un archivo de texto 1reset_master.sql y agregue la siguiente línea:
RESET MASTER;
Cree un archivo de texto 3setup_slave.sql y agregue las siguientes líneas:
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 es igual a 1 hora (en segundos). Luego copie el archivo de copia de seguridad tomado de nuestro maestro (que ha sido transferido a nuestro host Docker) al directorio "sql" y renómbrelo como 2mysqldump_complete.sql.gz:
$ cp ~/mysqldump_complete.tar.gz /storage/mysql-slave-1h/sql/2mysqldump_complete.tar.gz
El aspecto final de nuestro directorio "sql" debería ser algo como esto:
$ pwd
/storage/mysql-slave-1h/sql
$ ls -1
1reset_master.sql
2mysqldump_complete.sql.gz
3setup_slave.sql
Tenga en cuenta que anteponemos el nombre del archivo SQL con un número entero para determinar el orden de ejecución cuando Docker inicializa el contenedor MySQL.
Una vez que todo esté en su lugar, ejecute el contenedor MySQL para nuestro esclavo retrasado de 1 hora:
$ 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
** El valor de MYSQL_ROOT_PASSWORD debe ser el mismo que la contraseña raíz de MySQL en el maestro.
Las siguientes líneas son las que buscamos para verificar si MySQL se está ejecutando correctamente y conectado como esclavo a nuestro maestro (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
Luego puede verificar el estado de la replicación con la siguiente declaración:
$ 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
...
En este punto, nuestros contenedores esclavos retrasados de MySQL de 15 minutos y 1 hora se están replicando desde el maestro y nuestra arquitectura se parece a esto:
Esclavo retrasado de 6 horas
Prepare el archivo de configuración de MySQL para nuestro esclavo retrasado de 6 horas:
$ vim /storage/mysql-slave-15m/mysql.conf.d/my.cnf
Y agregue las siguientes líneas:
[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
** El valor de ID del servidor que usamos para este esclavo es 10006.
A continuación, en el directorio /storage/mysql-slave-6h/sql, cree dos archivos SQL, uno para RESET MASTER (1reset_master.sql) y otro para establecer el enlace de replicación mediante la instrucción CHANGE MASTER (3setup_slave.sql).
Cree un archivo de texto 1reset_master.sql y agregue la siguiente línea:
RESET MASTER;
Cree un archivo de texto 3setup_slave.sql y agregue las siguientes líneas:
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 es igual a 6 horas (en segundos). Luego copie el archivo de copia de seguridad tomado de nuestro maestro (que ha sido transferido a nuestro host Docker) al directorio "sql" y renómbrelo como 2mysqldump_complete.sql.gz:
$ cp ~/mysqldump_complete.tar.gz /storage/mysql-slave-6h/sql/2mysqldump_complete.tar.gz
El aspecto final de nuestro directorio "sql" debería ser algo como esto:
$ pwd
/storage/mysql-slave-6h/sql
$ ls -1
1reset_master.sql
2mysqldump_complete.sql.gz
3setup_slave.sql
Tenga en cuenta que anteponemos el nombre del archivo SQL con un número entero para determinar el orden de ejecución cuando Docker inicializa el contenedor MySQL.
Una vez que todo esté en su lugar, ejecute el contenedor MySQL para nuestro esclavo retrasado de 6 horas:
$ 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
** El valor de MYSQL_ROOT_PASSWORD debe ser el mismo que la contraseña raíz de MySQL en el maestro.
Las siguientes líneas son las que buscamos para verificar si MySQL se está ejecutando correctamente y conectado como esclavo a nuestro maestro (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
Luego puede verificar el estado de la replicación con la siguiente declaración:
$ 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
...
En este punto, nuestros contenedores esclavos retrasados de 5 minutos, 1 hora y 6 horas se replican correctamente y nuestra arquitectura se parece a esto:
Escenario de recuperación ante desastres
Digamos que un usuario accidentalmente dejó caer una columna incorrecta en una tabla grande. Considere que la siguiente declaración se ejecutó en el maestro:
mysql> USE shop;
mysql> ALTER TABLE settings DROP COLUMN status;
Si tiene la suerte de darse cuenta de inmediato, puede usar el esclavo retrasado de 15 minutos para ponerse al día antes de que ocurra el desastre y promoverlo para que se convierta en maestro, o exportar los datos faltantes y restaurarlos en el maestro.
En primer lugar, tenemos que encontrar la posición del registro binario antes de que ocurriera el desastre. Toma el tiempo ahora() en el maestro:
mysql> SELECT now();
+---------------------+
| now() |
+---------------------+
| 2018-12-04 14:55:41 |
+---------------------+
Luego, obtenga el archivo de registro binario activo en el maestro:
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 |
+---------------+----------+--------------+------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
Usando el mismo formato de fecha, extraiga la información que queramos del registro binario, binlog.000004. Estimamos la hora de inicio para leer del binlog hace unos 20 minutos (2018-12-04 14:35:00) y filtramos la salida para mostrar 25 líneas antes de la declaración "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
En las pocas líneas inferiores de la salida de mysqlbinlog, debe tener el comando erróneo que se ejecutó en la posición 19379556. La posición que debemos restaurar es un paso antes de esto, que está en la posición 19379491. Esta es la posición binlog donde queremos que nuestro esclavo retrasado para estar hasta.
Luego, en el esclavo retrasado elegido, detenga el esclavo de replicación retrasada y vuelva a iniciar el esclavo hasta una posición final fija que descubrimos anteriormente:
$ 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;
Supervise el estado de replicación y espere hasta que Exec_Master_Log_Pos sea igual al valor de Until_Log_Pos. Esto podría tomar algún tiempo. Una vez que te hayas puesto al día, deberías ver lo siguiente:
$ 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
...
Finalmente verifique si los datos faltantes que estábamos buscando están ahí (la columna "estado" todavía existe):
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 | |
+--------+------------------+------+-----+---------+----------------+
Luego exporte la tabla desde nuestro contenedor esclavo y transfiérala al servidor maestro:
$ docker exec -it mysql-slave-1h mysqldump -uroot -ppassword --single-transaction shop settings > shop_settings.sql
Suelte la tabla problemática y restáurela en el maestro:
$ mysql -uroot -p -e 'DROP TABLE shop.settings'
$ mysqldump -uroot -p -e shop < shop_setttings.sql
Ahora hemos recuperado nuestra mesa a su estado original antes del evento desastroso. Para resumir, la replicación retrasada se puede utilizar para varios propósitos:
- Para proteger contra errores de usuario en el maestro. Un DBA puede revertir un esclavo retrasado al momento justo antes del desastre.
- Para probar cómo se comporta el sistema cuando hay un retraso. Por ejemplo, en una aplicación, un retraso puede deberse a una gran carga en el esclavo. Sin embargo, puede ser difícil generar este nivel de carga. La replicación retrasada puede simular el retraso sin tener que simular la carga. También se puede usar para depurar condiciones relacionadas con un esclavo retrasado.
- Para inspeccionar cómo se veía la base de datos en el pasado, sin tener que volver a cargar una copia de seguridad. Por ejemplo, si el retraso es de una semana y el DBA necesita ver cómo se veía la base de datos antes de los últimos días de desarrollo, se puede inspeccionar el esclavo retrasado.
Reflexiones finales
Con Docker, se pueden ejecutar de manera eficiente varias instancias de MySQL en un mismo host físico. Puede usar herramientas de orquestación de Docker como Docker Compose y Swarm para simplificar la implementación de varios contenedores en lugar de los pasos que se muestran en esta publicación de blog.