PostgreSQL vs Percona Server: escalar lecturas — réplicas que funcionan vs réplicas que perjudican

¿Te fue útil?

Las réplicas de lectura se supone que son el botón fácil. Añades un nodo, apuntas las analíticas a él, reduces la carga en el primario y te vas a casa a tiempo. En producción, las réplicas a menudo hacen lo contrario: amplifican la contención por bloqueos, introducen lecturas obsoletas que rompen la lógica del negocio y convierten silenciosamente “escalar lecturas” en “depurar lag de replicación a las 2 a. m.”

Este artículo es para las personas que operan bases de datos, no para quienes coleccionan PDFs de vendedores. Compararemos PostgreSQL y Percona Server (compatible con MySQL) desde la perspectiva que importa: cómo se comportan las réplicas bajo carga real, cómo fallan y qué perillas realmente vale la pena tocar.

Un modelo mental útil: qué significa realmente «escalar lecturas»

Escalar lecturas con réplicas no es un solo problema. Son tres:

  1. Capacidad: ¿Pueden las réplicas ejecutar tu carga de lectura lo suficientemente rápido sin dejar sin recursos al primario ni entre ellas?
  2. Frescura: ¿Cuánto pueden estar desactualizados los resultados antes de que tu producto se vuelva “creativamente incorrecto”?
  3. Correctitud durante el failover: ¿Puedes promover una réplica sin perder escrituras o corromper las suposiciones de la aplicación?

PostgreSQL y Percona Server afrontan estos trade-offs de forma distinta.

La replicación física de PostgreSQL (streaming WAL) es conservadora: primero la correctitud y la simplicidad, y está dispuesta a cancelar consultas de lectura en réplicas para mantener la recuperación en marcha. Percona Server hereda la genética de replicación de MySQL: versátil, ampliamente conocida y muy capaz—pero con una historia de incidentes “funcionó en staging” causados por sutiles detalles de consistencia.

El escalado de lecturas también vive por encima de la base de datos. Si no tienes disciplina de enrutamiento—separación lectura/escritura, persistencia de sesión, manejo de “leer tus propias escrituras”—sólo estás distribuyendo confusión a más hosts.

Una verdad operativa que sigue apareciendo: si tus réplicas no son mediblemente más rápidas para las consultas objetivo que el primario, las réplicas son un impuesto, no una característica. Cuestan hardware, atención operativa y modos de fallo. Haz que lo merezcan.

Hechos interesantes y contexto histórico (que explican los bordes filosos de hoy)

  • “Hot standby” de PostgreSQL llegó en 9.0 (2010): antes de eso, las réplicas físicas eran mayormente para failover, no para servir lecturas en vivo.
  • La replicación de MySQL empezó como envío asíncrono de binlogs: la norma cultural se volvió “las réplicas pueden atrasarse,” y muchos patrones de aplicación asumen eso silenciosamente.
  • La replicación semisíncrona de MySQL se introdujo para reducir la brecha: mejora las garantías de durabilidad pero puede limitar el rendimiento bajo latencia.
  • La replicación de PostgreSQL está basada en WAL y orientada a páginas: es eficiente y determinista, pero acopla el progreso de la réplica al replay y a las interacciones de vacuum.
  • GTID en MySQL/Percona hizo el failover más razonable: reduce el caos de “¿en qué posición de binlog estamos?” que arruinaba fines de semana.
  • Percona Server añadió instrumentación operativa temprano: características como métricas extra y buena integración con performance schema lo hicieron popular entre equipos SRE que gustan de la evidencia.
  • La replicación lógica de PostgreSQL maduró más tarde (10+): abrió replicación selectiva y actualizaciones sin downtime, pero no es una bala de plata para escalar lecturas.
  • MySQL ha soportado desde hace tiempo topologías avanzadas y multi-source: puedes hacer cosas ingeniosas, y lo ingenioso trae fallos ingeniosos.

Réplicas de PostgreSQL: por qué suelen comportarse y cuándo muerden

Cómo funcionan las réplicas de lectura de PostgreSQL en la práctica

En la replicación por streaming clásica de PostgreSQL, el primario genera WAL (write-ahead log). Las réplicas reciben el WAL y lo reproducen. Las lecturas en una réplica se realizan contra una instantánea consistente de la base de datos hasta la posición de WAL reproducida.

Ese paso de replay importa. Si el replay es lento—IO, CPU, bloqueos en la standby o manejo de conflictos—el lag de replicación crece. La réplica está viva y contestando consultas, pero las contesta desde el pasado.

El modo de fallo “réplicas que perjudican”: conflictos de recuperación

PostgreSQL tiene una opinión: la recuperación debe avanzar. Si una consulta de larga duración en la standby entra en conflicto con el replay de WAL (comúnmente debido a limpieza de vacuum en el primario), PostgreSQL puede cancelar la consulta en la standby para mantener el replay en marcha. Verás errores como canceling statement due to conflict with recovery.

