El retardo de replicación es el impuesto silencioso al “escalado”. Añades una réplica de solo lectura, los paneles parecen más tranquilos y luego el informe del CEO muestra los números de ayer. O peor: tu aplicación lee desde una réplica, ve un estado obsoleto y ejecuta algo entusiasta e irreversible.
El lag raramente es “un problema de la base de datos”. Es un problema de colas con aristas afiladas: CPU, I/O, contención de locks, red, configuración, esquema y forma de la carga de trabajo toman turnos siendo el villano. El truco es encontrar cuál sostiene el cuchillo—rápido—y aplicar una solución que no rebote la semana siguiente.
Qué es realmente el retraso de replicación (y qué no es)
El retraso de replicación es una brecha entre el commit en el primario y la visibilidad en la réplica. Suena obvio hasta que te das cuenta de que hay al menos tres “brechas” escondidas bajo la misma palabra:
- Retraso de transporte: el primario generó cambios, pero la réplica aún no los ha recibido (red, limitación, ráfaga).
- Retraso de reproducción/aplicación: la réplica recibió los cambios pero no los ha aplicado (CPU, I/O, contención, aplicación mono-hilo, conflicto).
- Retraso de visibilidad: los cambios están aplicados pero no son visibles para una consulta/sesión concreta debido a reglas de snapshot (común con consultas de larga duración, especialmente en Postgres).
Si tratas todo esto como una sola métrica, “arreglarás” lo incorrecto. El lag siempre es una cola. Las colas tienen dos curas: aumentar la tasa de servicio, o reducir la tasa de llegada. Todo lo demás es adorno.
Dos definiciones que debes imponer internamente
Retraso operativo: “¿Cuánto tarda hasta que un write confirmado en el primario es consultable en la réplica?” Esto es lo que importan las apps.
Salud del pipeline de replicación: “¿Qué tan cerca está la réplica de ingerir y reproducir la corriente de cambios del primario?” Esto es lo que vigilan los SRE.
Hechos interesantes y contexto histórico (porque la historia evita repetirla)
- La replicación de MySQL empezó basada en sentencias, lo que hizo que “determinista” fuera una plegaria, no una garantía. El logging por filas se volvió el valor sensato por defecto después.
- La replicación por streaming integrada de PostgreSQL (WAL físico) llegó en la era 9.x y cambió “standby” de “restaurar desde archivo” a “casi tiempo real”.
- La replicación semisíncrona de MySQL se introdujo para reducir el riesgo de pérdida de datos, pero puede intercambiar latencia por durabilidad—tus SLOs lo notarán.
- GTID en MySQL hizo que el failover y los cambios de topología fueran menos propensos a errores, pero también facilitó construir automatizaciones peligrosamente confiadas.
- El hot standby de Postgres permitió consultas de solo lectura en réplicas, pero también introdujo comportamiento de conflicto de consultas que parece “cancelaciones aleatorias” si no lo planificas.
- La replicación lógica en Postgres llegó después que el streaming físico y no es “físico pero mejor”; es una herramienta distinta con modos de falla distintos.
- La replicación paralela en MySQL evolucionó con las versiones: implementaciones tempranas eran limitadas, las posteriores mejoraron la paralelización. Tu versión importa.
- Las métricas de lag de las réplicas mienten por omisión: “seconds behind master” en MySQL no es una verdad universal; es una estimación basada en timestamps con puntos ciegos.
Una cita que vale la pena poner en la pared, porque evita tonterías heroicas durante incidentes:
“La esperanza no es una estrategia.” — Vince Lombardi
El retraso de replicación es donde la esperanza va a morir. Bien. Ahora puedes diseñar ingeniería.
Cómo MySQL crea retraso en la replicación (y cómo se recupera)
La replicación clásica de MySQL es engañosamente simple: el primario escribe binlogs, las réplicas los obtienen y los aplican. El diablo está en cómo los aplican.
El pipeline de replicación de MySQL (vista práctica)
- Primario genera eventos de binlog (basado en sentencias o por fila; en producción normalmente se quiere por fila).
- Hilo I/O en la réplica lee eventos del primario y los escribe en relay logs.
- Hilo(s) SQL aplican los eventos del relay log a los datos de la réplica.
El lag ocurre cuando los relay logs crecen más rápido de lo que los hilos SQL pueden aplicar. Eso es todo. El resto son razones de por qué sucedió.
Formas comunes del lag en MySQL
- Transacción grande única: la réplica parece “atascada” y luego de repente se pone al día. Tu binlog está bien; la aplicación está ocupada procesando.
- Muchas transacciones pequeñas: la réplica se va quedando atrás lentamente en hora pico y nunca se recupera. Necesitas más throughput de aplicación o menos escrituras.
- Contención de locks en la réplica: consultas de lectura en la réplica bloquean la aplicación (o viceversa), produciendo un gráfico de lag en dientes de sierra.
Una verdad seca: en MySQL, la réplica no es tu espejo pasivo. Es un segundo servidor de base de datos que hace trabajo real, compitiendo por CPU, I/O y buffer pool.
Cómo PostgreSQL crea retraso en la replicación (y cómo se recupera)
La replicación por streaming de PostgreSQL es WAL enviado por una conexión persistente. Las réplicas (standbys) reciben registros WAL y los reproducen. Con hot standby, también sirven consultas de solo lectura.
El pipeline de replicación de Postgres (streaming físico)
- Primario escribe registros WAL por los cambios.
- WAL sender transmite WAL a las réplicas.
- WAL receiver en la réplica escribe el WAL recibido al disco.
- Startup/replay process reproduce el WAL para hacer que los archivos de datos sean consistentes con la línea de tiempo del primario.
El lag ocurre cuando el proceso de replay no puede mantenerse al día, o cuando se ve obligado a pausar/ralentizar por conflictos o limitaciones de recursos.
Realidades específicas del lag en Postgres
- La reproducción puede bloquearse por conflictos de recovery (hot standby): las consultas de larga duración pueden impedir la limpieza por vacuum en el primario, y en la réplica pueden cancelarse o causar retraso dependiendo de la configuración.
- Los picos en el volumen WAL pueden ser causados por actualizaciones masivas, reconstrucciones de índices o reescrituras completas de tablas; esto es más “física” que “tuning”.
- Las replication slots impiden la eliminación de WAL; si una réplica está caída o lenta, el WAL se acumula en el primario hasta que tu disco entra en pánico.
Segundo chiste (y último): el retraso de replicación es como una reunión corporativa—si una persona insiste en hablar para siempre, todos los demás se retrasan.
Guion de diagnóstico rápido
Este es el orden que encuentra el cuello de botella más rápido en sistemas reales. No improvises. Tu intuición no es observable.
Primero: clasifica el tipo de lag
- ¿Retraso de transporte? El primario genera cambios y la réplica no los recibe con suficiente rapidez.
- ¿Retraso de aplicación/reproducción? La réplica tiene los datos (relay/WAL recibido), pero la reproducción está atrasada.
- ¿Retraso de visibilidad? Está aplicado, pero las consultas aún ven snapshots antiguos (transacciones largas, semánticas repeatable read, etc.).
Segundo: decide dónde crece la cola
- MySQL: tamaño del relay log, estado del hilo SQL, estados de los workers de replicación.
- Postgres: LSN recibido vs LSN reproducido, retraso de replay, conflictos, estado del WAL receiver.
Tercero: encuentra la restricción de recursos
- Limitado por I/O: await alto, poca capacidad IOPS, flushing de páginas sucias, presión de WAL/fsync.
- Limitado por CPU: CPU alta, núcleo saturado (a menudo hilo de aplicación), sobrecarga de compresión/descompresión.
- Lock/contención: aplicación esperando locks, DDL, o conflictos de hot standby.
- Limitado por red: pérdida de paquetes, bajo throughput, buffers TCP mal dimensionados, replicación entre regiones.
Cuarto: elige la palanca menos peligrosa
- Prefiere cambios en la carga de trabajo (tamaño de lotes, tamaño de transacciones, índices, patrones de consulta) sobre “perillas mágicas”.
- Prefiere añadir paralelismo donde sea seguro (aplicación paralela en MySQL, más capacidad de I/O) sobre “apagar la durabilidad”.
- Prefiere mover lecturas fuera de la réplica durante la puesta al día (feature flags, circuit breakers) antes que dejar que las réplicas se ahoguen.
Tareas prácticas: comandos, salidas, decisiones
Estos son controles reales y ejecutables que puedes hacer durante un incidente. Cada uno incluye qué significa la salida y qué decisión tomar. Hazlos en este orden a menos que tengas evidencia sólida en contra.
Task 1 (MySQL): Check basic replica status and identify the bottleneck thread
cr0x@server:~$ mysql -e "SHOW SLAVE STATUS\G" | egrep "Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running|Slave_SQL_Running_State|Last_SQL_Error|Last_IO_Error|Relay_Log_Space"
Seconds_Behind_Master: 187
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Slave_SQL_Running_State: Waiting for dependent transaction to commit
Relay_Log_Space: 4294967296
Last_SQL_Error:
Last_IO_Error:
Significado: El hilo I/O está saludable (probablemente el transporte está bien). El hilo SQL está en marcha pero bloqueado por dependencia de commit; los relay logs son enormes. La aplicación es la cola.
Decisión: Inspecciona los ajustes de replicación paralela y el estado de los workers. Si las esperas por dependencias persisten, busca transacciones grandes o cuellos de botella en el orden de commits. Considera escalar I/O/CPU en la réplica y reducir lecturas de larga duración.
Task 2 (MySQL): Inspect replication workers (parallel apply health)
cr0x@server:~$ mysql -e "SELECT WORKER_ID, THREAD_ID, SERVICE_STATE, LAST_APPLIED_TRANSACTION, LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP, APPLYING_TRANSACTION FROM performance_schema.replication_applier_status_by_worker\G"
*************************** 1. row ***************************
WORKER_ID: 1
THREAD_ID: 52
SERVICE_STATE: ON
LAST_APPLIED_TRANSACTION: 0f3a2b1c-11aa-11ee-9d5b-0800272b3f7a:912334
LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP: 2025-12-31 11:58:01.123456
APPLYING_TRANSACTION: 0f3a2b1c-11aa-11ee-9d5b-0800272b3f7a:912390
*************************** 2. row ***************************
WORKER_ID: 2
THREAD_ID: 53
SERVICE_STATE: ON
LAST_APPLIED_TRANSACTION: 0f3a2b1c-11aa-11ee-9d5b-0800272b3f7a:912333
LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP: 2025-12-31 11:58:01.120000
APPLYING_TRANSACTION:
Significado: Algunos workers están inactivos mientras uno está aplicando. Existe paralelismo pero no se está usando eficazmente (la carga no es paralelizables o está bloqueada por orden de commit).
Decisión: Si la carga es sobre una sola fila caliente o un solo objeto de esquema, la aplicación paralela no te salvará. Enfócate en cambios de esquema/carga (shards, reducir contención) o acepta el lag y enruta lecturas de forma distinta.
Task 3 (MySQL): Identify the SQL thread’s current wait (locks vs I/O vs commit)
cr0x@server:~$ mysql -e "SHOW PROCESSLIST" | awk 'NR==1 || $4 ~ /Connect|Binlog Dump|SQL/ {print}'
Id User Host db Command Time State Info
14 system user replication Connect 187 Waiting for relay log NULL
15 system user replication Connect 187 Waiting for dependent transaction to commit NULL
102 app_ro 10.0.2.55:61234 prod Query 32 Sending data SELECT ...
Significado: La aplicación SQL de la réplica está esperando dependencias de commit; una consulta de lectura se está ejecutando durante 32s y puede estar causando churn de buffer o presión de locks.
Decisión: Si las consultas de lectura son pesadas, limítalas/termínalas durante la puesta al día, o muévelas a una réplica dedicada para analytics. Si las esperas por dependencia de commit dominan, revisa límites de group commit y ajustes de aplicación paralela.
Task 4 (MySQL): Confirm binlog format and row image (WAL/binlog volume control)
cr0x@server:~$ mysql -e "SHOW VARIABLES WHERE Variable_name IN ('binlog_format','binlog_row_image','sync_binlog','innodb_flush_log_at_trx_commit')"
+------------------------------+-----------+
| Variable_name | Value |
+------------------------------+-----------+
| binlog_format | ROW |
| binlog_row_image | FULL |
| innodb_flush_log_at_trx_commit | 1 |
| sync_binlog | 1 |
+------------------------------+-----------+
Significado: Ajustes durables (bien) pero potencialmente volumen de binlog pesado si hay tablas anchas con row image FULL.
Decisión: Considera binlog_row_image=MINIMAL solo si has validado herramientas y la seguridad de replicación para tu carga; de lo contrario optimiza el esquema y el tamaño de transacciones primero.
Task 5 (MySQL): Check InnoDB flush pressure on the replica
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty'; SHOW GLOBAL STATUS LIKE 'Innodb_data_pending_fsyncs'; SHOW GLOBAL STATUS LIKE 'Innodb_os_log_pending_fsyncs';"
+--------------------------------+--------+
| Variable_name | Value |
+--------------------------------+--------+
| Innodb_buffer_pool_pages_dirty | 184223 |
+--------------------------------+--------+
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| Innodb_data_pending_fsyncs| 67 |
+---------------------------+-------+
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| Innodb_os_log_pending_fsyncs | 29 |
+---------------------------+-------+
Significado: Páginas sucias y fsyncs pendientes implican que el almacenamiento es el limitador; la aplicación no puede vaciar lo suficientemente rápido de forma segura.
Decisión: Añade IOPS (discos más rápidos, almacenamiento mejor aprovisionado), ajusta el flushing, o reduce la amplificación de escritura (índices, batching de transacciones). No “arregles” esto desactivando fsync a menos que te guste practicar ejercicios de pérdida de datos.
Task 6 (Postgres): Check received vs replayed LSN (transport vs replay)
cr0x@server:~$ psql -x -c "SELECT now() AS ts, pg_last_wal_receive_lsn() AS receive_lsn, pg_last_wal_replay_lsn() AS replay_lsn, pg_wal_lsn_diff(pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn()) AS bytes_pending;"
-[ RECORD 1 ]--+------------------------------
ts | 2025-12-31 12:00:12.1122+00
receive_lsn | 3A/9F1200A0
replay_lsn | 3A/9E8803D8
bytes_pending | 58982400
Significado: La réplica está recibiendo WAL pero está ~56MB por detrás en la reproducción. El transporte está bien; la reproducción es la cola.
Decisión: Revisa CPU/I/O en la réplica y conflictos de hot standby. Si bytes_pending crece en hora pico, necesitas más throughput de replay o menos escrituras que generen mucho WAL.
Task 7 (Postgres): Measure time-based lag as seen by the system
cr0x@server:~$ psql -x -c "SELECT now() AS ts, pg_last_xact_replay_timestamp() AS last_replay_ts, now() - pg_last_xact_replay_timestamp() AS replay_delay;"
-[ RECORD 1 ]---+------------------------------
ts | 2025-12-31 12:00:20.004+00
last_replay_ts | 2025-12-31 11:56:59.771+00
replay_delay | 00:03:20.233
Significado: Aproximadamente 3m20s de retraso de replay. Esto está más cercano a “lo que sienten las aplicaciones” que los bytes.
Decisión: Si tu negocio no tolera esto, enruta lecturas críticas al primario, implementa read-your-writes, o invierte en enfoques síncronos/quórum (con los ojos abiertos respecto a la latencia).
Task 8 (Postgres): Check WAL receiver status and any network symptoms
cr0x@server:~$ psql -x -c "SELECT status, receive_start_lsn, received_lsn, latest_end_lsn, latest_end_time, conninfo FROM pg_stat_wal_receiver;"
-[ RECORD 1 ]---+---------------------------------------------
status | streaming
receive_start_lsn | 3A/9B000000
received_lsn | 3A/9F1200A0
latest_end_lsn | 3A/9F1200A0
latest_end_time | 2025-12-31 12:00:18.882+00
conninfo | host=10.0.1.10 port=5432 user=replicator ...
Significado: El streaming está saludable; latest_end_time está actual. El transporte no es el cuello de botella.
Decisión: Deja de culpar “la red”. Mira las limitaciones de replay: almacenamiento, CPU, conflictos y checkpoints.
Task 9 (Postgres): Detect hot standby conflicts and cancellations
cr0x@server:~$ psql -c "SELECT datname, confl_snapshot, confl_lock, confl_bufferpin, confl_deadlock FROM pg_stat_database_conflicts;"
datname | confl_snapshot | confl_lock | confl_bufferpin | confl_deadlock
-----------+----------------+------------+-----------------+----------------
postgres | 0 | 0 | 0 | 0
prod | 144 | 3 | 12 | 0
Significado: Los conflictos son reales en prod; los conflictos de snapshot son comunes cuando consultas largas en la réplica impiden que replay elimine versiones antiguas de filas.
Decisión: Si valoras datos frescos más que lecturas largas, ajusta para cancelar consultas antes. Si valoras lecturas largas, espera retrasos y construye la lógica de la app en consecuencia. Intentar tener ambos suele acabar con ruido en el pager.
Task 10 (Postgres): Identify long-running queries on the replica causing conflicts or bloat
cr0x@server:~$ psql -x -c "SELECT pid, usename, state, now() - xact_start AS xact_age, wait_event_type, wait_event, query FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_start ASC LIMIT 5;"
-[ RECORD 1 ]----+---------------------------------------------
pid | 21844
usename | app_ro
state | active
xact_age | 00:18:41.772
wait_event_type | Client
wait_event | ClientRead
query | SELECT ... FROM big_table JOIN ...
Significado: Una transacción de 18 minutos en un hot standby es un imán de conflictos y un riesgo de ralentización de la reproducción.
Decisión: Mátala durante la respuesta al incidente, y luego corrige el patrón: aplica statement timeouts, mueve consultas analíticas fuera del hot standby, o usa un warehouse.
Task 11 (System): Check disk latency that makes apply/replay crawl
cr0x@server:~$ iostat -x 1 3
Linux 6.1.0 (db-replica-1) 12/31/2025 _x86_64_ (8 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.11 0.00 7.88 28.42 0.00 51.59
Device r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await svctm %util
nvme0n1 120.0 980.0 6400.0 51200.0 97.4 22.8 21.8 0.9 99.2
Significado: 99% de utilización y 20ms de await no es “aceptable”. Tu réplica está limitada por almacenamiento; la reproducción/aplicación se quedará atrás.
Decisión: Escala el almacenamiento (mejores discos, más IOPS), reduce la amplificación de escrituras (índices, batching), o mueve la réplica a volúmenes separados para WAL/relay logs vs datos cuando sea aplicable.
Task 12 (System): Validate network health (because sometimes it actually is the network)
cr0x@server:~$ mtr -r -c 20 10.0.1.10
Start: 2025-12-31T12:02:00+0000
HOST: db-replica-1 Loss% Snt Last Avg Best Wrst StDev
1.|-- 10.0.2.1 0.0% 20 0.4 0.5 0.3 1.2 0.2
2.|-- 10.0.10.5 0.0% 20 1.1 1.3 1.0 2.4 0.3
3.|-- 10.0.1.10 2.0% 20 2.2 2.4 2.0 6.8 1.1
Significado: 2% de pérdida y picos de jitter pueden crear retraso de transporte y retransmisiones, especialmente bajo streams sostenidos.
Decisión: Si Postgres/MySQL muestra gaps de recepción, arregla el camino (errores de NIC, enlace sobreutilizado, vecino ruidoso). No ajustes la base de datos para compensar pérdida de paquetes. Eso es como bajar tus estándares de contratación porque las sillas chirrían.
Task 13 (Postgres): Check replication slots and WAL retention risk
cr0x@server:~$ psql -x -c "SELECT slot_name, slot_type, active, restart_lsn, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS retained_bytes FROM pg_replication_slots;"
-[ RECORD 1 ]---+------------------------------
slot_name | replica_1
slot_type | physical
active | f
restart_lsn | 3A/12000000
retained_bytes | 107374182400
Significado: El slot está inactivo y reteniendo ~100GB de WAL. Tu disco primario es ahora un temporizador de cuenta regresiva.
Decisión: Decide si esa réplica volverá pronto. Si no, elimina el slot (tras confirmar que no será necesario) o trae la réplica de vuelta y deja que se ponga al día.
Task 14 (MySQL): Check relay log growth rate and disk usage (avoid “replica died” as a side quest)
cr0x@server:~$ du -sh /var/lib/mysql/*relay* 2>/dev/null; df -h /var/lib/mysql
4.2G /var/lib/mysql/mysql-relay-bin.000123
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p1 400G 362G 18G 96% /var/lib/mysql
Significado: Los relay logs son grandes y el disco está casi lleno. Si llega al 100%, MySQL tendrá un muy mal día y arrastrará tu incidente con él.
Decisión: Libera espacio de inmediato (añade volumen, purga de forma segura si es posible, o detén temporalmente consumidores pesados de replicación). Luego arregla la causa raíz: throughput de aplicación y dimensionamiento de disco.
Task 15 (Postgres): See if checkpoints are too frequent (WAL pressure and I/O spikes)
cr0x@server:~$ psql -c "SELECT checkpoints_timed, checkpoints_req, checkpoint_write_time, checkpoint_sync_time, buffers_checkpoint FROM pg_stat_bgwriter;"
checkpoints_timed | checkpoints_req | checkpoint_write_time | checkpoint_sync_time | buffers_checkpoint
------------------+-----------------+-----------------------+----------------------+--------------------
112 | 389 | 9823412 | 4412231 | 18442201
Significado: Muchas checkpoints solicitadas sugiere volumen de WAL o ajustes que fuerzan checkpoints frecuentes; esto puede dejar sin recursos a la reproducción con tormentas de I/O.
Decisión: Ajusta settings de checkpoint y almacenamiento; reduce fuentes de ráfagas WAL (actualizaciones masivas, reconstrucción de índices). Valida con I/O medido y comportamiento de replay, no con intuiciones.
Eso son más de una docena de tareas. Úsalas como checklist, no como buffet.
Causas de lag que realmente importan (por subsistema)
1) Forma de las transacciones: commits grandes, muchos commits y la tiranía de la “fila caliente”
La replicación es fundamentalmente serial en lugares clave. Incluso cuando tienes aplicación paralela, el orden de commit y el seguimiento de dependencias pueden forzar serialización. Tu carga decide si el paralelismo es real o cosmético.
- Transacciones grandes crean lag en ráfagas: la réplica debe aplicar un gran bloque antes de que el “lag” mejore.
- Muchas transacciones pequeñas generan overhead constante: fsync, adquisición de locks, actualizaciones de btree y bookkeeping de commits dominan.
- Filas calientes (contadores, last_seen, cuenta única) crean cadenas de dependencia que la aplicación paralela no puede romper.
Qué hacer: agrupa escrituras intencionalmente, evita actualizaciones “charlatanas” por fila y rediseña hotspots (eventos append-only, contadores shardados, rollups periódicos).
2) Amplificación de escritura: índices, claves foráneas y restricciones “útiles”
Cada escritura no es una sola escritura. Es un pequeño desfile: actualización del heap/páginas, actualizaciones de índices, WAL/binlog, flush de páginas, metadata y a veces lecturas extra para chequear restricciones.
Si las réplicas se atrasan durante periodos de escritura intensa, tu primer sospechoso debería ser la amplificación de escritura, no las perillas de replicación.
3) Física del almacenamiento: la latencia vence al ancho de banda
La aplicación/reproducción de replicación es una carga de trabajo intensiva en escrituras con puntos de sincronización desagradables. La baja latencia importa más que números impresionantes de throughput secuencial.
- Un
awaitalto eniostatsuele correlacionar directamente con crecimiento de lag. - Los volúmenes en la nube con créditos de ráfaga pueden “parecer bien” hasta que dejan de estarlo, entonces el lag sube y nunca se recupera.
- Las cargas mixtas (replicación + lecturas analíticas) pueden destrozar caches y causar amplificación de I/O aleatoria.
4) Tráfico de lectura en réplicas: el falso “almuerzo gratis”
Servir lecturas desde réplicas suena como capacidad gratis. En la práctica es un problema de competencia por recursos:
- Lecturas pesadas causan churn de cache, obligando a más lecturas de disco para la aplicación/reproducción.
- Las lecturas largas en hot standby de Postgres pueden causar conflictos con la reproducción.
- En MySQL, las lecturas compiten por buffer pool y I/O; también los locks de metadata y DDL pueden complicarse.
Regla: una réplica destinada a la frescura no debe ser también tu playground analítico improvisado.
5) Red: latencia, pérdida y fantasías entre regiones
La replicación entre regiones no es un ejercicio de tuning; es física y probabilidad. La pérdida causa retransmisiones; la latencia estira bucles de feedback; el jitter crea micro-rafagas.
Si replicas por internet público (o un VPN sobrecargado), no estás ejecutando una base de datos; estás ejecutando un experimento de redes.
Soluciones específicas para MySQL y sus compensaciones
Replicación paralela: útil, no mágica
MySQL moderno soporta aplicación paralela, pero el grado depende de la versión y ajustes. La carga también debe permitir paralelismo (múltiples esquemas, transacciones independientes).
Hacer: habilita y valida workers paralelos; monitoriza la utilización de workers.
Evitar: “subir workers a 64” sin medir esperas de locks y cadenas de dependencia de commit. Obtendrás overhead y el mismo lag.
Formato de binlog y row image: controla el volumen cuidadosamente
El logging por fila es operativamente más seguro para la mayoría de cargas en producción, pero puede generar mucho binlog—especialmente con filas anchas e row image FULL.
Hacer: reduce actualizaciones innecesarias, evita actualizar columnas sin cambios y mantén filas estrechas cuando importe.
Considerar: binlog_row_image=MINIMAL solo con comprobaciones cuidadosas de compatibilidad (herramientas downstream, auditoría, CDC y tus propias necesidades de depuración).
Esquema y patrones de consulta amigables con réplicas
- Mantén índices secundarios intencionales. Cada índice extra es trabajo extra de aplicación en cada réplica.
- Prefiere escrituras idempotentes y evita bucles read-modify-write que crean hotspots.
- Agrupa jobs en background y evita “commits diminutos para siempre”.
Los ajustes de durabilidad no son un plan de rendimiento
Puedes reducir la presión de fsync con ajustes de durabilidad, pero estás comprando rendimiento con riesgo de pérdida de datos. A veces es aceptable en una tienda tipo cache. ¿Datos críticos? No.
Primer chiste (y el otro): desactivar la durabilidad para arreglar el lag es como quitar el detector de humo porque no te deja dormir.
Soluciones específicas para PostgreSQL y sus compensaciones
Conflictos de hot standby: elige un lado
En un hot standby, la reproducción necesita aplicar cambios que pueden eliminar versiones de filas o bloquear recursos. Las consultas de larga duración pueden entrar en conflicto con esa reproducción.
Postgres te da opciones, no milagros:
- Priorizar la frescura de replay: cancelar consultas en conflicto más pronto. Los usuarios verán fallos de consulta, pero los datos serán más frescos.
- Priorizar estabilidad de lectura: permitir consultas largas, aceptar retrasos de replay. Los usuarios obtienen resultados, pero los datos se quedan obsoletos.
Qué hacer: fija expectativas claras para cada rol de réplica. “Esta réplica es para dashboards, hasta 5 minutos de retraso” es una decisión de producto válida. Pretender que es en tiempo real es cómo los incidentes se convierten en reuniones.
Gestión del volumen WAL: la victoria poco glamorosa
El WAL es la factura por tu comportamiento de escritura. Reduce la factura:
- Evita reescrituras completas de tablas en horas pico.
- Sé cauteloso con actualizaciones masivas que tocan muchas filas; a menudo un INSERT en tabla nueva + swap es operativamente mejor, aunque cambia el patrón de WAL.
- Considera tunear autovacuum y mantenimiento para reducir bloat (que de otro modo aumenta I/O durante la reproducción).
Replication slots: barandillas con dientes
Los slots impiden que el WAL sea eliminado mientras un consumidor aún lo necesita. Genial para replicación lógica y standbys robustos. También genial para llenar discos cuando un consumidor desaparece.
Regla operativa: si usas slots, debes tener alertas sobre el tamaño de WAL retenido y la actividad del slot, y un runbook para “¿es seguro eliminar este slot?”.
Replicación síncrona: cuándo usarla y qué pagas
La replicación síncrona puede acotar el lag haciendo que los commits esperen la confirmación de la réplica. Eso no es “corrección gratis”; es un impuesto de latencia. Úsala para un pequeño conjunto de caminos de datos críticos, o para HA regional donde la consistencia es prioritaria.
Errores comunes: síntoma → causa raíz → arreglo
1) “Seconds behind master” está bajo, pero los usuarios siguen viendo lecturas obsoletas
Síntoma: MySQL reporta bajo lag, pero la app lee datos viejos.
Causa raíz: Estás midiendo lo incorrecto. El lag basado en timestamps puede ser engañoso durante periodos inactivos, o tu app usa read-after-write sin sticky sessions.
Arreglo: Implementa read-your-writes: enruta al primario después de un write, usa stickiness de sesión, o rastrea GTID/LSN y espera hasta que la réplica se ponga al día.
2) El lag crece constantemente cada día, y luego “aleatoriamente” se resetea tras mantenimiento
Síntoma: Las réplicas se vuelven más lentas con el tiempo; reinicios o vacaciones lo “arreglan”.
Causa raíz: Churn de cache, bloat, índices en crecimiento o aumentos de amplificación de escritura. A veces también son créditos de ráfaga de almacenamiento que se agotan.
Arreglo: Mide latencia I/O y comportamiento del buffer, rastrea crecimiento de tablas/índices y planifica almacenamiento con margen. Trata el bloat y la indexación como trabajo de capacidad, no como falla personal.
3) Réplica Postgres cancela consultas y los dashboards se quejan
Síntoma: Aparece “canceling statement due to conflict with recovery”; los usuarios se quejan.
Causa raíz: Conflicto hot standby: lecturas snapshot de larga duración en conflicto con la limpieza/reproducción.
Arreglo: Mueve consultas largas a otro lugar; añade statement_timeout en la réplica; ajusta settings de hot standby según el rol. No pongas cargas BI en una réplica enfocada en frescura.
4) El disco del primario de Postgres se llena con WAL inesperadamente
Síntoma: El directorio WAL crece rápido; presión de disco; el slot de replicación muestra bytes retenidos enormes.
Causa raíz: Consumidor inactivo/lento con un replication slot; el WAL no puede reciclarse.
Arreglo: Restaura al consumidor o elimina el slot tras la verificación. Añade monitorización y una política para el ciclo de vida de los slots.
5) Réplica MySQL está “en ejecución” pero la aplicación nunca se pone al día tras el pico
Síntoma: Hilo I/O ok, hilo SQL ok, el lag sube y se mantiene alto.
Causa raíz: Throughput de aplicación por debajo de la tasa sostenida de escrituras: latencia de almacenamiento, CPU insuficiente, demasiados índices, limitaciones de aplicación mono-hilo, o contención.
Arreglo: Aumenta recursos de réplica (IOPS, CPU), ajusta replicación paralela donde aporte, reduce amplificación de escritura y divide cargas entre réplicas con roles claros.
6) Escalas réplicas y el lag empeora
Síntoma: Añadir más réplicas aumenta la carga del primario y el lag en las réplicas.
Causa raíz: Cada réplica añade overhead del sender de replicación (red, hilos de binlog dump) y puede incrementar presión de fsync/log dependiendo de la configuración.
Arreglo: Usa replicación en cascada (cuando aplique), asegura red provisionada y valida CPU/I/O del primario antes de añadir réplicas.
Tres micro-historias corporativas desde las trincheras del lag
Mini-historia #1: El incidente causado por una suposición equivocada (las réplicas de lectura son “seguras”)
Una compañía SaaS mediana tenía una diapositiva de arquitectura limpia: writes al primario, lecturas a las réplicas. También tenían una función llamada “exportación instantánea” que los usuarios podían ejecutar justo después de cambiar configuraciones.
Alguien asumió que la lectura de configuraciones podía venir de una réplica porque “es solo configuración”. El job de exportación escribió un registro “started” en el primario, luego leyó configuraciones de una réplica y produjo salida basada en lo que encontró.
Durante una campaña de marketing, las escrituras se dispararon. El lag de replicación subió de segundos a minutos. Las exportaciones empezaron a usar configuraciones antiguas—filtros equivocados, formatos equivocados, destinos equivocados. Nada se cayó. El sistema simplemente produjo resultados incorrectos con confianza, que es el tipo de fallo más caro de todos.
La solución no fue una perilla de tuning. Implementaron read-your-writes: después de actualizar configuraciones, la sesión del usuario quedó anclada al primario por una ventana breve. También etiquetaron réplicas por rol: “frescura” vs “analytics”. La función de exportación dejó de fingir que podía vivir con lecturas “baratas”.
Mini-historia #2: La optimización que salió mal (lotes más grandes, dolor mayor)
Una plataforma de pagos ejecutaba MySQL con réplicas. Un job nocturno actualizaba estados en lotes. El lag era molesto pero manejable. Alguien notó que el job pasaba demasiado tiempo comiteando y decidió “optimizar” aumentando dramáticamente el tamaño de lote.
Al primario le encantó al principio: menos commits, menos overhead. Luego las réplicas empezaron a atrasarse en ráfagas feas. Los threads de apply golpeaban una transacción masiva, tardaban mucho en aplicarla y mantenían los relay logs creciendo. Durante esa ventana, las réplicas sirvieron lecturas cada vez más obsoletas.
Peor aún: cuando la gran transacción finalmente se aplicó en la réplica, provocó una ola de flushes pesada. La latencia de almacenamiento se disparó. La aplicación se ralentizó aún más. Los gráficos del equipo se convirtieron en una exhibición de arte moderno.
La verdad aburrida: optimizaron el overhead de commits del primario e ignoraron la mecánica de aplicación de la réplica. La solución fue ajustar el tamaño del lote a un punto dulce medido: lo suficientemente pequeño para mantener la aplicación suave, lo suficientemente grande para evitar muerte por commits. También añadieron guardrails: tamaño máximo de transacción y un breaker que pausaba el job cuando el lag de réplica excedía un umbral.
Mini-historia #3: La práctica aburrida pero correcta que salvó el día (réplicas por rol y runbooks)
Una compañía retail operaba Postgres con dos standbys. Uno estaba designado “standby fresco” con cancelación agresiva de consultas en conflicto; el otro era “standby de reporting” donde se permitían lecturas largas y se esperaba lag.
Durante temporada alta, un desarrollador lanzó un informe caro. Cayó en el reporting standby como estaba previsto. Ese standby se retrasó minutos, pero nadie entró en pánico porque su SLO lo permitía. El standby fresco se mantuvo cerca del tiempo real porque no alojaba la carga del reporte.
Luego un despliegue introdujo una migración intensiva en escrituras. La reproducción en el standby fresco empezó a desviarse. On-call siguió un runbook: verificar receive vs replay LSN, revisar iostat, confirmar ausencia de conflictos de consultas, y temporalmente enrutar lecturas “sensibles a staleness” al primario. No debatieron 45 minutos en un canal. Actuaron.
El impacto al negocio fue mínimo. No porque tuvieran replicación perfecta, sino porque tenían un propósito definido para cada réplica, expectativas medidas y un plan practicado. Lo aburrido salvó el día. Otra vez.
Listas de verificación / plan paso a paso
Checklist A: Cuando el lag se dispara ahora mismo (respuesta a incidentes)
- Confirma el alcance: ¿una réplica o todas? ¿MySQL o Postgres? ¿una sola región o entre regiones?
- Clasifica el lag: transporte vs aplicación vs visibilidad (usa las tareas arriba).
- Protege la corrección: enruta lecturas críticas al primario; habilita read-your-writes.
- Reduce la carga en la réplica: detén consultas analíticas, pausa jobs background, limita escritores por lotes.
- Revisa la latencia de almacenamiento: si
awaites alto y %util está al límite, arregla infra o reduce escrituras. - Busca un gran culpable: transacción enorme, rebuild de índice, migración o conflictos de vacuum.
- Haz el cambio más pequeño y seguro: cambio temporal de enrutamiento, pausar job, terminar consulta, añadir capacidad.
- Documenta la línea temporal: cuándo empezó el lag, qué se correlacionó (deploy, job, tráfico, eventos de almacenamiento).
Checklist B: Evitar que el lag sea tu personalidad (plan de ingeniería)
- Define roles de réplica: réplica de frescura vs réplica de reporting vs réplica DR.
- Establece presupuestos de lag: segundos/minutos aceptables por rol; comunícalo a producto.
- Instrumenta el pipeline:
- MySQL: crecimiento de relay log, estados de workers, tasa de aplicación.
- Postgres: LSN recibido vs reproducido, retraso por timestamp de replay, contadores de conflicto, retención de slots.
- Construye seguridad en la app: read-your-writes, tokens de consistencia (GTID/LSN), fallback al primario.
- Controla la amplificación de escritura: higiene de índices, evitar filas anchas, evitar updates inútiles.
- Planifica ventanas de mantenimiento: operaciones masivas fuera de pico; rate-limit en migraciones y backfills.
- Capacidad con margen: IOPS y CPU dimensionados para pico + catch-up, no para promedio.
- Practica failover: el lag interactúa con failover de maneras desagradables; ensáyalo.
Checklist C: Patrones de esquema y carga que reducen lag por diseño
- Reemplaza “actualizar la misma fila constantemente” por eventos append-only + compactación periódica.
- Agrupa escrituras a un tamaño estable; evita ambos extremos (commits diminutos, commits monstruosos).
- Mantén índices secundarios intencionales; elimina los que son “quizá útiles algún día”.
- En Postgres, evita transacciones largas en hot standbys; cuestan frescura o fiabilidad.
Preguntas frecuentes
1) ¿Es fiable la medición de lag en MySQL?
Es útil, no un evangelio. Seconds_Behind_Master es una estimación basada en timestamps y puede ser engañosa en periodos inactivos o con ciertas cargas. Mide también crecimiento de relay logs y estados de aplicación.
2) ¿Es más fácil razonar sobre el lag en Postgres?
Usualmente, sí. Las métricas basadas en LSN te permiten separar recepción vs reproducción. El lag basado en tiempo vía pg_last_xact_replay_timestamp() también es práctico, aunque es NULL si aún no se han reproducido transacciones.
3) ¿Debería ejecutar analytics en réplicas?
Sí, pero no en la misma réplica de la que dependes para frescura. Dale analytics su propia réplica (o sistema). Si no, estás cambiando “menos consultas en el primario” por “la replicación se queda atrás y la corrección se vuelve rara”.
4) ¿Añadir más réplicas reduce el lag?
No. Las réplicas reducen la carga de lectura en el primario (a veces). El lag trata de la capacidad de la réplica para recibir y aplicar. Más réplicas pueden aumentar el overhead del primario y el uso de red.
5) ¿Cuál es la forma más rápida y segura de reducir el impacto al usuario durante el lag?
Enruta lecturas sensibles a obsolescencia al primario y reserva las réplicas para lecturas no críticas. Añade read-your-writes después de escrituras. Esto es una red de seguridad a nivel de aplicación que funciona independientemente del motor.
6) ¿Puede la replicación paralela “resolver” el lag en MySQL?
Puedes ayudar mucho cuando las transacciones son independientes. No ayudará mucho con cadenas de dependencia (filas calientes, contención en una sola tabla, orden estricto de commit). Trátala como un multiplicador sobre una carga que ya se paraleliza.
7) ¿Por qué Postgres a veces cancela consultas en una réplica?
Conflictos de hot standby: la reproducción necesita aplicar cambios que entran en conflicto con el snapshot de una consulta o con locks necesarios. Dependiendo de la configuración, Postgres o retrasa la reproducción (más lag) o cancela consultas (más fallos de consulta). Elige con intención.
8) ¿Son necesarias las replication slots en Postgres?
Son necesarias para la replicación lógica y útiles para prevenir la eliminación de WAL antes de que los consumidores lo reciban. También son máquinas para llenar disco si los consumidores desaparecen. Úsalas con monitorización y una política de ciclo de vida.
9) ¿Debería usar replicación síncrona para eliminar el lag?
La replicación síncrona puede acotar el lag haciendo que los commits esperen. Pagas en latencia de escritura y disponibilidad reducida si la réplica síncrona está enferma. Úsala en caminos de datos donde la estaleness acotada valga el coste.
10) ¿Cuál es la causa raíz más común que ves en producción?
Latencia de almacenamiento bajo carga de escritura, amplificada por elecciones de esquema (demasiados índices) y cargas mixtas en réplicas (analytics + frescura). Las bases de datos reciben la culpa; las gráficas de I/O callan la verdad.
Conclusión: siguientes pasos que no te avergonzarán
Si recuerdas una cosa: el retraso de replicación es una cola. No lo “sintonizas” con pensamiento mágico. O aumentas la capacidad de aplicación/reproducción, reduces el volumen de escrituras, o cambias lo que esperas de las réplicas.
Pasos prácticos:
- Implementa read-your-writes para la corrección visible al usuario. Deja de fingir que las réplicas son inmediatamente consistentes.
- Separa roles de réplica: una para frescura, otra para reporting, otra para DR. Da a cada una un presupuesto de lag y hazlo cumplir.
- Instrumenta el pipeline de replicación con métricas de recepción vs aplicación/replay, no solo un único número de lag.
- Arregla el headroom de almacenamiento: si los discos de réplica están saturados, nada más importa.
- Audita la amplificación de escritura: índices, jobs por lotes y filas calientes. La mayoría de “problemas de replicación” empiezan en el esquema y la carga, no en la replicación.
- Escribe el runbook y ensáyalo. El peor momento para descubrir qué métrica miente es a las 3 a.m. en un festivo.
MySQL y PostgreSQL replican bien—cuando tratas la replicación como un sistema de producción de primera clase, no como una casilla para marcar. El lag que toleras es una decisión de producto. El lag que no mides es una decisión de carrera.