MySQL vs PostgreSQL: réplicas de lectura — cuándo ayudan y cuándo mienten

¿Te fue útil?

Añadiste réplicas de lectura para detener el pager de “CPU 95%”. Los gráficos mejoraron. Luego aparece soporte con capturas: “Acabo de pagar, pero el pedido aparece como no pagado.”
Las réplicas no solo dejaron de ayudar. Ayudaron de la peor manera posible: confirmándose erróneas con confianza.

Las réplicas de lectura son una herramienta legítima de escalado. También son una excelente manera de delegar tu coherencia a la física y la esperanza. Esta es una guía práctica sobre
cómo se comportan las réplicas de MySQL y PostgreSQL en producción, cuándo te dan margen y cuándo mienten a tu aplicación.

Cuándo las réplicas ayudan vs cuándo mienten

Las réplicas ayudan cuando las lecturas son lecturas verdaderas

Las réplicas de lectura brillan cuando tu carga contiene lecturas pesadas y repetibles que no necesitan los datos más recientes:
reporting, analítica, indexado de búsqueda, navegación de “lista de productos”, timelines con estaleidad aceptable, y
consultas de larga duración que prefieres no ejecutar en el primario. Puedes apartar esas lecturas y ganar espacio para escrituras.

También ayudan cuando necesitas flexibilidad operativa: cambios de esquema online, alivio de presión de vacuum (Postgres),
entornos secundarios para verificación y ventanas de mantenimiento más seguras. Una réplica puede ser el lugar donde ejecutar consultas “raras”
sin jugarte la caché y la latencia del primario.

Las réplicas mienten cuando las usas como balanceador de carga para tráfico sensible a la coherencia

La replicación suele ser asíncrona. Eso significa que la réplica está atrasada por definición. La única pregunta es “¿cuánto?”
y “¿por cuánto tiempo?”. La mentira es sutil: la réplica responde rápido y con confianza, pero con historia.

Si envías tráfico “lee mi propia escritura” a una réplica sin salvaguardas, creas inconsistencia visible por el usuario.
El usuario hace algo; lo aceptas; lee de vuelta; le muestras estado antiguo. Parece pérdida de datos, aun cuando es
“solo latencia”.

Primera regla: si una lectura cambia lo que el usuario hará a continuación (dinero, autenticación, inventario, permisos),
no la dejes provenir de una réplica asíncrona salvo que tengas lógica explícita de consistencia por sesión.

Segunda regla: las réplicas no arreglan consultas lentas. Solo permiten ejecutar más de ellas. Si tu ruta de lectura
no está indexada, has construido un sistema de eliminación de basura más grande, no una cocina más limpia.

Chiste #1: La latencia de replicación es como una reunión a la que no fuiste invitado: solo descubres que ocurrió después de que ya se tomaron las decisiones.

Cómo se ve la “ayuda” en métricas

  • La CPU del primario baja y se mantiene baja durante picos de lecturas.
  • Mejora la tasa de aciertos del buffer pool / shared buffers del primario (menos churn por consultas intensivas de lectura).
  • Mejoran p95 y p99 de latencia en transacciones de escritura porque se reduce la presión de locks y el churn de caché.
  • El lag de la réplica se mantiene acotado bajo el pico esperado (conoces el presupuesto y lo haces cumplir).

Cómo se ve la “mentira” en incidentes

  • “Acabo de cambiar mi contraseña y aún acepta la vieja.”
  • “El inventario muestra 3 unidades, el checkout falla.”
  • “Soporte lo ve; el cliente no.” (soporte consulta el primario; clientes consultan réplicas)
  • Ocurre un failover y tu “réplica” se promueve a primario con escrituras faltantes que creías comprometidas.

Replicación MySQL vs PostgreSQL: qué es realmente diferente

Ambos sistemas pueden tener réplicas de lectura. Ambos pueden configurarse con opciones asíncronas y semi/síncronas. Ambos pueden perjudicarte si finges que la replicación es un espejo mágico. Pero los modos de fallo difieren porque la mecánica difiere.

Replicación MySQL: binlogs, GTID y decisiones que no puedes ignorar

La replicación clásica de MySQL se basa en que el primario escribe cambios en el log binario y las réplicas lo descargan y aplican. Las perillas clave en producción:

  • Binlog basado en filas vs basado en sentencias: el modo basado en filas suele ser más seguro y determinista; el basado en sentencias es más pequeño pero puede ser “creativo”.
  • GTID: los identificadores globales de transacción hacen que el failover y el seguimiento de posición sean mucho menos dolorosos.
  • Aplicación de un solo hilo vs paralela: el hilo SQL de replicación puede ser cuello de botella y generar lag aunque la red esté bien.
  • Semi-sync: reduce el riesgo de pérdida de datos en el commit obligando a que al menos una réplica reconozca recepción (no siempre aplicación) antes de devolver el commit.

En MySQL, “la réplica se puso al día” suele expresarse en términos del conjunto GTID ejecutado o segundos detrás de la fuente, pero debes entender qué significa realmente ese “segundos”: se deriva de timestamps en eventos, no es una medida monotónica y precisa de verdad.

Replicación PostgreSQL: WAL, streaming y la realidad del “hot standby”