Esto no es PostgreSQL siendo cruel; es PostgreSQL siendo honesto. Le estás pidiendo a una réplica que sea a la vez máquina del tiempo y almacén de datos. Elige una cosa o ingeniería para evitarlo:

  • Usa hot_standby_feedback con cuidado para reducir cancelaciones (pero acepta riesgo de bloat en el primario).
  • Configura max_standby_streaming_delay para cambiar lag de replay por finalización de consultas.
  • Enruta consultas analíticas largas a una réplica lógica o un sistema dedicado, no a tu standby de HA.

Controles de frescura que importan

PostgreSQL te da palancas para acotar la obsolescencia. La mejor operativamente es synchronous_commit junto con ajustes de replicación síncrona (synchronous_standby_names). Puede asegurar que al menos una standby confirme recepción (o aplicación) antes de que commit retorne.

La cuenta oculta: la latencia de la red pasa a formar parte de tu camino de escritura. Si tu producto es sensible a la latencia y tu standby está en otra región, estás pagando efectivamente un envío internacional por cada commit.

Rendimiento de réplicas en Postgres: no ignores lo básico

Los nodos standby necesitan el mismo cariño que los primarios: shared buffers dimensionados razonablemente, effective_cache_size realista y almacenamiento que pueda seguir el ritmo de lecturas aleatorias además de las escrituras de replay WAL. Una standby con discos lentos puede atrasarse incluso si apenas sirve consultas.

Además, las réplicas de Postgres no son “lecturas gratis.” Cada consulta necesita CPU, memoria e IO. Si apuntas una flota de dashboards a una sola standby sin pooling de conexiones, puedes DDoSear tu propia base de datos de forma educada.

Chiste #1: Una réplica de lectura es como un becario tomando notas—útil hasta que le pides predecir el futuro, entonces entra en pánico y borra tu hoja de cálculo.

Réplicas de Percona Server: rápidas, familiares y llenas de trampas

Qué estás ejecutando realmente cuando ejecutas Percona Server

Percona Server es compatible con MySQL, típicamente usado como reemplazo drop-in con instrumentación extra y características de rendimiento. Para escalar lecturas, los comportamientos centrales vienen de la replicación de MySQL: el primario escribe binlogs, las réplicas los obtienen y los aplican. Históricamente, el apply ha sido single-threaded; versiones más nuevas ofrecen replicación paralela, pero sólo si lo configuras y tu carga coopera.

El dolor clásico: el lag de replicación no es un bug, es por defecto

Muchas configuraciones MySQL/Percona tratan las réplicas como “caches eventualmente consistentes.” Está bien hasta que alguien enruta lecturas críticas de negocio a réplicas sin un contrato de frescura. Si tu aplicación hace “escribir, luego leer, luego cobrar dinero,” el lag se vuelve tickets de soporte y finanzas enfadadas.

Semisync, GTID y la ilusión de seguridad

La replicación semisíncrona ayuda haciendo que el primario espere a que al menos una réplica reconozca la transacción antes de commitear (según la configuración). Reduce la ventana de pérdida ante fallas del primario, pero no asegura mágicamente que las réplicas estén al día para lecturas. Recibir no es aplicar.

La replicación basada en GTID hace que el failover y la gestión de topologías sean menos propensos a errores, pero no te protege de un enrutamiento inseguro. GTID resuelve “en qué transacción estamos”, no “qué asume la app”.

El rendimiento de la réplica suele ser un problema de apply, no de consulta

Cuando una réplica Percona se atrasa, los equipos suelen añadir CPU o aumentar buffer pool esperando mejorar. A veces eso funciona. A menudo la réplica está limitado en el apply del hilo SQL: una transacción grande, un cambio de esquema, una tabla caliente o ajustes de paralelismo insuficientes.

La replicación paralela puede ser beneficiosa, pero tiene prerrequisitos. Si tu carga se basa mayormente en contención de una sola tabla o transacciones serializadas grandes, los workers paralelos estarán inactivos esperando su turno—como una reunión donde todos están “alineados” pero nada sale.

Campos minados de correctitud en réplicas: statement vs row y comportamiento de DDL

La mejor práctica moderna es la replicación basada en row para seguridad. La replicación basada en statement puede desviarse con funciones no deterministas o diferencias de zona horaria. El modo mixto es un compromiso, y los compromisos son donde nacen los incidentes.

El DDL también puede bloquear el apply de replicación o crear paradas largas. Los cambios de esquema en línea ayudan, pero introducen su propia complejidad operativa y pueden salir mal si tu tooling no está afinado para tu carga.

Chiste #2: La replicación paralela es como contratar más mudaderos—genial hasta que te das cuenta de que el sofá sólo entra por la puerta de una manera.

Separación lectura/escritura: patrones de enrutamiento que no te prenden fuego

Patrón 1: “Leer tus propias escrituras” con persistencia de sesión

