¿Cómo funcionan las réplicas de lectura y el read/write splitting, y cuál es la trampa (el lag de replicación)?
Enviar los SELECT a réplicas multiplica tu capacidad de lectura casi gratis... hasta que el lag de replicación hace que un usuario deje de ver su propio cambio recién guardado.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo funcionan las réplicas de lectura y el read/write splitting, y cuál es la trampa (el lag de replicación)?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#La topología
- Un primario (writer) acepta todos los
INSERT/UPDATE/DELETEy, en la mayoría de los motores, el DDL. - Una o más réplicas de lectura reciben continuamente un flujo de cambios desde el primario (mediante envío de WAL en Postgres, replicación de binlog en MySQL, o mecanismos específicos del motor) y los aplican localmente.
- La aplicación enruta las escrituras y las lecturas críticas de "leer mi propia escritura" al primario, y todo lo demás —reportes, dashboards, páginas de listado, búsqueda— a la réplica menos cargada.
Esto funciona porque en la gran mayoría de sistemas en producción las lecturas superan a las escrituras entre 10:1 y 1000:1. Las réplicas de lectura escalan horizontalmente el lado de lectura sin tocar para nada el camino de escritura, que suele ser el cuello de botella real que conviene posponer (shardear el writer es mucho más difícil).
#Cómo llegan realmente los datos
Dos modos de replicación dominantes:
- Replicación asíncrona — el primario confirma (commit) y responde al cliente de inmediato; el cambio se transmite a las réplicas después. Máximo throughput de escritura, pero las réplicas pueden atrasarse desde milisegundos hasta (bajo carga o con una partición de red) minutos.
- Replicación síncrona / semi-síncrona — el primario espera a que al menos una réplica (o un quórum) confirme la escritura antes de responder. Garantiza que no haya pérdida de datos en un failover y acota el atraso, al costo directo de latencia de escritura.
La mayoría de los sistemas usan async por defecto para las réplicas de escalado de lectura, y reservan la replicación síncrona (si acaso) para un número reducido de réplicas dedicadas específicamente a failover/HA, no al tráfico de lectura.
#El atraso de replicación: la trampa
El "lag" es el retraso entre el momento en que una escritura se confirma en el primario y el momento en que esa escritura se vuelve visible en una réplica. Proviene de:
- Latencia de red entre primario y réplica (peor entre regiones).
- La réplica aplicando cambios de forma single-threaded (el clásico hilo SQL único de MySQL) o estando limitada por CPU/IO debido a su propio tráfico de lectura.
- Transacciones largas o ráfagas pesadas de escritura en el primario que la réplica tiene que ponerse al día.
- Cambios de esquema grandes o jobs batch pesados que generan un muro de WAL/binlog por reproducir.
El síntoma que todo backend termina depurando: un usuario envía un formulario, la escritura se confirma contra el primario, la app de inmediato hace un SELECT contra una réplica para renderizar la página de confirmación — y la fila todavía no está ahí. Esta es la clásica violación de consistencia de lectura-tras-escritura (read-after-write), y es el trade-off central de las réplicas de lectura: estás cambiando consistencia estricta por escalabilidad de lectura.
#Arreglar read-after-write sin renunciar a las réplicas
- Enrutamiento sticky/por sesión: después de que un usuario escribe, enruta sus siguientes lecturas (durante una ventana, o durante esa sesión HTTP) al primario, o a la réplica específica que ya demostró estar al día.
- Tokens causales: la escritura devuelve una posición de log/LSN; la petición de lectura lleva ese token y el enrutador espera (o elige) una réplica cuya posición aplicada sea ≥ al token antes de responder.
- Leer del primario en rutas conocidas como críticas: no sobre-diseñes — enrutar "leer mi propio perfil justo después de editarlo" al primario, y dejar la navegación masiva en réplicas, resuelve el 90% de las quejas sin maquinaria extra.
- Monitorear y enrutar alrededor del atraso: las réplicas exponen su lag (
pg_stat_replication/SHOW SLAVE STATUS→Seconds_Behind_Master), así que un enrutador o proxy puede sacar automáticamente de rotación a una réplica no saludable, con atraso severo.
#Dónde vive la lógica de enrutamiento
- Basado en proxy: PgBouncer más una capa de enrutamiento, ProxySQL, o una capa administrada (RDS Proxy, el reader endpoint de Aurora) — la aplicación se conecta a un solo lugar y el proxy decide. Centraliza la lógica, pero añade un salto.
- A nivel de aplicación/ORM: la capa de acceso a datos elige un pool de conexiones según el tipo de sentencia o una pista explícita. Más visibilidad y control, más código que mantener.
Un enrutador mínimo a nivel de aplicación, que ilustra el arreglo de read-after-write con enrutamiento sticky:
import timeimport randomclass ReplicaRouter:def __init__(self, primary_pool, replica_pools, sticky_window_seconds=5):self.primary_pool = primary_poolself.replica_pools = replica_poolsself.sticky_window_seconds = sticky_window_secondsself._last_write_at = {} # user_id -> timestamp monotónicodef record_write(self, user_id):self._last_write_at[user_id] = time.monotonic()def get_connection_for_read(self, user_id):last_write = self._last_write_at.get(user_id)if last_write is not None:still_sticky = (time.monotonic() - last_write) < self.sticky_window_secondsif still_sticky:# Fuerza las lecturas de este usuario de vuelta al primario hasta# que expire la ventana, para que nunca vea "desaparecer" su propia escritura.return self.primary_pool.get_connection()healthy_replicas = [r for r in self.replica_pools if r.lag_seconds() < 2.0]pool = random.choice(healthy_replicas or [self.primary_pool])return pool.get_connection()def get_connection_for_write(self):return self.primary_pool.get_connection()
Los detalles clave: las escrituras siempre van a primary_pool; la ventana sticky significa que las siguientes lecturas de ese usuario en particular evitan las réplicas por completo, sin importar en cuál hubieran caído; y cualquier réplica que reporte más de 2 segundos de atraso queda excluida del pool antes de siquiera poder ser elegida, en lugar de confiar en ella y devolver datos obsoletos.
#Otros modos de falla que vale la pena nombrar
- Read-your-write a través de una cadena de réplicas: con réplicas en cascada (réplica de una réplica), el atraso se acumula — revisa la topología real, no solo "¿hay una réplica o no?".
- El failover promueve una réplica atrasada: si una réplica se promueve a primario durante un incidente mientras está atrasada, cualquier escritura confirmada en el primario viejo pero aún no replicada se pierde. Por esto existe la replicación síncrona para la réplica designada para HA, separada de la flota dedicada a escalar lecturas.
- Cambios de esquema: un cambio de DDL aplicado al primario tiene que llegar a cada réplica; una migración larga puede en sí misma causar un pico de atraso en toda la flota.