Las réplicas de Postgres transmiten WAL (write-ahead log). Las réplicas pueden ser hot standbys: legibles mientras aplican WAL. Esto es operativamente agradable, hasta que tus consultas de lectura chocan con vacuum y la limpieza de versiones de fila.

  • Replication slots: protegen el WAL necesario para réplicas, pero pueden llenar discos si una réplica se atrasa.
  • WAL receiver/replay: el lag puede darse en receive, flush o replay—cada etapa señala un cuello de botella distinto.
  • Conflictos en hot standby: consultas largas pueden bloquear limpieza; la limpieza puede cancelar lecturas según la configuración.
  • Replicación síncrona: puede forzar esperas de commit en los standbys; eliges cuántos deben reconocer.

Postgres ofrece métricas ricas y por etapa de lag, lo cual es genial porque finalmente puedes dejar de discutir con tu equipo de aplicación sobre si “segundos detrás” es una vibra o un número.

La gran diferencia operativa: qué significa “durable” bajo replicación

Si le confirmas una escritura a un cliente, el cliente la tratará como verdad. Si el primario se estrella justo después y tu failover promueve una réplica que nunca recibió esa escritura, tu sistema ha realizado un viaje accidental en el tiempo.

MySQL semi-sync y la replicación síncrona de Postgres mitigan eso, pero tienen semánticas distintas. El semi-sync de MySQL suele significar “al menos una réplica recibió el evento”. La replicación síncrona de Postgres puede significar “uno o más standbys confirmaron flush” (o, en configuraciones más nuevas, elecciones más matizadas), lo que se acerca más a “está en disco en otro lado”.

Consejo práctico: si usas réplicas para seguridad ante failover, define explícitamente la durabilidad de commit requerida.
Si usas réplicas para escalar lecturas, define explícitamente la estaleidad aceptable. Son objetivos distintos.
Mezclarlos sin reconocer los trade-offs es como obtener lo peor de ambos.

Modelos de consistencia que puedes implementar (y su coste)

1) “Las lecturas de réplica son best effort” (barato, peligroso)

Distribuyes peticiones GET entre réplicas y aceptas que a veces los usuarios ven datos antiguos. Esto está bien para contenido no crítico. No está bien para transiciones de estado. Si no puedes clasificar las lecturas fácilmente, acabarás usando este modelo por defecto para todo.

2) Leer tus propias escrituras por sesión (el por defecto responsable)

Después de que un usuario escribe algo, las lecturas subsecuentes en esa sesión deberían verlo. Puedes implementarlo de varias formas:

  • Pinning al primario: después de una escritura, enruta ese usuario al primario durante N segundos.
  • Fencing por posición GTID/LSN: registra la posición de commit (GTID o LSN) devuelta por el primario; solo lee de réplicas que hayan reproducido hasta esa posición.
  • Cache a nivel de aplicación con versionado: guarda resultados recientes de escrituras en caché y fúndelos con lecturas de réplicas (más difícil de lo que parece).

Coste: complejidad en el enrutamiento y la instrumentación. Beneficio: tu producto deja de gaslightear a tus clientes.

3) Replicación síncrona para escrituras sensibles a la corrección (cara, a veces necesaria)

Si no debes perder transacciones confirmadas ante la falla de un nodo, necesitas esperar a las confirmaciones de réplica antes de devolver éxito. Eso aumenta latencia de commit y reduce throughput—especialmente entre zonas de disponibilidad.

Úsalo para movimientos de dinero, flujos irreversibles y credenciales. No lo actives a lo loco porque alguien oyó la frase “consistencia fuerte” y se emocionó.

4) Replicación lógica / change streams para modelos de lectura (a menudo la mejor historia de escalado)

A veces la respuesta correcta es: dejar de hacer que tu base de datos primaria sirva todas las formas de lectura. Usa replicación (decodificación lógica en Postgres; CDC basado en binlog en MySQL) para construir almacenes optimizados para lectura: índices de búsqueda, caches, tablas desnormalizadas, motores OLAP. Eso es escalado. Las réplicas son una casa intermedia.

Hechos interesantes y contexto histórico

  • Hecho 1: La replicación de MySQL precede a que InnoDB fuera el motor por defecto; las suposiciones iniciales de replicación se moldearon con motores de almacenamiento más simples y expectativas de durabilidad más laxes.
  • Hecho 2: La replicación basada en sentencias fue popular en parte porque reducía el tamaño del binlog, pero expuso a los usuarios a no determinismo (funciones de tiempo, orden aleatorio sin ORDER BY, etc.).
  • Hecho 3: Los GTID en MySQL fueron un gran cambio operativo: convirtieron “encontrar el archivo binlog y la posición correctos” en “comprobar la identidad de la transacción”, que es mucho menos propenso a errores durante un failover.
  • Hecho 4: La replicación basada en WAL de PostgreSQL evolucionó del envío de archivos al streaming; la replicación por streaming hizo realista “réplica como servicio” en vez de “trabajos de restauración y plegarias”.
  • Hecho 5: El hot standby de Postgres introdujo una nueva clase de drama en producción: las consultas de lectura en réplicas pueden entrar en conflicto con el vacuum, provocando cancelaciones o bloat.
  • Hecho 6: Los replication slots en Postgres son a la vez cinturón de seguridad y rifle: evitan pérdida de WAL para réplicas atrasadas, pero pueden llenar discos rápido si una réplica desaparece.
  • Hecho 7: Las métricas “segundos detrás del master/fuente” han engañado a ingenieros durante décadas porque no son una medida universal de frescura; son una heurística ligada a timestamps de eventos.
  • Hecho 8: La replicación semi-síncrona en MySQL surgió porque la replicación asíncrona más failover automático creaba ventanas de pérdida de datos inaceptables para muchas empresas.
  • Hecho 9: La replicación síncrona de Postgres permite elegir cuántos standbys deben confirmar, habilitando durabilidad estilo quórum a costa de latencia cola.