Si un usuario acaba de escribir datos y espera leerlos inmediatamente, tienes dos opciones sensatas:

  • Enrutar esa sesión al primario por una ventana (persistencia al primario tras una escritura), y luego permitir lecturas en réplicas más tarde.
  • Usar una valla de frescura: leer sólo de réplicas que hayan confirmado aplicar al menos el LSN/GTID de la escritura (más complejo, pero preciso).

Todo lo demás es ingeniería basada en esperanza.

Patrón 2: Dividir por carga, no por endpoint

No enrutes “todos los SELECT” a las réplicas. Enruta clases de consultas específicas. Analíticas, informes y consultas de dashboards son buenos candidatos—si toleran obsolescencia y proteges las réplicas de concurrencia descontrolada.

Por el contrario, “SELECT … FOR UPDATE” pertenece al primario. También lo hacen lecturas que deben ver estado inmediatamente consistente (comprobaciones de inventario, claves de idempotencia, lógica antifraude).

Patrón 3: Designar réplicas por propósito

En PostgreSQL, una hot standby para HA no es lo mismo que un nodo de pool de lectura para BI. En Percona, una réplica usada para backups y ETL no es la misma que la que promoverías durante un incidente. Mezclar propósitos crea inversiones de prioridad: tus backups estrangulan a tu candidato de failover, o tus dashboards causan cancelaciones que aumentan el bloat del primario.

Patrón 4: Pon un gobernador delante de las réplicas

Los poolers de conexión, timeouts de consulta y límites de recursos por rol se pagan solos. La réplica es un recurso compartido. Trátala como tal. “Conexiones ilimitadas de dashboards” es cómo aprendes qué se siente al agotar descriptores de archivo.

Idea parafraseada (atribuida): John Allspaw sostiene que la fiabilidad viene de aprender y mejorar sistemas, no de culpar a individuos después de fallos.

Tareas prácticas: comandos, salidas y decisiones (haz esto en producción)

Estas son las tareas que realmente ejecuto cuando las réplicas se portan mal. Cada una incluye: el comando, qué significa la salida y qué decisión tomar a continuación.

PostgreSQL: salud de réplicas y lag

Task 1: Check replication state from the primary

cr0x@server:~$ psql -X -c "SELECT application_name, client_addr, state, sync_state, write_lag, flush_lag, replay_lag FROM pg_stat_replication;"
 application_name | client_addr  |   state   | sync_state | write_lag | flush_lag | replay_lag
-----------------+--------------+-----------+------------+-----------+-----------+-----------
 standby-a        | 10.0.2.11    | streaming | async      | 00:00:00  | 00:00:00  | 00:00:02
 standby-b        | 10.0.2.12    | streaming | async      | 00:00:00  | 00:00:01  | 00:00:15
(2 rows)

Significado: El lag se desglosa por etapa. El replay lag es lo que sienten tus lecturas. Standby-b va 15s por detrás en replay.

Decisión: Si el replay lag se correlaciona con incidentes de lecturas obsoletas, enruta lecturas sensibles lejos de standby-b o arregla los cuellos de botella de replay.

Task 2: Measure byte lag precisely (LSN diff)

cr0x@server:~$ psql -X -c "SELECT application_name, pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS bytes_behind FROM pg_stat_replication;"
 application_name | bytes_behind
-----------------+--------------
 standby-a        |       524288
 standby-b        |     73400320
(2 rows)

Significado: Standby-b está ~70MB por detrás. El lag en tiempo puede parecer pequeño hasta que llega un pico; los bytes muestran el backlog.

Decisión: Si los bytes_behind crecen de forma sostenida, tienes subcapacidad sostenida (red, disco, CPU o conflictos de apply).

Task 3: On the standby, check replay timestamp lag

cr0x@server:~$ psql -X -c "SELECT now() - pg_last_xact_replay_timestamp() AS replay_delay;"
 replay_delay
--------------
 00:00:14.832
(1 row)

Significado: “¿Qué tan en el pasado está la última transacción aplicada?” Muy cercano a la obsolescencia que experimentan los usuarios.

Decisión: Si replay_delay supera la tolerancia de tu producto, deja de usar esa standby para lecturas críticas en frescura.

Task 4: See if standby queries are being canceled

cr0x@server:~$ sudo journalctl -u postgresql -n 50 --no-pager
Dec 30 10:12:01 standby-a postgresql[1832]: ERROR:  canceling statement due to conflict with recovery
Dec 30 10:12:01 standby-a postgresql[1832]: DETAIL:  User query might have needed to see row versions that must be removed.
Dec 30 10:12:01 standby-a postgresql[1832]: STATEMENT:  SELECT ... FROM events WHERE ...

Significado: Conflictos de hot standby. El replay WAL está limpiando versiones de filas; tu consulta las necesita.

Decisión: O bien mueve consultas largas fuera de la standby, o acepta el trade-off de hot_standby_feedback=on (con un plan para el bloat), o ajusta los parámetros de delay de la standby.

Task 5: Check if hot_standby_feedback is enabled

