--- title: "Sincronizar datos durante una migración de servidor" description: "Mantén archivos y bases de datos sincronizados entre el servidor antiguo y el nuevo con lsyncd y replicación MySQL o PostgreSQL para migrar con un corte mínimo." category: "Migración de Servidores" language: es source: https://cubepath.com/es/docs/migración-de-servidores/sincronizacion-durante-migracion updated: 2026-09-25 --- # Sincronizar datos durante una migración de servidor Si copias los datos una sola vez, todo lo que cambie en el origen después de esa copia se pierde o te obliga a una ventana de corte larga. La solución es mantener el servidor nuevo sincronizado con el antiguo mientras este sigue en producción, de modo que el corte se reduzca a detener las escrituras, esperar unos segundos y cambiar el DNS. En este tutorial sincronizarás de forma continua un directorio de archivos con lsyncd y una base de datos con replicación nativa de MySQL o PostgreSQL entre dos servidores con Ubuntu 24.04, y harás el cambio final de forma ordenada. ## Requisitos previos - Dos servidores con Ubuntu 24.04 LTS: el origen, en producción, y el destino, por ejemplo un VPS de CubePath, con conectividad entre ellos por IP. - Un usuario no root con privilegios `sudo` en ambos. - La misma versión mayor de la base de datos en los dos servidores: MySQL 8.0 o PostgreSQL 16, que son las que incluye Ubuntu 24.04. La replicación física de PostgreSQL no funciona entre versiones mayores distintas. - Una primera copia completa de los archivos ya hecha con `rsync`, o tiempo para hacerla. En esta guía, `old_server_ip` es la IP del origen, `new_server_ip` la del destino, `/var/www/` el directorio de archivos y `app_db` la base de datos. Sustitúyelos por tus valores. ## Qué método usar para cada dato | Tipo de dato | Método | Retraso típico | |---|---|---| | Archivos que cambian poco (código, configuración) | `rsync` repetido a mano | El que tú decidas | | Archivos que cambian continuamente (subidas de usuarios) | lsyncd: inotify + rsync | Unos segundos | | MySQL o MariaDB | Replicación nativa con GTID | Menos de un segundo | | PostgreSQL | Replicación por streaming | Menos de un segundo | No copies los archivos de datos de una base de datos en funcionamiento (`/var/lib/mysql`, `/var/lib/postgresql`) con rsync o lsyncd: la copia sería inconsistente. Las bases de datos se sincronizan siempre con sus propios mecanismos. ## Paso 1: Sincronizar archivos de forma continua con lsyncd lsyncd vigila un directorio con inotify, agrupa los cambios durante unos segundos y lanza rsync solo sobre los archivos modificados. Se ejecuta en el **origen** como servicio de systemd y, como root, necesita acceso SSH como root al destino. En el **origen**, crea una clave SSH para root: ```bash sudo ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N "" sudo cat /root/.ssh/id_ed25519.pub ``` En el **destino**, añade esa clave pública a las claves autorizadas de root: ```bash sudo mkdir -p /root/.ssh sudo nano /root/.ssh/authorized_keys ``` Pega la línea de la clave y guarda. Ubuntu permite por defecto el acceso de root solo con clave (`PermitRootLogin prohibit-password`). Comprueba desde el origen que la conexión funciona y acepta la huella del destino: ```bash sudo ssh root@new_server_ip hostname ``` Instala lsyncd en el **origen** y crea el directorio de logs: ```bash sudo apt update sudo apt install lsyncd sudo mkdir -p /etc/lsyncd /var/log/lsyncd ``` Crea el archivo de configuración. El paquete de Ubuntu lo lee de `/etc/lsyncd/lsyncd.conf.lua`: ```bash sudo nano /etc/lsyncd/lsyncd.conf.lua ``` ```lua settings { logfile = "/var/log/lsyncd/lsyncd.log", statusFile = "/var/log/lsyncd/lsyncd.status", insist = true, } sync { default.rsyncssh, source = "/var/www/", host = "root@new_server_ip", targetdir = "/var/www/", delay = 5, exclude = { "cache/", "*.tmp" }, rsync = { archive = true, compress = false, _extra = { "--numeric-ids" }, }, } ``` - `default.rsyncssh` usa rsync para transferir archivos y SSH para mover y borrar en el destino, lo que evita volver a copiar un archivo solo porque se ha renombrado. - `delay = 5` agrupa los cambios de cinco segundos en una sola ejecución de rsync. - `insist = true` hace que lsyncd arranque aunque el destino no responda y reintente después. - Por defecto lsyncd **borra** en el destino lo que se borra en el origen, igual que `rsync --delete`. Al arrancar, lsyncd hace una sincronización completa del directorio y después solo envía los cambios. Habilita e inicia el servicio: ```bash sudo systemctl enable --now lsyncd sudo systemctl status lsyncd --no-pager ``` Sigue el log mientras se hace la sincronización inicial. Cuando termine aparecerá una línea con `Startup of "/var/www/" finished` y no debe haber líneas con `Error`: ```bash sudo tail -f /var/log/lsyncd/lsyncd.log ``` Para probarlo, crea un archivo en el origen y comprueba que aparece en el destino unos segundos después: ```bash sudo touch /var/www/prueba-lsyncd.txt sleep 10 sudo ssh root@new_server_ip ls -l /var/www/prueba-lsyncd.txt ``` Borra el archivo de prueba en el origen; también desaparecerá del destino. > Nota: cada directorio vigilado consume un "watch" de inotify. En directorios con cientos de miles de subdirectorios, comprueba el límite con `cat /proc/sys/fs/inotify/max_user_watches`. Si el log muestra errores de inotify, súbelo creando `/etc/sysctl.d/90-inotify.conf` con `fs.inotify.max_user_watches = 1048576` y aplícalo con `sudo sysctl --system`. ## Paso 2: Replicar una base de datos MySQL La replicación nativa hace que el destino aplique en tiempo real cada cambio del origen. Con GTID, cada transacción tiene un identificador único y el destino sabe exactamente por dónde continuar. ### Configurar el origen En el **origen**, crea un archivo de configuración para la replicación. Los archivos de `/etc/mysql/mysql.conf.d/` se leen en orden alfabético, así que este se aplica después de `mysqld.cnf`: ```bash sudo nano /etc/mysql/mysql.conf.d/99-replicacion.cnf ``` ```ini [mysqld] server-id = 1 log_bin = /var/log/mysql/mysql-bin.log gtid_mode = ON enforce_gtid_consistency = ON bind-address = 0.0.0.0 ``` > Importante: `enforce_gtid_consistency` rechaza las sentencias que no son seguras para GTID, como actualizar tablas InnoDB y MyISAM en la misma transacción. Si tu aplicación aún usa tablas MyISAM, pruébalo antes en un entorno de pruebas. Reinicia MySQL y permite la conexión al puerto 3306 solo desde el destino: ```bash sudo systemctl restart mysql sudo ufw allow from new_server_ip to any port 3306 proto tcp ``` Crea el usuario de replicación con una contraseña robusta en lugar de `your_strong_password`: ```bash sudo mysql -e "CREATE USER 'repl'@'new_server_ip' IDENTIFIED BY 'your_strong_password'; GRANT REPLICATION SLAVE ON *.* TO 'repl'@'new_server_ip';" ``` Haz el volcado inicial. `--single-transaction` obtiene una copia consistente sin bloquear las tablas InnoDB, y `--set-gtid-purged=ON` incluye la posición GTID desde la que debe continuar la réplica: ```bash sudo mysqldump --single-transaction --routines --triggers --events \ --set-gtid-purged=ON --databases app_db > app_db.sql ``` Copia el volcado al destino con `scp` o `rsync`. ### Configurar la réplica En el **destino**, crea la configuración con otro `server-id` y en modo solo lectura, para que nadie escriba en la réplica por error: ```bash sudo nano /etc/mysql/mysql.conf.d/99-replicacion.cnf ``` ```ini [mysqld] server-id = 2 log_bin = /var/log/mysql/mysql-bin.log gtid_mode = ON enforce_gtid_consistency = ON read_only = ON ``` Reinicia MySQL, vacía el historial GTID local (en un servidor recién instalado no contiene nada que necesites) e importa el volcado: ```bash sudo systemctl restart mysql sudo mysql -e "RESET MASTER;" sudo mysql < app_db.sql ``` En MySQL 8.4 o posterior, `RESET MASTER` se sustituye por `RESET BINARY LOGS AND GTIDS`. El volcado solo contiene la base de datos: crea también en el destino el usuario de la aplicación con los mismos permisos que en el origen. Conecta la réplica al origen e iníciala. `SOURCE_SSL=1` cifra la conexión, necesario para el método de autenticación predeterminado de MySQL 8: ```bash sudo mysql -e "CHANGE REPLICATION SOURCE TO SOURCE_HOST='old_server_ip', SOURCE_USER='repl', SOURCE_PASSWORD='your_strong_password', SOURCE_AUTO_POSITION=1, SOURCE_SSL=1; START REPLICA;" ``` Comprueba el estado de la replicación: ```bash sudo mysql -e "SHOW REPLICA STATUS\G" | grep -E 'Replica_IO_Running|Replica_SQL_Running:|Seconds_Behind_Source|Last_.*Error:' ``` ```output Replica_IO_Running: Yes Replica_SQL_Running: Yes Seconds_Behind_Source: 0 Last_IO_Error: Last_SQL_Error: ``` Las dos primeras deben ser `Yes`. Si `Last_IO_Error` muestra un error de conexión, revisa el cortafuegos y el `bind-address` del origen. ## Paso 3: Replicar una base de datos PostgreSQL La replicación por streaming envía al destino el registro de transacciones (WAL) del origen, de modo que el destino es una copia exacta de todo el clúster, con todas las bases de datos y usuarios. ### Configurar el origen En el **origen**, crea el rol de replicación: ```bash sudo -u postgres psql -c "CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'your_strong_password';" ``` Haz que PostgreSQL escuche en la IP pública. Edita el archivo de configuración: ```bash sudo nano /etc/postgresql/16/main/postgresql.conf ``` ```ini listen_addresses = 'localhost,old_server_ip' ``` Los valores predeterminados de PostgreSQL 16 (`wal_level = replica`, `max_wal_senders = 10`) ya permiten la replicación. Autoriza la conexión del destino al final de `pg_hba.conf`: ```bash sudo nano /etc/postgresql/16/main/pg_hba.conf ``` ```text host replication replicator new_server_ip/32 scram-sha-256 ``` Reinicia PostgreSQL y abre el puerto 5432 solo al destino: ```bash sudo systemctl restart postgresql sudo ufw allow from new_server_ip to any port 5432 proto tcp ``` ### Crear la réplica En el **destino**, detén PostgreSQL y aparta el directorio de datos vacío de la instalación: ```bash sudo systemctl stop postgresql sudo mv /var/lib/postgresql/16/main /var/lib/postgresql/16/main.orig ``` Clona el origen con `pg_basebackup`. `-R` deja la réplica configurada para conectarse al origen, y `-C -S migracion` crea un slot de replicación para que el origen conserve el WAL que la réplica aún no ha recibido: ```bash sudo -u postgres pg_basebackup -h old_server_ip -U replicator \ -D /var/lib/postgresql/16/main -X stream -C -S migracion -R -P -W ``` Introduce la contraseña del rol `replicator` cuando la pida. Al terminar, arranca PostgreSQL: ```bash sudo systemctl start postgresql ``` Comprueba en el destino que está en modo réplica: ```bash sudo -u postgres psql -c "SELECT pg_is_in_recovery();" ``` ```output pg_is_in_recovery ------------------- t ``` Y en el origen, que la réplica está conectada y al día: ```bash sudo -u postgres psql -c "SELECT client_addr, state, sent_lsn, replay_lsn FROM pg_stat_replication;" ``` ```output client_addr | state | sent_lsn | replay_lsn ---------------+-----------+------------+------------ new_server_ip | streaming | 0/3000148 | 0/3000148 ``` `state` debe ser `streaming`, y `sent_lsn` y `replay_lsn` iguales o muy próximos. > Advertencia: mientras exista el slot `migracion`, el origen guardará todo el WAL que la réplica no haya confirmado. Si detienes la réplica durante días, el disco del origen puede llenarse. Elimina el slot en cuanto termines la migración (paso 4). ## Paso 4: Hacer el corte final Con archivos y base de datos sincronizados, el corte dura lo que tardes en seguir estos pasos. Antes, baja a 300 segundos el TTL de los registros DNS que vas a cambiar. 1. **Detén las escrituras en el origen**: activa el modo de mantenimiento de la aplicación o detén sus servicios y el cron (`sudo systemctl stop cron`). 2. **Vacía la cola de lsyncd.** Espera unos segundos a que el log no muestre actividad, detén el servicio y lanza una última pasada de rsync para confirmar que no queda nada pendiente. Con `--itemize-changes` verás cada archivo que aún se transfiera: ```bash sudo systemctl stop lsyncd sudo rsync -aHAX --numeric-ids --delete --itemize-changes \ --exclude='cache/' --exclude='*.tmp' \ /var/www/ root@new_server_ip:/var/www/ ``` Usa las mismas exclusiones que en la configuración de lsyncd para no borrar ni copiar lo que dejaste fuera. 3. **Promociona la base de datos.** Con MySQL, pon el origen en solo lectura: ```bash sudo mysql -e "SET GLOBAL super_read_only = ON;" ``` Compara el GTID ejecutado en ambos servidores con `sudo mysql -e "SELECT @@GLOBAL.gtid_executed;"`. Cuando coincidan, desconecta la réplica y permite escrituras en el destino: ```bash sudo mysql -e "STOP REPLICA; RESET REPLICA ALL; SET GLOBAL read_only = OFF;" ``` Quita también la línea `read_only = ON` de `99-replicacion.cnf` en el destino para que no vuelva a activarse al reiniciar. Con PostgreSQL, detén el origen para que no acepte más escrituras y promociona el destino: ```bash sudo systemctl stop postgresql ``` ```bash sudo -u postgres psql -c "SELECT pg_promote();" ``` El primer comando va en el origen y el segundo en el destino. `pg_promote()` devuelve `t` y, a partir de ese momento, `SELECT pg_is_in_recovery();` devuelve `f`. 4. **Arranca la aplicación en el destino** apuntando a la base de datos local y pruébala contra la IP nueva con tu archivo `hosts` local. 5. **Cambia el DNS** a `new_server_ip` y verifica la propagación con `dig +short A your_domain @1.1.1.1`. Las sesiones de usuario guardadas en archivos locales o en la memoria del servidor antiguo se perderán en el cambio y los usuarios tendrán que volver a iniciar sesión. Si eso no es aceptable, guárdalas en la base de datos o en un Redis compartido antes de migrar. ## Paso 5: Verificar y limpiar Comprueba que el número de archivos coincide en ambos servidores: ```bash sudo find /var/www -type f | wc -l sudo ssh root@new_server_ip 'find /var/www -type f | wc -l' ``` Para la base de datos, compara el número de filas de las tablas principales en el origen (ya en solo lectura o detenido, puedes arrancarlo solo para consultar) y en el destino, por ejemplo con `SELECT COUNT(*) FROM orders;`. Cuando confirmes que el destino funciona, limpia todo lo que se creó para la sincronización: - Elimina el slot de replicación de PostgreSQL en el origen, si vuelves a arrancarlo: `sudo -u postgres psql -c "SELECT pg_drop_replication_slot('migracion');"`. - Borra el usuario `repl` de MySQL o el rol `replicator` de PostgreSQL y cierra el puerto de la base de datos en el cortafuegos del origen (`sudo ufw status numbered` y `sudo ufw delete `). - Deshabilita lsyncd en el origen con `sudo systemctl disable lsyncd`. - Quita la clave del origen de `/root/.ssh/authorized_keys` en el destino. ## Solución de problemas - **lsyncd se detiene con `Error: Terminating since out of inotify watches`**: sube `fs.inotify.max_user_watches` como se indica en el paso 1 y reinicia el servicio. - **`Replica_IO_Running: Connecting` en MySQL**: el destino no alcanza el puerto 3306 del origen. Comprueba `bind-address`, el cortafuegos y prueba con `nc -vz old_server_ip 3306` desde el destino. - **`Last_SQL_Error` con un error de clave duplicada**: alguien escribió en la réplica o el volcado se importó sobre datos existentes. Vuelve a crear la réplica desde un volcado nuevo. - **`pg_basebackup: error: connection ... no pg_hba.conf entry for replication connection`**: la línea de `pg_hba.conf` no coincide con la IP de origen de la conexión o falta reiniciar PostgreSQL. ## Conclusión Has mantenido un servidor nuevo sincronizado con el de producción: los archivos con lsyncd y la base de datos con replicación nativa de MySQL o PostgreSQL, y has reducido el corte a detener escrituras, promocionar la réplica y cambiar el DNS. Como siguientes pasos, ensaya el corte completo en un entorno de pruebas para cronometrarlo, y guarda un plan de vuelta atrás que indique cómo volver a sincronizar en sentido inverso si tuvieras que regresar al servidor antiguo.