Guía rápida de diagnóstico

Estás de guardia. El tráfico de lectura se dispara. Alguien dice “simplemente añade otra réplica”. Antes de hacerlo, encuentra el cuello de botella.
Este playbook asume que quieres una respuesta en menos de 15 minutos, no un debate filosófico.

Primero: decide si el primario está realmente limitado por lecturas

  • Revisa CPU e I/O wait del primario: si la CPU está al máximo pero I/O wait es bajo, puede que estés limitado por cómputo (consultas malas, índices faltantes, mal plan).
  • Revisa tasa de aciertos de buffer/cache: si tu working set no cabe en memoria, las réplicas pueden solo multiplicar el dolor de I/O.
  • Revisa las consultas top: una consulta puede comerse una granja de réplicas en el desayuno.

Segundo: mide el lag de replicación correctamente

  • MySQL: compara conjuntos GTID, no vibras. Seconds behind source es una pista, no una promesa.
  • Postgres: separa receive lag de replay lag. Red vs disco vs CPU aparecen distinto.

Tercero: busca throttles específicos en réplicas

  • MySQL: hilo SQL de replicación que no da abasto, trabajadores paralelos mal configurados, transacciones de larga duración en el primario que generan eventos gigantes.
  • Postgres: replay de WAL atascado por I/O, backlog de replication slot, conflictos de hot standby que cancelan consultas o retrasan limpieza.

Cuarto: valida el enrutamiento y el comportamiento read-after-write

  • Confirma qué porcentaje del tráfico llega a réplicas vs primario.
  • Confirma si las lecturas sensibles a la coherencia están fijadas o enjauladas (fenced).
  • Confirma si tu balanceador está fallando abiertamente a réplicas durante sobrecarga del primario.

Tareas prácticas: comandos, salidas, decisiones

A continuación hay tareas de nivel producción. Cada una incluye un comando, salida de ejemplo, qué significa y qué decisión tomar.
Ejecútalas desde un host admin seguro. Ajusta usuarios, sockets y hostnames a tu entorno.

Tarea 1 (MySQL): Comprobar estado de replicación y lag

cr0x@server:~$ mysql -e "SHOW REPLICA STATUS\G"
*************************** 1. row ***************************
             Replica_IO_State: Waiting for source to send event
                Source_Host: mysql-primary-1
              Source_Log_File: binlog.003921
          Read_Source_Log_Pos: 812993441
             Relay_Log_File: relay.000778
              Relay_Log_Pos: 813002118
      Replica_SQL_Running: Yes
       Replica_IO_Running: Yes
          Seconds_Behind_Source: 7
           Retrieved_Gtid_Set: 2f1c9b2a-...:1-91833122
            Executed_Gtid_Set: 2f1c9b2a-...:1-91833098

Qué significa: los hilos I/O y SQL están en ejecución; la réplica reporta 7s de retraso, pero el GTID muestra que le faltan 24 transacciones.

Decisión: Si tu SLA de estaleidad es < 1s (común para estado de usuario), esta réplica no puede servir lecturas post-escritura. O fija al primario tras escrituras o protege con fencing por GTID.

Tarea 2 (MySQL): Comparar ejecución de GTID entre primario y réplica

cr0x@server:~$ mysql -h mysql-primary-1 -e "SELECT @@global.gtid_executed\G"
*************************** 1. row ***************************
@@global.gtid_executed: 2f1c9b2a-...:1-91833122
cr0x@server:~$ mysql -h mysql-replica-1 -e "SELECT @@global.gtid_executed\G"
*************************** 1. row ***************************
@@global.gtid_executed: 2f1c9b2a-...:1-91833098

Qué significa: la réplica está atrasada por un rango GTID específico; puedes cuantificar la frescura sin adivinar.

Decisión: Usa esto para automatización: dirige sesiones a réplicas solo después de que alcancen un GTID objetivo (o enruta al primario hasta entonces).

Tarea 3 (MySQL): Detectar si la replicación está limitada por aplicación mono-hilo

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'replica_parallel_workers'; SHOW VARIABLES LIKE 'replica_parallel_type';"
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| replica_parallel_workers | 0     |
+--------------------------+-------+
+------------------------+----------+
| Variable_name          | Value    |
+------------------------+----------+
| replica_parallel_type  | DATABASE |
+------------------------+----------+