cr0x@server:~$ psql -X -c "SHOW hot_standby_feedback;"
 hot_standby_feedback
---------------------
 off
(1 row)

Significado: La standby no le dice al primario que retenga versiones antiguas de filas por más tiempo.

Decisión: Si estás cancelando lecturas importantes, considera activarlo—pero sólo después de revisar el bloat del primario y el margen de vacuum.

Task 6: Verify synchronous replication settings (freshness contract)

cr0x@server:~$ psql -X -c "SHOW synchronous_commit; SHOW synchronous_standby_names;"
 synchronous_commit
-------------------
 on
(1 row)

 synchronous_standby_names
--------------------------
 standby-a
(1 row)

Significado: Los commits esperan a standby-a (dependiendo del modo sync). Esto puede limitar la latencia de escritura pero ajusta el RPO.

Decisión: Si tu presupuesto de latencia de escritura no lo permite, no finjas que tienes durabilidad síncrona. Pasa a async y diseña para eso, o mantén sync pero asegura que standby-a sea de baja latencia y bien aprovisionada.

Task 7: Find top read queries on the standby

cr0x@server:~$ psql -X -c "SELECT query, calls, mean_exec_time, rows FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 5;"
                     query                      | calls | mean_exec_time | rows
------------------------------------------------+-------+----------------+------
 SELECT * FROM report_rollups WHERE ...          |   120 |        842.123 |  1200
 SELECT ... JOIN ... WHERE ...                   |  5400 |        120.512 |    40
(2 rows)

Significado: Tu réplica es lenta porque le piden hacer cosas costosas, repetidamente.

Decisión: Añade índices, reduce conjuntos de resultados, cachea o mueve esa carga a un almacén analítico dedicado. “Rélica” no es una palabra mágica que convierte malas consultas en buenas.

Percona Server: salud de réplicas y lag de apply

Task 8: On a replica, check replication threads and lag

cr0x@server:~$ mysql -e "SHOW REPLICA STATUS\G"
*************************** 1. row ***************************
             Replica_IO_Running: Yes
            Replica_SQL_Running: Yes
                   Seconds_Behind_Source: 27
                  Retrieved_Gtid_Set: 1-100-984433
                   Executed_Gtid_Set: 1-100-984120
                Replica_SQL_Running_State: Waiting for dependent transaction to commit

Significado: IO está bien, el apply SQL está atrasado. Los sets GTID muestran cuánto. El estado SQL insinúa dependencia/serialización.

Decisión: Enfócate en el rendimiento del apply: configuración paralela, tamaño de transacciones, tablas/filas calientes y rendimiento de disco para redo/undo.

Task 9: Check parallel replication configuration

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'replica_parallel%'; SHOW VARIABLES LIKE 'slave_parallel%';"
+-----------------------------------+-------+
| Variable_name                     | Value |
+-----------------------------------+-------+
| replica_parallel_workers          | 8     |
| replica_parallel_type             | LOGICAL_CLOCK |
+-----------------------------------+-------+
+-----------------------------------+-------+
| Variable_name                     | Value |
+-----------------------------------+-------+
| slave_parallel_workers            | 8     |
| slave_parallel_type               | LOGICAL_CLOCK |
+-----------------------------------+-------+

Significado: Workers paralelos activados. Si el lag persiste, tu carga puede no paralelizarse bien o estás limitado en otro lugar.

Decisión: Si los workers son 0, actívalos con cuidado; si ya están activados, investiga transacciones grandes o stalls de disco en vez de “añadir más workers.”

Task 10: Find the largest recent transactions (binlog apply pain)

cr0x@server:~$ mysql -e "SELECT THREAD_ID, EVENT_NAME, TIMER_WAIT/1000000000000 AS seconds, SQL_TEXT FROM performance_schema.events_statements_history_long ORDER BY TIMER_WAIT DESC LIMIT 3;"
+-----------+--------------------------+---------+--------------------------------------+
| THREAD_ID | EVENT_NAME               | seconds | SQL_TEXT                              |
+-----------+--------------------------+---------+--------------------------------------+
|      2213 | statement/sql/insert     |  12.421 | INSERT INTO audit_log SELECT ...      |
|      1987 | statement/sql/update     |   9.812 | UPDATE orders SET status='...' WHERE...|
+-----------+--------------------------+---------+--------------------------------------+

Significado: Las sentencias grandes pueden serializar el apply y crear picos de lag.

Decisión: Divide transacciones monstruo, evita “INSERT INTO … SELECT” en horas pico y considera patrones de batching que respeten la replicación.

Task 11: Check InnoDB pressure on the replica (buffer pool and IO)

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';"
+--------------------------+----------+
| Variable_name            | Value    |
+--------------------------+----------+
| Innodb_buffer_pool_reads | 18399231 |
+--------------------------+----------+
+----------------------------------+------------+
| Variable_name                    | Value      |
+----------------------------------+------------+
| Innodb_buffer_pool_read_requests | 9912837712 |
+----------------------------------+------------+

