La forma más simple de mover una base de datos es volcarla, copiarla e importarla, pero la aplicación tiene que estar parada todo ese tiempo, que con decenas de gigabytes puede ser más de una hora. Con replicación, el servidor nuevo recibe primero una copia completa y después aplica en tiempo real los cambios del antiguo, así que el corte se reduce a los segundos que tardas en apuntar la aplicación al servidor nuevo. En este tutorial migrarás una base de datos MySQL 8.0 con replicación por binlog y una PostgreSQL 16 con replicación lógica, ambas a Ubuntu 24.04, y verificarás los datos antes del cambio.
Requisitos previos
- Servidor de origen (
old_db_ip): el que tiene hoy la base de datos, con MySQL 8.0 o PostgreSQL 16. - Servidor de destino (
new_db_ip): Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con el mismo motor instalado (sudo apt install mysql-serverosudo apt install postgresql). Usa la misma versión mayor que el origen o una más reciente. - Un usuario no root con
sudoen ambos servidores y conectividad entre ellos por red, preferiblemente privada, en el puerto 3306 (MySQL) o 5432 (PostgreSQL). - Espacio libre en el destino para al menos el doble del tamaño de los datos.
- La base de datos se llama
your_dben los ejemplos. Sigue la sección del motor que uses.
Mira el tamaño de la base de datos para elegir método:
sudo mysql -e "SELECT table_schema, ROUND(SUM(data_length+index_length)/1024/1024) AS MB FROM information_schema.tables GROUP BY table_schema"
sudo -u postgres psql -c "SELECT datname, pg_size_pretty(pg_database_size(datname)) FROM pg_database"
| Método | Corte de servicio | Cuándo usarlo |
|---|---|---|
| Volcado e importación | Todo el tiempo de volcado, copia e importación | Bases pequeñas (unos pocos GB) o si puedes permitirte una ventana de mantenimiento |
| Replicación | Segundos | Bases grandes o servicios que no pueden parar |
Notacon el método de volcado basta con parar la aplicación, ejecutar
mysqldump --single-transaction --routines --triggers --eventsopg_dump -Fc, copiar el archivo e importarlo. El resto de esta guía describe la migración con replicación.
Parte A: MySQL 8.0
Paso 1: Preparar el servidor de origen
MySQL 8.0 activa el log binario por defecto, que es lo que usa la replicación. Compruébalo en el origen:
sudo mysql -e "SELECT @@log_bin, @@server_id, @@bind_address"
+-----------+-------------+----------------+
| @@log_bin | @@server_id | @@bind_address |
+-----------+-------------+----------------+
| 1 | 1 | 127.0.0.1 |
+-----------+-------------+----------------+
En Ubuntu, MySQL solo escucha en localhost. Para que el destino pueda conectarse, edita la configuración:
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
Cambia bind-address para que incluya la IP por la que llegará el destino:
bind-address = 127.0.0.1,old_db_ip
Este cambio exige reiniciar MySQL, lo que supone unos segundos de corte. Hazlo en un momento de poco tráfico:
sudo systemctl restart mysql
sudo ss -ltnp | grep 3306
Permite el puerto 3306 solo desde el servidor de destino:
sudo ufw allow from new_db_ip to any port 3306 proto tcp
Crea el usuario de replicación. Usa una contraseña fuerte en lugar de repl_password:
sudo mysql
CREATE USER 'repl'@'new_db_ip' IDENTIFIED BY 'repl_password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'new_db_ip';
EXIT;
Paso 2: Volcar los datos con la posición del binlog
--source-data=2 añade al volcado, como comentario, el archivo y la posición del binlog exactos en los que se tomó la copia. La réplica empezará a leer cambios desde ese punto, así que no se pierde ni se duplica nada. --single-transaction hace la copia coherente sin bloquear las tablas InnoDB, salvo un bloqueo global muy breve al inicio para leer la posición:
sudo mysqldump --single-transaction --source-data=2 --routines --triggers --events \
--databases your_db | gzip > ~/your_db.sql.gz
Consulta la posición guardada:
zcat ~/your_db.sql.gz | grep -m1 -E 'CHANGE (REPLICATION SOURCE|MASTER) TO'
-- CHANGE REPLICATION SOURCE TO SOURCE_LOG_FILE='binlog.000004', SOURCE_LOG_POS=1257;
Anota el archivo y la posición. Copia el volcado al servidor de destino:
scp ~/your_db.sql.gz your_user@new_db_ip:~/
Paso 3: Configurar el servidor de destino como réplica
Cada servidor de una replicación necesita un server-id distinto. En el destino, edita la configuración y añade estas líneas al final de la sección [mysqld]:
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
server-id = 2
read_only = ON
read_only evita que la aplicación o un usuario escriban por error en la réplica; el propio proceso de replicación no se ve afectado. Reinicia e importa el volcado. Como se creó con --databases, incluye la sentencia CREATE DATABASE:
sudo systemctl restart mysql
zcat ~/your_db.sql.gz | sudo mysql
Crea en el destino los usuarios de la aplicación. --databases no copia la tabla de usuarios, así que tienes que crearlos con sus mismos permisos. Puedes ver los que tiene el origen con SHOW GRANTS FOR 'your_db_user'@'host';.
Conecta la réplica al origen usando el archivo y la posición del paso anterior. GET_SOURCE_PUBLIC_KEY=1 es necesario porque MySQL 8 usa por defecto caching_sha2_password y la conexión no va cifrada con TLS:
sudo mysql
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='old_db_ip',
SOURCE_USER='repl',
SOURCE_PASSWORD='repl_password',
SOURCE_LOG_FILE='binlog.000004',
SOURCE_LOG_POS=1257,
GET_SOURCE_PUBLIC_KEY=1;
START REPLICA;
Comprueba el estado:
SHOW REPLICA STATUS\G
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 0
Last_IO_Error:
Last_SQL_Error:
Los dos hilos deben estar en Yes. Si Seconds_Behind_Source es alto, la réplica está recuperando los cambios acumulados durante la importación; espera a que baje a 0.
Paso 4: Verificar los datos
Con la réplica al día, compara las tablas más importantes. CHECKSUM TABLE calcula una suma del contenido; ejecútalo en ambos servidores y compara los resultados:
CHECKSUM TABLE your_db.orders, your_db.users;
+----------------+------------+
| Table | Checksum |
+----------------+------------+
| your_db.orders | 2863184502 |
| your_db.users | 917552311 |
+----------------+------------+
En tablas que reciben escrituras continuamente los valores pueden diferir durante unos instantes; repite la comprobación o hazla durante el cambio del paso 5, cuando el origen ya no acepta escrituras.
Paso 5: Cambiar la aplicación al servidor nuevo
Este es el único momento con impacto, y dura segundos. Prepara de antemano la configuración de la aplicación con la nueva IP para aplicarla rápido.
En el origen, bloquea las escrituras. super_read_only impide escribir incluso a usuarios con privilegios de administrador:
SET GLOBAL super_read_only = ON;
SHOW MASTER STATUS;
+---------------+----------+
| File | Position |
+---------------+----------+
| binlog.000004 | 982231 |
+---------------+----------+
En MySQL 8.4 y posteriores, el comando equivalente es SHOW BINARY LOG STATUS. En el destino, espera a que la réplica haya ejecutado hasta esa misma posición:
SHOW REPLICA STATUS\G
Relay_Source_Log_File: binlog.000004
Exec_Source_Log_Pos: 982231
Cuando coincidan, desconecta la réplica y permite escrituras:
STOP REPLICA;
RESET REPLICA ALL;
SET GLOBAL read_only = OFF;
Quita también la línea read_only = ON de /etc/mysql/mysql.conf.d/mysqld.cnf en el destino para que no se active en el próximo reinicio. Ahora apunta la aplicación a new_db_ip, reiníciala si es necesario y comprueba en sus logs que escribe correctamente.
Deja el servidor de origen en super_read_only unos días como referencia y para poder comparar datos, y después retíralo tras una última copia de seguridad.
Parte B: PostgreSQL 16
PostgreSQL ofrece replicación lógica, que copia los cambios tabla a tabla y funciona entre versiones mayores distintas (por ejemplo, de PostgreSQL 14 a 16). Tiene dos limitaciones que debes conocer: no replica cambios de esquema (DDL) ni los valores de las secuencias. Evita cambios de esquema durante la migración; las secuencias se sincronizan en el paso de cambio.
Paso 1: Preparar el servidor de origen
Edita la configuración principal del origen. En Ubuntu está en /etc/postgresql/<versión>/main/:
sudo nano /etc/postgresql/16/main/postgresql.conf
listen_addresses = 'localhost,old_db_ip'
wal_level = logical
Permite que el usuario de replicación se conecte a la base de datos desde el destino. La replicación lógica usa una entrada normal con el nombre de la base de datos, no la palabra replication:
sudo nano /etc/postgresql/16/main/pg_hba.conf
host your_db repl new_db_ip/32 scram-sha-256
Cambiar wal_level requiere reiniciar PostgreSQL, unos segundos de corte:
sudo systemctl restart postgresql
sudo -u postgres psql -Atc "SHOW wal_level"
sudo ufw allow from new_db_ip to any port 5432 proto tcp
logical
Crea el usuario de replicación con permiso de lectura sobre las tablas y la publicación, que define qué tablas se replican:
sudo -u postgres psql -d your_db
CREATE ROLE repl WITH LOGIN REPLICATION PASSWORD 'repl_password';
GRANT USAGE ON SCHEMA public TO repl;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO repl;
CREATE PUBLICATION migracion FOR ALL TABLES;
Si usas otros esquemas además de public, repite los GRANT para cada uno.
Para replicar UPDATE y DELETE, cada tabla necesita una clave primaria. Busca las que no la tienen:
SELECT n.nspname, c.relname
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
AND NOT EXISTS (
SELECT 1 FROM pg_constraint p WHERE p.conrelid = c.oid AND p.contype = 'p'
);
Para cada tabla que aparezca, añade una clave primaria o, si no es posible, ejecuta ALTER TABLE nombre_tabla REPLICA IDENTITY FULL; (funciona, pero es más lento en tablas grandes).
Paso 2: Copiar roles y esquema al destino
La replicación lógica necesita que las tablas ya existan en el destino. En el origen, exporta los roles y el esquema (sin datos):
sudo -u postgres pg_dumpall --roles-only -f /tmp/roles.sql
sudo -u postgres pg_dump --schema-only --no-publications --no-subscriptions -d your_db -f /tmp/your_db-schema.sql
En el destino, cópialos y aplícalos:
scp your_user@old_db_ip:/tmp/roles.sql your_user@old_db_ip:/tmp/your_db-schema.sql /tmp/
sudo -u postgres psql -f /tmp/roles.sql
sudo -u postgres createdb -O your_db_owner your_db
sudo -u postgres psql -d your_db -f /tmp/your_db-schema.sql
Sustituye your_db_owner por el propietario de la base de datos en el origen. El error role "postgres" already exists al importar los roles es normal. Borra después los archivos de /tmp de ambos servidores, porque roles.sql contiene los hashes de las contraseñas.
Paso 3: Crear la suscripción
En el destino, crea la suscripción. PostgreSQL copia primero todos los datos existentes y después aplica los cambios en tiempo real:
sudo -u postgres psql -d your_db
CREATE SUBSCRIPTION migracion
CONNECTION 'host=old_db_ip port=5432 dbname=your_db user=repl password=repl_password'
PUBLICATION migracion;
NOTICE: created replication slot "migracion" on publisher
CREATE SUBSCRIPTION
Sigue el progreso de la copia inicial. Cada tabla pasa por varios estados hasta r (ready):
SELECT srrelid::regclass AS tabla, srsubstate FROM pg_subscription_rel ORDER BY 2;
tabla | srsubstate
-------------+------------
orders | r
users | r
Cuando todas estén en r, compara el número de filas de las tablas principales en ambos servidores:
SELECT count(*) FROM orders;
Paso 4: Cambiar la aplicación al servidor nuevo
Detén la aplicación o ponla en modo mantenimiento para que no haya escrituras en el origen. Comprueba en el origen que la réplica ha confirmado todo el WAL. Las dos columnas deben coincidir:
sudo -u postgres psql -Atc "SELECT confirmed_flush_lsn, pg_current_wal_lsn() FROM pg_replication_slots WHERE slot_name = 'migracion'"
0/5A3F2C8|0/5A3F2C8
Las secuencias no se replican, así que en el destino siguen en su valor inicial y los siguientes INSERT fallarían por clave duplicada. Genera en el origen los comandos setval con los valores actuales:
sudo -u postgres psql -d your_db -Atc "SELECT format('SELECT setval(%L, %s, true);', quote_ident(schemaname) || '.' || quote_ident(sequencename), last_value) FROM pg_sequences WHERE last_value IS NOT NULL" > /tmp/sequences.sql
Cópialo al destino y aplícalo:
scp your_user@old_db_ip:/tmp/sequences.sql /tmp/
sudo -u postgres psql -d your_db -f /tmp/sequences.sql
Elimina la suscripción en el destino. Esto también borra el slot de replicación del origen, que de lo contrario seguiría acumulando WAL y llenaría el disco:
sudo -u postgres psql -d your_db -c "DROP SUBSCRIPTION migracion"
Apunta la aplicación a new_db_ip, arráncala y comprueba que puede leer y escribir. Por último, en el origen, elimina la publicación:
sudo -u postgres psql -d your_db -c "DROP PUBLICATION migracion"
Plan de vuelta atrás
Mientras no hayas escrito en el servidor nuevo, volver atrás consiste en revertir la configuración de la aplicación y desbloquear el origen (SET GLOBAL super_read_only = OFF; en MySQL, o arrancar de nuevo la aplicación en PostgreSQL). Si el servidor nuevo ya ha recibido escrituras, tendrías que copiarlas al origen, así que decide si vuelves atrás en los primeros minutos tras el cambio. Conserva el servidor de origen intacto hasta que la aplicación lleve varios días estable en el nuevo.
Solución de problemas
- MySQL:
Replica_IO_Running: ConnectingconLast_IO_Errorde autenticación: faltaGET_SOURCE_PUBLIC_KEY=1, la contraseña no coincide o el usuarioreplse creó con otra IP de origen. Prueba la conexión desde el destino conmysql -h old_db_ip -u repl -p --get-server-public-key. - MySQL:
Replica_SQL_Running: Nocon error 1062 (entrada duplicada): la posición del binlog usada no corresponde al volcado importado. Repite el volcado y la importación con la posición correcta. - PostgreSQL:
could not connect to the publisher: revisalisten_addresses, la línea depg_hba.conf(y recárgalo consudo systemctl reload postgresql) y el firewall del origen. - PostgreSQL:
permission denied for tabledurante la copia inicial: al usuarioreplle faltaSELECTsobre alguna tabla o esquema. - PostgreSQL:
duplicate key value violates unique constrainttras el cambio: no se sincronizaron las secuencias. Repite la generación y aplicación desetval.
Conclusión
Has migrado la base de datos a un servidor nuevo manteniéndola sincronizada con el origen mientras verificabas los datos, y has limitado el corte a los segundos de bloquear escrituras y cambiar la conexión de la aplicación. Como siguientes pasos, configura copias de seguridad automáticas en el servidor nuevo, prueba una restauración y, si el servidor migrado es solo la base de datos de un sitio web, consulta la guía de migración de sitios web sin downtime para mover también los archivos.