Qué significa: los workers paralelos están deshabilitados. Un solo hilo SQL aplica todo.

Decisión: Si la tasa de escrituras del primario es alta y el lag crece bajo carga, habilita replicación paralela (con cuidado), y vuelve a medir lag y restricciones de orden de commits.

Tarea 4 (MySQL): Comprobar transacciones grandes que causen lag por ráfagas

cr0x@server:~$ mysql -e "SHOW PROCESSLIST;"
+----+-----------+-----------------+------+---------+------+------------------------+------------------+
| Id | User      | Host            | db   | Command | Time | State                  | Info             |
+----+-----------+-----------------+------+---------+------+------------------------+------------------+
| 12 | repl      | 10.0.2.15:45512  | NULL | Binlog Dump GTID |  0 | Source has sent all binlog to replica | NULL |
| 33 | app       | 10.0.3.21:60433  | prod | Query   | 218  | updating               | UPDATE orders ... |
+----+-----------+-----------------+------+---------+------+------------------------+------------------+

Qué significa: un UPDATE de larga duración en el primario puede crear eventos de binlog gigantes; las réplicas aplican después y “se ponen al día” en saltos feos.

Decisión: Fragmenta updates masivos en trozos o ejecútalos fuera de pico. Si no puedes, deja de servir lecturas sensibles a la coherencia desde réplicas durante la operación.

Tarea 5 (Postgres): Medir lag de receive, flush y replay

cr0x@server:~$ psql -h pg-replica-1 -d postgres -c "SELECT now() AS ts, pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn(), pg_last_xact_replay_timestamp();"
              ts              | pg_last_wal_receive_lsn | pg_last_wal_replay_lsn | pg_last_xact_replay_timestamp
-----------------------------+--------------------------+-------------------------+-------------------------------
 2025-12-29 14:03:11.120+00  | 3A/9F12A7B0              | 3A/9F0C1028             | 2025-12-29 14:03:05.004+00
(1 row)

Qué significa: la réplica está recibiendo WAL por delante de lo que ha reproducido. El replay va retrasado; la última transacción reproducida tiene ~6 segundos.

Decisión: si receive está por delante pero replay va atrasado, céntrate en CPU/disco del replicante y en ajustes de replay. Si receive también se retrasa, revisa red o WAL sender del primario.

Tarea 6 (Postgres primario): Identificar standbys retrasados y su estado

cr0x@server:~$ psql -h pg-primary-1 -d postgres -c "SELECT application_name, state, sync_state, write_lag, flush_lag, replay_lag FROM pg_stat_replication;"
 application_name |   state   | sync_state | write_lag | flush_lag | replay_lag
------------------+-----------+------------+-----------+-----------+------------
 pg-replica-1      | streaming | async      | 00:00:01  | 00:00:01  | 00:00:06
 pg-replica-2      | streaming | async      | 00:00:00  | 00:00:00  | 00:00:01
(2 rows)

Qué significa: Replica-1 está replaying 6s detrás; replica-2 está casi en tiempo real.

Decisión: si tu router puede elegir, prefiere replica-2 para lecturas sensibles a la frescura. Investiga por qué replica-1 repite más lento (I/O, CPU, conflictos).

Tarea 7 (Postgres): Detectar cancelaciones por hot standby

cr0x@server:~$ psql -h pg-replica-1 -d postgres -c "SELECT datname, confl_lock, confl_snapshot, confl_bufferpin, confl_deadlock FROM pg_stat_database_conflicts;"
 datname | confl_lock | confl_snapshot | confl_bufferpin | confl_deadlock
---------+------------+----------------+-----------------+----------------
 prod    |          0 |             17 |               0 |              0
(1 row)

Qué significa: ocurrieron 17 conflictos de snapshot—se cancelaron consultas porque el replay necesitaba eliminar versiones de fila de las que dependía una consulta larga.

Decisión: mueve esas consultas largas a otro lugar, ajusta max_standby_streaming_delay, o acepta cancelaciones con lógica de reintento. No lo pretendas como “aleatorio”.

Tarea 8 (Postgres): Comprobar replication slots por retención descontrolada de WAL

cr0x@server:~$ psql -h pg-primary-1 -d postgres -c "SELECT slot_name, active, restart_lsn, wal_status FROM pg_replication_slots;"
    slot_name     | active | restart_lsn | wal_status
------------------+--------+-------------+------------
 replica1_slot     | f      | 39/FF0012A0 | extended
 replica2_slot     | t      | 3A/9E1000A0 | reserved
(2 rows)

Qué significa: replica1_slot está inactiva; Postgres puede retener WAL desde restart_lsn en adelante, potencialmente llenando disco.

Decisión: si replica-1 se perdió para siempre, elimina el slot. Si está temporalmente caída, presupuestar disco y arreglar la réplica rápido. Esto no es trabajo “para después”.

Tarea 9 (Sistema): Verificar latencia de disco en la réplica (cuello de botella común de replay)