Significado: Si las lecturas del buffer pool aumentan rápido relativo a las requests, estás fallando en cache y golpeando disco. Las réplicas que hacen analíticas tienden a eso.

Decisión: Aumenta buffer pool (si la memoria lo permite), reduce el working set, añade índices o mueve analíticas fuera de la réplica.

Task 12: Check if the replica is throttled by fsync/IO

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

Device            r/s     w/s   rMB/s   wMB/s  await  %util
nvme0n1         120.0   410.0     8.1    52.3   18.4  98.7

Significado: El disco está saturado (%util ~99%). El await es alto. El apply y las lecturas compiten por IO.

Decisión: Mueve redo/binlog a almacenamiento más rápido, reduce la carga de lectura, ajusta cuidadosamente parámetros de flush o añade réplicas para repartir la carga de lectura.

Task 13: Verify binlog/GTID mode from the source (failover readiness)

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'gtid_mode'; SHOW VARIABLES LIKE 'enforce_gtid_consistency'; SHOW MASTER STATUS\G"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| gtid_mode     | ON    |
+---------------+-------+
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| enforce_gtid_consistency | ON    |
+--------------------------+-------+
*************************** 1. row ***************************
             File: binlog.000812
         Position: 94822133
Executed_Gtid_Set: 1-100-984433

Significado: GTID está activado y consistente. Esa es tu base para herramientas de failover sensatas.

Decisión: Si GTID está off en 2025, planifica tiempo para arreglarlo. Puedes seguir sin él, pero eliges operaciones manuales propensas a errores.

Task 14: Spot read routing mistakes via processlist

cr0x@server:~$ mysql -e "SHOW PROCESSLIST;"
+-----+------+-----------------+------+---------+------+------------------------+------------------------------+
| Id  | User | Host            | db   | Command | Time | State                  | Info                         |
+-----+------+-----------------+------+---------+------+------------------------+------------------------------+
| 101 | app  | 10.9.1.25:42110  | prod | Query   |   12 | Sending data           | SELECT ... FOR UPDATE ...    |
| 142 | app  | 10.9.1.25:42144  | prod | Query   |    8 | Waiting for table lock | UPDATE users SET ...         |
+-----+------+-----------------+------+---------+------+------------------------+------------------------------+

Significado: Escrituras y lecturas que bloquean en una réplica indican enrutamiento roto. Algunos proxies clasifican mal consultas; algunas apps ignoran la intención lectura/escritura.

Decisión: Arregla reglas de enrutamiento y aplica usuarios de sólo lectura en réplicas. Si la réplica es escribible, alguien escribirá en ella—accidentalmente o “temporalmente”.

Tasks transversales: OS, red y realidad de capacidad

Task 15: Check network retransmits (replication stream quality)

cr0x@server:~$ ss -ti dst :5432
ESTAB 0 0 10.0.2.11:50824 10.0.1.10:5432
	 cubic wscale:7,7 rto:214 rtt:3.1/1.2 ato:40 mss:1448 pmtu:1500 rcvmss:1448 advmss:1448 cwnd:10 bytes_sent:248120122 bytes_retrans:81234 bytes_acked:248038888

Significado: Hay retransmisiones; si son altas y crecen, tu “lag de replicación” es en parte un problema de red.

Decisión: Arregla la pérdida de paquetes antes de tunear perillas de base de datos. Las bases de datos son buenas en muchas cosas; la terapia contra pérdida de paquetes no es una de ellas.

Task 16: Confirm a replica is actually read-only at the database level

cr0x@server:~$ psql -X -c "SHOW transaction_read_only;"
 transaction_read_only
-----------------------
 off
(1 row)

Significado: Esta sesión puede escribir. En una standby, las escrituras fallarán, pero en un nodo promovido o mal configurado, esto puede ser peligroso.

Decisión: Aplica roles de sólo lectura y restricciones en el proxy/aplicación. Confiar en “es una standby” no es una política.

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'read_only'; SHOW VARIABLES LIKE 'super_read_only';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| read_only     | ON    |
+---------------+-------+
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| super_read_only | ON    |
+-----------------+-------+

Significado: La réplica está protegida incluso frente a usuarios privilegiados (con matices).

Decisión: Activa super_read_only en réplicas que nunca deben aceptar escrituras.

Guía rápida de diagnóstico

Cuando alguien te dice “las réplicas están lentas” o “las lecturas son inconsistentes,” no tienes tiempo para debatir filosóficamente sobre el teorema CAP. Haz esto.

Primero: decide si es frescura o rendimiento

  • PostgreSQL: comprueba now() - pg_last_xact_replay_timestamp() en la réplica y pg_stat_replication en el primario.
  • Percona: comprueba Seconds_Behind_Source y el estado del hilo SQL, además de GTID ejecutado vs recuperado.

Si el lag es alto, la queja de “lecturas lentas” puede ser bugs de lecturas obsoletas, no consultas lentas.