cr0x@server:~$ iostat -x 1 3
Linux 6.5.0 (pg-replica-1) 	12/29/2025 	_x86_64_	(8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.30    0.00    3.90   24.10    0.00   59.70

Device            r/s     w/s   rkB/s   wkB/s  avgrq-sz avgqu-sz   await  svctm  %util
nvme0n1         120.0   380.0  8120.0 48000.0     279.0     9.80   21.40   1.20  60.00

Qué significa: iowait es alto; el tiempo medio de espera es ~21ms. El replay de WAL probablemente esté limitado por I/O.

Decisión: deja de culpar a “la replicación de Postgres”. Arregla el almacenamiento: disco más rápido, menos contención, WAL separado, ajusta checkpointing o reduce workloads que compitan en la réplica.

Tarea 10 (MySQL): Comprobar si semi-sync está habilitado (y qué implica)

cr0x@server:~$ mysql -h mysql-primary-1 -e "SHOW VARIABLES LIKE 'rpl_semi_sync_master_enabled'; SHOW STATUS LIKE 'Rpl_semi_sync_master_status';"
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| rpl_semi_sync_master_enabled  | ON    |
+-------------------------------+-------+
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Rpl_semi_sync_master_status| ON    |
+----------------------------+-------+

Qué significa: semi-sync está habilitado y activo. Los commits deberían esperar a que al menos una réplica reconozca la recepción de eventos.

Decisión: bueno para reducir pérdida de datos en failover, pero no garantiza que la réplica esté fresca para lecturas. No lo trates como “seguro para leer de réplicas inmediatamente”.

Tarea 11 (Postgres): Confirmar configuración de replicación síncrona

cr0x@server:~$ psql -h pg-primary-1 -d postgres -c "SHOW synchronous_commit; SHOW synchronous_standby_names;"
 synchronous_commit
-------------------
 on
(1 row)

 synchronous_standby_names
--------------------------
 FIRST 1 (pg-replica-2)
(1 row)

Qué significa: el primario espera a un standby nombrado (replica-2) para semánticas de commit síncrono.

Decisión: si cuentas con esto para durabilidad, monitoriza que el standby nombrado esté sano. Si cae, tu sistema puede estancarse o retroceder a async según la configuración y orquestación.

Tarea 12 (Postgres): Identificar consultas lentas en una réplica (porque las réplicas también pueden sobrecargarse)

cr0x@server:~$ psql -h pg-replica-1 -d prod -c "SELECT pid, now()-query_start AS age, state, wait_event_type, wait_event, left(query,120) AS q FROM pg_stat_activity WHERE state <> 'idle' ORDER BY age DESC LIMIT 5;"
 pid  |   age    | state  | wait_event_type |  wait_event   |                          q
------+----------+--------+-----------------+--------------+----------------------------------------------------------
 8412 | 00:03:41 | active | IO              | DataFileRead | SELECT ... FROM events WHERE ... ORDER BY created_at DESC
 9120 | 00:01:12 | active | CPU             |              | SELECT ... FROM orders JOIN order_items ...
(2 rows)

Qué significa: la réplica está ocupada. Una consulta espera I/O; otra consume CPU.

Decisión: si las réplicas están saturadas, añadir más réplicas puede ayudar—pero solo si arreglas el patrón de consultas o los índices también. Si no, escalas horizontalmente el dolor.

Tarea 13 (MySQL): Observar distribución de lecturas (sanidad del proxy/balanceador)

cr0x@server:~$ mysql -h mysql-primary-1 -e "SHOW GLOBAL STATUS LIKE 'Questions';"
+---------------+-----------+
| Variable_name | Value     |
+---------------+-----------+
| Questions     | 819332199 |
+---------------+-----------+
cr0x@server:~$ mysql -h mysql-replica-1 -e "SHOW GLOBAL STATUS LIKE 'Questions';"
+---------------+-----------+
| Variable_name | Value     |
+---------------+-----------+
| Questions     | 83322188  |
+---------------+-----------+

Qué significa: el primario sigue recibiendo la mayoría de consultas. O el enrutamiento no funciona, o tu app hace pinning excesivo.

Decisión: antes de añadir réplicas, arregla el enrutamiento. Si no puedes demostrar que el tráfico de lectura se movió, solo estás comprando hardware para decorar dashboards.

Tarea 14 (Sistema): Comprobar retransmisiones de red (la replicación por streaming odia pérdida de paquetes)

cr0x@server:~$ ss -ti dst 10.0.1.10:5432 | head -n 20
ESTAB 0 0 10.0.2.20:48932 10.0.1.10:5432
	 cubic wscale:7,7 rto:204 rtt:2.1/0.9 ato:40 mss:1448 pmtu:1500 rcvmss:1448 advmss:1448 cwnd:10 bytes_sent:9231123 bytes_acked:9231000 bytes_received:1840012 segs_out:6123 segs_in:5981 data_segs_out:5123 data_segs_in:4981 send 55.2Mbps lastsnd:8 lastrcv:7 lastack:7 pacing_rate 110Mbps retrans:14/231

Qué significa: hay retransmisiones. Pocas son normales; un número creciente con lag sugiere problemas de red que causan receive lag.

Decisión: si el receive lag crece con retransmisiones, no “tunes Postgres”. Arregla la ruta de red o mueve el tráfico de replicación a un enlace menos congestionado.

Tres mini-historias de la vida corporativa

Mini-historia 1: Un incidente causado por una suposición equivocada

Un negocio de suscripción mediano tenía una arquitectura limpia en el papel: base de datos primaria para escrituras, dos réplicas de lectura para “escala” y un proxy que enroutaba SELECTs.
El equipo de producto añadió un flujo de “cancelar suscripción”. Escribía en el primario y luego leía inmediatamente el estado de la suscripción para decidir la UI siguiente.
Con poca carga parecía correcto. Bajo pico, se convirtió en generador de soporte al cliente.

La suposición equivocada fue simple: “la réplica es básicamente en tiempo real.” El proxy no tenía concepto de consistencia por sesión. No sabía que un usuario acababa de escribir.
Así que a veces la lectura post-cancelación pegaba con una réplica que aún mostraba “activa”. La UI ofrecía acciones erróneas. Los clientes reintentaban. Los reintentos generaron más escrituras.
El lag empeoró. Puedes imaginar el resto.

La parte divertida: los dashboards parecían “saludables.” La CPU de réplica estaba baja. La CPU del primario moderada. Latencia bien. La única métrica que gritaba era la de tickets de soporte.
Los ingenieros inicialmente persiguieron cacheo front-end, luego narrativas de “consistencia eventual”, luego intentaron añadir una tercera réplica. Eso no mejoró porque el problema no era capacidad. Era semántica.

La solución fue aburrida e inmediata: tras una escritura en ese flujo, fijaron lecturas al primario durante 30 segundos para ese usuario/token de sesión.
También añadieron una métrica de “frescura de réplica” al proxy para que evitara réplicas con lag.
El incidente terminó, y todos fingieron que siempre supieron que era un problema de enrutamiento.

Mini-historia 2: Una optimización que se volvió en contra

Un equipo empresarial intentó reducir la carga del primario moviendo “todas las consultas de reporting” a un hot standby de Postgres. Razonable. Excepto que las consultas de reporting no eran informes;
eran dashboards ad-hoc con escaneos de minutos y la costumbre de exportar CSVs en cierres trimestrales.

En papel esto era seguro porque el standby no servía escrituras. En la realidad, esos snapshots largos retuvieron expectativas de limpieza y provocaron conflictos en el standby.
El equipo de ops ajustó settings para evitar cancelar consultas—porque a los ejecutivos no les gustan dashboards parciales. Eso redujo cancelaciones pero aumentó el retraso en el replay de WAL.

Luego hubo una prueba de failover. El standby promovido tardó más de lo esperado en volverse consistente, y un bloque de lecturas sensibles al tiempo ahora se sirvieron desde un nodo con minutos de retraso.
Nada se “corrompió”. Simplemente no estaba actualizado. Mientras tanto, el disco del primario subió porque el WAL se retenía más tiempo de lo anticipado.

La optimización que se volvió en contra fue mezclar workloads de “reporting en tiempo humano” con responsabilidades de “standby operacional”. La solución correcta fue separar responsabilidades:
una réplica de analítica dedicada con cancelaciones agresivas permitidas, y un standby separado configurado para replay rápido y comportamiento de failover predecible.

Mini-historia 3: Una práctica aburrida pero correcta que salvó el día

Una plataforma cercana a pagos tenía MySQL y Postgres en dominios distintos. Tenían una regla que sonaba a burocracia:
cualquier réplica usada para lecturas debía publicar una señal de frescura, y el router debía rechazarla si excedía el presupuesto de lag.

La gente se quejaba en reuniones de planificación. “Simplemente añadiremos más réplicas.” “No necesitamos la complejidad.”
La respuesta de SRE fue consistente: “Si no podemos medir frescura, no podemos garantizar corrección.”
Así implementaron fencing: los workflows MySQL almacenaban el último GTID de la transacción de escritura; los workflows Postgres almacenaban el último LSN.

Un año después, un controlador de almacenamiento en una réplica empezó a atascar escrituras intermitentemente. La réplica parecía “up”, aceptaba conexiones y servía lecturas.
Pero el apply de WAL/relay se ralentizó y el lag se desvió fuera del presupuesto.

El router drenó automáticamente esa réplica del pool. Sin impacto al cliente. El on-call recibió una alerta clara: “frescura de réplica violada”, no “clientes enfadados”.
Reemplazaron el hardware defectuoso en horario laboral, porque las salvaguardas aburridas te compran incidentes aburridos.

Errores comunes: síntoma → causa raíz → solución

1) Los usuarios no ven sus actualizaciones

Síntoma: Tras una actualización, la UI muestra el valor antiguo por unos segundos; un refresh a veces lo arregla.

Causa raíz: lecturas post-escritura enviadas a réplicas asíncronas; sin consistencia por sesión.

Solución: fija la sesión al primario tras escrituras, o protege usando GTID/LSN. Añade enforcement del presupuesto de lag en el router.

2) “Seconds behind source” es bajo pero faltan datos

Síntoma: MySQL muestra 0–1s de retraso, pero una fila recién confirmada no aparece en la réplica.

Causa raíz: la métrica se basa en timestamps y puede ser engañosa; además la aplicación SQL puede estar momentáneamente parada o la marca temporal del evento difiere.

Solución: compara conjuntos GTID; protege en base a GTID ejecutado. Trata Seconds_Behind_Source como alarma, no como regla.

3) El lag de réplica crece linealmente durante el pico y luego “se pone al día” en ráfagas

Síntoma: el lag aumenta de forma sostenida y luego baja de golpe; lecturas están obsoletas intermitentemente.

Causa raíz: transacciones grandes o aplicación mono-hilo; la réplica no puede paralelizar trabajo.

Solución: fragmenta escrituras; habilita/verifica apply paralelo; evita updates multi-tabla gigantes en ventanas pico.