Segundo: encuentra la categoría del cuello de botella

  1. Cuello de botella de apply/replay: IO saturado, hilo SQL serializado, replay WAL atascado, conflictos/cancelaciones.
  2. Cuello de botella de consulta: consultas costosas, índices faltantes, demasiada concurrencia, planes malos.
  3. Cuello de botella del sistema: CPU steal, presión de memoria, pérdida de red, vecino ruidoso.

Tercero: toma una decisión de enrutamiento inmediatamente

  • Si el lag > tolerancia: enruta lecturas sensibles al primario o a una réplica más saludable.
  • Si la réplica está sobrecargada: limita la concurrencia (pooling, timeouts) y descarga tráfico de lectura no crítico.
  • Si el apply está atrasado: deja de enviar analíticas pesadas al candidato de HA. Déjalo ponerse al día.

Cuarto: arregla con la palanca más pequeña que cambia el resultado

Los problemas de réplica a menudo se solucionan con acciones aburridas: un índice, una reescritura de consulta, una reducción de conexiones, una mejora de IO. Resiste la tentación de rediseñar la topología de replicación durante un incidente.

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

Mistake 1: “Podemos enviar todos los SELECT a réplicas”

Sintomas: Lecturas obsoletas al azar, filas “faltantes”, estado de UI inconsistente después de actualizaciones.

Causa raíz: No hay contrato de frescura; el lag asíncrono es normal bajo carga.

Solución: Implementa “leer tus propias escrituras” (persistencia al primario tras escritura o vallas LSN/GTID). Enruta sólo clases de consultas tolerantes a obsolescencia a réplicas.

Mistake 2: La standby de Postgres cancela consultas BI

Sintomas: Errores sobre conflicto con recuperación, dashboards que agotan el tiempo durante periodos con mucho vacuum.

Causa raíz: Conflicto de hot standby entre replay WAL y snapshots de larga duración.

Solución: Mueve BI a una réplica dedicada/replicación lógica; o activa hot_standby_feedback con un plan para bloat; o acota tiempos de ejecución de consultas agresivamente.

Mistake 3: La réplica Percona “está sana” porque el hilo IO está corriendo

Sintomas: Hilo IO Yes, hilo SQL Yes, pero el lag sigue creciendo; las lecturas se vuelven cada vez más obsoletas.

Causa raíz: El throughput de apply es insuficiente (serialización del hilo SQL, transacciones grandes, saturación de disco).

Solución: Ajusta replicación paralela, reduce tamaño de transacciones, arregla tablas hotspot y provisiona IO para apply además de lectura.

Mistake 4: Réplicas usadas para backups, reporting y failover

Sintomas: El candidato de failover siempre va por detrás; promover causa mayor ventana de pérdida de datos; los backups ralentizan el clúster.

Causa raíz: Réplicas multi-propósito crean prioridades en competencia y lag impredecible.

Solución: Asigna roles: al menos una réplica HA “limpia”, réplicas de pool de lectura separadas, y un nodo separado para reporting/ETL si hace falta.

Mistake 5: Sobre-optimizar durabilidad en la capa incorrecta

Sintomas: Las escrituras se vuelven más lentas tras habilitar sync/semisync; picos en p99; la carga de incidentes aumenta.

Causa raíz: La latencia ahora está ligada al RTT de la réplica y a fsync. La réplica no estaba aprovisionada para esa responsabilidad.

Solución: Si necesitas mayor durabilidad, mantén objetivos síncronos cercanos y rápidos. De lo contrario acepta async e ingeniera explícitamente en torno a RPO/RTO.

Mistake 6: El pool de lectura se derrite por tormentas de conexiones

Sintomas: Réplicas muestran alto cambio de contexto CPU, muchas conexiones inactivas, presión de memoria, picos súbitos de latencia.

Causa raíz: Sin pooling o límites de concurrencia; dashboards y jobs por lotes abren muchas conexiones.

Solución: Usa PgBouncer o ProxySQL (o equivalente), fija tamaños de pool, aplica timeouts e aisla jobs por lotes.

Tres mini-historias de la vida corporativa (anonimizadas, dolorosamente familiares)

Incidente causado por una suposición incorrecta: “Las lecturas no pueden romper flujos de dinero”

Una plataforma de suscripción mediana movió “la mayoría de lecturas” a réplicas compatibles con MySQL para proteger el primario. Era un objetivo razonable: reducir CPU del primario, mantener estable la latencia de escritura y escalar horizontalmente. Un proxy hacía la separación basándose en si la consulta empezaba con SELECT.

Dos semanas después, el soporte notó un patrón: cancelaciones que deberían haber sido inmediatas a veces parecían “pendientes” durante minutos. Finanzas observó algo peor: cargos dobles ocasionales cuando los usuarios reintentaban acciones. Nadie había cambiado la lógica de facturación. Los gráficos de la base de datos parecían bien—hasta que revisaron el lag de réplica durante tráfico pico.