4) El standby de Postgres cancela consultas

Síntoma: los reportes fallan con “canceling statement due to conflict with recovery.”

Causa raíz: conflictos hot standby entre replay (limpieza) y consultas de larga duración.

Solución: acelera las consultas, mueve el reporting fuera del standby, o acepta cancelaciones con reintentos. Ajusta intencionalmente los umbrales de delay en standby.

5) Disco del primario se llena inesperadamente (Postgres)

Síntoma: el directorio WAL crece rápido; alertas de disco; replicación por lo demás “bien”.

Causa raíz: slot de replicación inactivo retiene WAL para una réplica faltante o estancada.

Solución: elimina slots sin usar; monitoriza lag de slots; trata slots inactivos como riesgo de producción, no como curiosidad.

6) Añadir réplicas no reduce la carga del primario

Síntoma: añades dos réplicas; la CPU del primario apenas cambia.

Causa raíz: el enrutamiento no envía tráfico significativo, o el tráfico importante está ligado a escrituras, o la app hace pinning al primario.

Solución: mide conteo de consultas por nodo; valida reglas del router; clasifica lecturas por necesidad de corrección; arregla primero las consultas lentas.

7) Las réplicas son rápidas pero la aplicación va más lenta

Síntoma: nodos DB lucen bien; p95 de la app sube; timeouts aumentan.

Causa raíz: tormentas de conexiones, pools mal dimensionados, o retries del balanceador causando herds sobre réplicas.

Solución: limita pools de conexiones; usa pooling de transacciones donde sea posible; implementa circuit breakers cuando réplicas excedan lag o tasa de errores.

8) El failover causa “datos faltantes” (en realidad: escrituras confirmadas perdidas)

Síntoma: tras el crash del primario y promoción, faltan las últimas transacciones de segundos atrás.

Causa raíz: replicación asíncrona; el failover promovió una réplica que no había recibido/aplicado esas escrituras.

Solución: usa replicación síncrona para esas transacciones, o acepta el RPO explícitamente. Alinea la automatización de failover con tu modelo de durabilidad.

Chiste #2: Una réplica de lectura es una base de datos honesta con mala memoria. Por desgracia, a tus usuarios no les interesa su biografía.

Listas de verificación / plan paso a paso

Paso a paso: decidir si las réplicas ayudarán a tu carga

  1. Clasifica las lecturas: marca endpoints/consultas como “requiere frescura” vs “acepta estaleidad”. Si no puedes, asume requiere frescura.
  2. Mide el cuello de botella del primario: CPU, iowait, tiempo de locks, tasa de aciertos de buffer, consultas top.
  3. Establece un presupuesto de lag: elige un número (p. ej., 250ms, 1s, 5s) por clase de lectura. Escríbelo.
  4. Elige política de enrutamiento: pinning al primario después de escrituras o fencing por GTID/LSN. No pongas réplicas sin una política.
  5. Instrumenta frescura de réplicas: exporta métricas de lag que reflejen tu mecanismo de fencing elegido.
  6. Prueba con carga incluyendo replicación: incluye tasas de escritura realistas; mide throughput de apply en réplicas, no solo throughput de consultas.
  7. Planifica fallos de réplica: qué pasa si una réplica es lenta, cae o está obsoleta? Tu router debe degradar a primario con gracia.

Paso a paso: construir una ruta de lectura “las réplicas no mienten”

  1. Al escribir: captura GTID (MySQL) o LSN (Postgres) del contexto de la transacción.
  2. Almacénalo por sesión/petición: en una cookie, token o almacenamiento de sesión server-side.
  3. Antes de leer en réplica: elige una réplica que haya ejecutado/reproducido al menos esa posición.
  4. Si ninguna califica: lee del primario, o espera hasta un timeout pequeño (peligroso; puede amplificar carga).
  5. Impon un techo global de lag: si una réplica excede el presupuesto, drénala automáticamente.
  6. Audita endpoints trimestralmente: las nuevas funciones tienden a colar lecturas sensibles a la coherencia en pools “escalados para lectura”.

Checklist operativo: réplicas como función SRE, no como slide arquitectónico

  • Alertas de lag de réplica que paginen en el punto de inconsistencia visible al usuario, no con “paciencia infinita”.
  • Plan de capacidad para réplicas: CPU y I/O de disco dimensionados para replay y carga de consultas simultáneamente.
  • Runbooks para: re-seed de réplicas, recuperación de backlog de slot/WAL, manejo de relay logs rotos y failover controlado.
  • Pruebas de caos: mata una réplica, introduce 1% de pérdida de paquetes, limita disco—verifica que el enrutamiento drene y que el fencing siga funcionando.
  • Backups existen y se prueban. La replicación no es backup; es duplicación de tus errores a velocidad de red.

FAQ

1) ¿Puedo usar réplicas de lectura para escalar escrituras?

No. Las réplicas escalan lecturas (y algunas tareas operativas). Las escrituras siguen siendo cuello de botella en el primario a menos que hagas sharding o rediseño.
Si tu primario está limitado por escrituras, las réplicas no lo arreglarán; incluso pueden empeorarlo al añadir overhead de replicación.

2) ¿La replicación de PostgreSQL es “más consistente” que la de MySQL?

Ambos pueden configurarse para garantías fuertes de durabilidad con replicación síncrona, y ambos se usan comúnmente asíncronos por rendimiento.
La diferencia no es moral; es mecánica y observable. Postgres da visibilidad de lag más granular; la herramienta GTID de MySQL puede ser excelente si se usa bien.

3) ¿Semi-sync de MySQL significa que mis lecturas en réplicas son frescas?

No. Semi-sync normalmente significa que una réplica reconoció recepción de los eventos antes de que el commit devuelva. No garantiza que la réplica los haya aplicado
y los haya hecho visibles para lecturas.

4) ¿Cuál es la estrategia de enrutamiento más simple y segura?

Pinning al primario tras escrituras por un TTL corto es lo más simple. No es perfecto, pero suele ser suficiente y fácil de razonar.
Si necesitas más corrección con menos lecturas al primario, pasa a fencing por GTID/LSN.

5) ¿Cuántas réplicas debo tener?

“Suficientes para manejar la carga de lectura pico con una réplica caída” es un punto de partida práctico. Luego añade requisitos:
un standby separado para failover, una réplica para reporting y quizás otra para mantenimiento/pruebas.
Mezclar todos los roles en una sola réplica es cómo obtienes conflictos de rol durante incidentes.

6) ¿Por qué el lag de réplica sube durante backups o mantenimiento?

Los backups pueden saturar disco y expulsar caché. Trabajos de mantenimiento generan I/O y CPU intensos. El apply de replicación compite por los mismos recursos.
La solución es aislamiento: dedica réplicas para backup/reporting, limita mantenimiento y monitoriza replay/hilo SQL.

7) ¿Las réplicas son sustituto del cache?

No realmente. Las réplicas aún ejecutan consultas y aún golpean almacenamiento. Los caches absorben lecturas repetidas barato; las réplicas distribuyen ejecución de consultas.
Usa caches para keys/valores calientes o respuestas computadas; usa réplicas para descargar lecturas complejas y aislar workloads.

8) ¿Cuál es la forma más rápida de saber si debo añadir una réplica o arreglar consultas?

Si las pocas consultas top dominan el tiempo y son lentas por índices faltantes o malos planes, arregla consultas primero.
Si tienes muchas lecturas moderadamente caras y el primario está limitado por CPU, las réplicas pueden ayudar.
Si estás limitado por I/O porque el working set no cabe en memoria, las réplicas a menudo solo multiplican la contención de disco.

9) ¿Puedo tener réplicas de lectura multi-región para baja latencia?

Sí, pero compras latencia a cambio de estaleidad. La replicación asíncrona entre regiones se atrasará con cualquier tasa de escritura significativa.
Para lecturas sensibles a la corrección, aún necesitarás pinning al primario o fencing, y eso suele traer tráfico de vuelta a la región de escritura.

10) ¿Qué frase debo recordar cuando me tienta “simplemente añadir réplicas”?

“La esperanza no es una estrategia.” — idea parafraseada atribuida a cultura de ingeniería y operaciones.
La versión operacional es: mide lag, aplica presupuestos y diseña para la consistencia que realmente necesitas.

Próximos pasos que puedes hacer esta semana

Si quieres réplicas que ayuden en vez de mentir, haz esto en orden:

  1. Escribe tu presupuesto de estaleidad: por endpoint o flujo. Si no puedes, asume 0 para todo lo que implique dinero, autenticación o permisos.
  2. Mide lag con el primitivo correcto: conjuntos GTID en MySQL; receive/replay lag y LSN en Postgres.
  3. Añade una regla de enrutamiento: pin-to-primary tras escrituras, o implementa fencing GTID/LSN para sesiones.
  4. Drena réplicas obsoletas automáticamente: una réplica fuera de presupuesto debe tratarse como una falla parcial, no como una molestia menor.
  5. Separa roles: no ejecutes reporting de fin de trimestre en el mismo standby que planeas promover en un fallo primario.
  6. Arregla las peores consultas: las réplicas no son excusa para mantener una consulta de 90 segundos en producción. Hazla rápida o aíslala.

Las réplicas son una herramienta. Las buenas herramientas aún cortan cuando se usan sin cuidado. Si diseñas tu ruta de lectura con reglas explícitas de frescura y las aplicas,
las réplicas se vuelven aburridas—y lo aburrido es lo que quieres a las 3 a.m.

← Anterior
Instantáneas ZFS: el superpoder que también puede llenar tu pool
Siguiente →
ZFS para Docker: diseño de datasets que evita la explosión de capas

Deja un comentario