La suposición errónea fue simple: “las lecturas de facturación son solo lecturas.” Pero el flujo de facturación hacía escribir, luego leer para verificar estado antes de llamar a un proveedor de pago externo. Cuando esa verificación llegó a una réplica atrasada, vio el estado antiguo y reintentó. El sistema se comportó de forma consistente, sólo que incorrecta.

La solución no fue exótica. Añadieron persistencia al primario tras escritura por sesión de usuario y marcaron ciertas rutas de consulta como sólo primario. El lag aún ocurría a veces, pero dejó de ser un bug de correctitud. También empezaron a alertar sobre “lag por encima de la tolerancia de negocio”, no “lag por encima de un número aleatorio”.

Optimización que salió mal: “Hagamos que la standby deje de cancelar consultas”

Otra compañía ejecutaba PostgreSQL con una hot standby que servía dashboards. El equipo de BI se quejó de consultas canceladas durante periodos de muchas escrituras. El equipo de bases de datos activó hot_standby_feedback=on para que la standby dejara de perder consultas largas.

Los dashboards se estabilizaron. Todos celebraron. Luego el uso de disco en el primario empezó a subir lentamente de una forma que no encajaba con el crecimiento de datos. La actividad de autovacuum aumentó, pero el bloat de tablas seguía creciendo. Los scans de índices se ralentizaron. El primario trabajaba más para hacer el mismo trabajo.

Qué ocurrió: hot_standby_feedback evitó la limpieza por vacuum de tuplas muertas que la standby podría necesitar. Las consultas largas en la standby efectivamente le decían al primario “mantén versiones antiguas,” y el primario lo hacía hasta que se volvió un problema de almacenamiento y rendimiento.

Revirtieron el cambio y separaron cargas: una standby de HA optimizada para replay y failover, y una réplica de reporting separada con timeouts de consulta controlados y un calendario para informes pesados. Al equipo de BI no le encantaron las restricciones, pero el primario dejó de asfixiarse lentamente.

Práctica aburrida pero correcta que salvó el día: “Una réplica es sagrada”

Una empresa SaaS tenía una regla que sonaba burocrática: una réplica en cada clúster es el candidato a promoción, y no se usa para lecturas ad-hoc, ETL, backups ni experimentos. La gente se quejaba de que era “hardware desperdiciado.” No lo era.

Durante un incidente de almacenamiento en un primario, tuvieron que hacer failover bajo presión. La réplica candidata a promoción estaba consistentemente dentro de un estrecho límite de lag, porque estaba protegida de cargas ruidosas y su presupuesto de IO estaba reservado para el apply. La promoción fue rápida y predecible.

Mientras tanto, otra réplica que servía reporting iba minutos atrás debido a un job por lotes que hacía barridos grandes. Si ese nodo se hubiese promovido, la ventana de pérdida de escrituras habría sido materialmente mayor y la recuperación habría exigido conciliación a nivel de negocio. En cambio, fue un failover estándar y un postmortem estándar.

La práctica no era glamorosa. No requirió herramientas nuevas. Salvó el día al reducir las sorpresas. En trabajo de fiabilidad, reducir sorpresas es básicamente todo el trabajo.

Listas de verificación / plan paso a paso: réplicas que funcionan

Step 1: Define el contrato de frescura en términos del negocio

  • Lista los flujos que deben “leer tus propias escrituras” (pagos, auth, inventario, claves de idempotencia, acciones de administración).
  • Define la obsolescencia máxima aceptable para todo lo demás (segundos, no sensaciones).
  • Convierte eso en reglas de enrutamiento y alertas.

Step 2: Separa roles de réplica

  • Réplica HA: tráfico de lectura mínimo, afinada para replay/apply, monitorizada para preparación de promoción.
  • Réplicas de pool de lectura: sirven lecturas de la aplicación que toleran lag; protegidas por pooling y timeouts.
  • Reporting/ETL: nodo separado si la carga es pesada o de larga duración.

Step 3: Haz el enrutamiento aplicable

  • Usa usuarios/roles distintos para primario vs réplica.
  • En réplicas Percona: habilita super_read_only.
  • En Postgres: usa default_transaction_read_only para roles de réplica (y aplica también en el proxy/capa de la app).

Step 4: Limita la concurrencia y el tiempo de ejecución

  • Instala pooling de conexiones; fija límites duros por servicio.
  • Configura timeouts de sentencias para dashboards y herramientas ad-hoc.
  • Vigila transacciones largas; destrozan ambos sistemas de distintas formas, pero los destrozan.

Step 5: Asegura que las réplicas tengan IO suficiente para dos trabajos

  • Las réplicas deben aplicar cambios y servir lecturas. Eso es IO de escritura + lectura.
  • Mide saturación de disco durante tráfico pico, no un martes al mediodía.
  • Si el almacenamiento es el cuello de botella, tunear SQL es un pasatiempo, no una solución.

Step 6: Prueba el failover como si lo necesitaras

  • Mide: tiempo de promoción, tiempo de reconexión de clientes y ventana de pérdida de datos (o confirmación de ninguna, si es síncrono).
  • Verifica la configuración del nodo promovido: flags de sólo lectura apagados, server_id correcto, configuración de replicación correcta, backups correctos.
  • Practica restaurar una nueva réplica desde backup + catch-up. Si no puedes hacer eso, no tienes un sistema de replicación; tienes mascotas.

Preguntas frecuentes

1) ¿Las réplicas de PostgreSQL son «más consistentes» que las de Percona?

Por defecto, ambas suelen ser asíncronas, lo que significa que ambas pueden estar obsoletas. PostgreSQL tiende a ser más explícito sobre conflictos (cancela consultas) en lugar de quedarse atrás silenciosamente, pero la obsolescencia sigue siendo real.

2) ¿Cuál es el enfoque más simple y seguro para escalar lecturas?

Enruta sólo lecturas tolerantes a obsolescencia a réplicas, mantén “leer tus propias escrituras” en el primario mediante persistencia de sesión y limita la concurrencia en réplicas con pooling.

3) ¿Por qué mi standby de Postgres está cancelando consultas?

Los conflictos de hot standby ocurren cuando el replay WAL necesita eliminar versiones antiguas de filas (a menudo por vacuum) pero una consulta de la standby aún las necesita. Soluciona moviendo consultas largas, afinando el delay de la standby o usando hot_standby_feedback con cuidado.

4) ¿Por qué Seconds_Behind_Source a veces miente?

En MySQL/Percona puede ser NULL o engañoso durante ciertos estados, y es una métrica basada en tiempo en lugar de backlog. Compara sets GTID y observa el estado del hilo apply para una imagen más clara.

5) ¿Debo usar replicación síncrona para hacer las réplicas seguras para lecturas?

Los ajustes síncronos ayudan la durabilidad y reducen la pérdida de datos en failover. No garantizan automáticamente “lecturas seguras” a menos que también asegures que las lecturas vayan a nodos que hayan aplicado las transacciones necesarias. Sigue siendo un problema de enrutamiento.

6) ¿Puedo ejecutar analíticas en la réplica de HA?

Puedes, hasta el día en que necesites hacer failover y descubras que tu nodo de HA está retrasado o sin recursos. Si te importa un failover predecible, mantén una réplica aburrida y protegida.

7) ¿La replicación lógica es mejor para escalar lecturas?

A veces. La replicación lógica puede reducir algunos problemas de conflicto en replay y permitir replicar subconjuntos, pero añade complejidad operativa y no elimina la necesidad de disciplina de enrutamiento y tuning de rendimiento.

8) ¿Cuántas réplicas necesito?

Suficientes para manejar la concurrencia máxima de lectura pico con margen, más al menos un nodo reservado para HA (según tu diseño de failover). Si una réplica siempre está saturada, no tienes suficientes o estás enrutando la carga equivocada.

9) ¿Cuál es la mejor alerta para la salud de réplicas?

Alerta sobre lag relativo a la tolerancia de negocio, además de saturación de disco en réplicas y cancelaciones de consultas (Postgres) o stalls de apply (Percona). Las alertas que no mapean al impacto de usuario generan fatiga de alertas.

10) ¿Pueden los proxies automatizar completamente la separación lectura/escritura de forma segura?

Ayudan, pero no pueden leer los requisitos de tu producto. Un proxy puede clasificar consultas; no puede saber qué lecturas deben ser frescas. Debes codificar esa lógica en el enrutamiento de la aplicación o en la política de sesión.

Próximos pasos prácticos

  • Escribe el contrato de frescura para tus 10 flujos de usuario principales. Si no puedes declararlo, no puedes hacerlo cumplir.
  • Elige roles de réplica: una candidata HA mantenida limpia, una o más réplicas de pool de lectura y (si hace falta) un nodo de reporting/ETL.
  • Implementa guardarraíles de enrutamiento: persistencia al primario tras escritura, usuarios exclusivos para réplicas y enforcement de sólo lectura en réplicas.
  • Ejecuta los comandos anteriores semanalmente y durante picos: lag en tiempo y bytes, estado de apply, saturación de IO y consultas principales.
  • Arregla el cuello de botella más grande primero: discos saturados, concurrencia desbocada o una consulta patológica. No tunées perillas como sustituto de capacidad.
  • Practica el failover y mídelo. Si el failover es una teoría no probada, será probado por ti más tarde, en un mal momento.

Las réplicas pueden absolutamente escalar lecturas. También pueden escalar la confusión. La diferencia es si tratas la replicación como un sistema de ingeniería con contratos, presupuestos y observabilidad—o como un truco mágico que esperas que nadie te pida explicar.

← Anterior
Proxmox “pve-apt-hook failed”: por qué se bloquean las actualizaciones y cómo desbloquearlas de forma segura
Siguiente →
ZFS SAS vs SATA timeouts: por qué SAS parece ‘más estable’ bajo estrés

Deja un comentario