Copiar sin escribir dos veces
«¿Cuándo pagué el gimnasio?»
Es la pregunta que más se repite en el chat de soporte, así que Cuentasver en el mapa quiere un buscador en la app: escribes «gimnasio» y salen tus pagos al gimnasio, aunque el comercio se llame «GYM FIT SL», estén escritos con faltas o sean de hace tres años.
Eso no lo resuelve una réplica. Una réplica es la misma base de datos, con el mismo modelo, y la tabla de movimientos está pensada para registrar y consultar por cuenta y fecha, no para buscar palabras sueltas entre millones de conceptos. Hace falta otro motor, un índice de búsqueda, con otro modelo de los mismos datos. Y a la vez, Reportingver en el mapa necesita esos mismos movimientos para cargar su warehouse, del que va el próximo episodio.
Así que cada movimiento que se registra en Cuentas tiene que llegar también a otros dos sitios, cada uno con su forma. La idea obvia es escribirlo en los tres. La idea obvia falla.
Dos formas de copiar
Cuentas registra 20 movimientos por segundo. Unos pocos acaban en ROLLBACK: la transacción comprueba el saldo al final y no llega. A la izquierda, dual write: el código de Cuentas escribe en su base de datos y, además, en el buscador, después del COMMIT o antes, según elijas. A la derecha, change data capture (CDC): Cuentas solo escribe en su base de datos, y un conector aparte lee el WAL y manda los cambios al buscador y al warehouse.
Arranca y tira el buscador mientras Ana paga el gimnasio; luego que vuelva y que Ana busque. Prueba el dual write antes del COMMIT, tira el conector y renombra una columna.
Qué ha pasado
Dual write: dos escrituras que no son una
Después del COMMIT, el movimiento ya está en la base de datos de Cuentas cuando se intenta indexar.probar en el laboratorio Si el buscador está caído, la segunda escritura falla y no hay nada que la vuelva a intentar: el pago de Ana existe en Cuentas y no existe en el buscador. Cuando el buscador vuelve, sigue sin existir. Ana busca «gimnasio» y no sale nada. El contador de la izquierda sube con cada movimiento que se registra mientras dura la caída, y no baja nunca.
Poner la escritura en el buscador dentro de la transacción, antes del COMMIT, cambia el fallo de sitio sin quitarlo.probar en el laboratorio Cuando la transacción se revierte después de haber indexado, el ROLLBACK deshace la base de datos, pero el buscador ya tiene el movimiento: un fantasma, un pago que Ana encuentra buscando y que no aparece en su cuenta. Y si el buscador se cae, ahora falla la transacción entera: no se puede pagar porque no se puede indexar. Un buscador, que es un extra, se ha vuelto imprescindible para mover dinero.
El fondo del problema es que son dos sistemas y no hay una transacción que abarque los dos. Reintentar ayuda, pero si el proceso de Cuentas se reinicia entre una escritura y otra, el reintento se pierde con él. Y con dos actualizaciones del mismo movimiento casi a la vez, pueden llegar al buscador en el orden contrario. Es el mismo problema que el episodio 4 de la serie de eventos resolvía con el outbox.
CDC: leer lo que ya está escrito
A la derecha, Cuentas escribe solo en su base de datos, como siempre. Un conector se conecta al primario como si fuera una réplica, pero en vez de copiar la base de datos, lee su WAL y lo traduce a cambios de fila: «insertado en movimientos: id 8812, cuenta de Ana, −39 €, Gimnasio». Manda cada cambio al buscador y al warehouse, en el mismo orden en que se confirmaron. Lo que el primario ha revertido nunca llega al WAL confirmado, así que no hay fantasmas; y si el buscador se cae, el conector simplemente espera y reintenta desde donde se quedó, sin perder nada. Cuando el buscador vuelve, llegan todos los pendientes, y Ana encuentra su pago.
El precio es la edad: el buscador va unos cientos de milisegundos por detrás en un día tranquilo, y todo lo que dure una caída en uno malo. Y el código de Cuentas no ha cambiado ni una línea: añadir el warehouse de Reporting ha sido darle al conector otro destino.
Replicación física y lógica
Las réplicas de los episodios anteriores también leen el WAL, pero de otra forma. La replicación física copia el WAL tal cual: «en el bloque 4.812 del fichero de la tabla, a partir del byte 36, pon estos bytes». Es rapidísima y da una copia idéntica, bit a bit, pero solo la entiende otra instancia del mismo motor, en la misma versión, con las mismas tablas. La replicación lógica descodifica ese WAL en cambios de fila con nombre: qué tabla, qué columnas, qué valores. Eso ya lo puede entender cualquiera: otra versión de PostgreSQL, un esquema distinto, solo algunas tablas, o un motor que no tiene nada que ver, como un buscador. Es la replicación lógica la que hace posible el CDC.
Cuándo hace daño: el slot y el esquema
Para no perderse nada, el conector tiene un slot de replicación en el primario: una marca que dice hasta dónde ha leído.probar en el laboratorio El primario no borra el WAL que el slot aún no ha confirmado, que es justo lo que permite retomar sin pérdidas. Pero si el conector se cae y nadie se da cuenta, el slot se queda quieto y el WAL se acumula en el disco del primario, minuto a minuto, hasta llenarlo. Y un primario sin disco no escribe: un conector que solo alimentaba un buscador ha parado los pagos del banco. Es el mismo mecanismo que la transacción olvidada abierta del episodio 4 de la serie de consistencia, que impedía a VACUUM limpiar: algo que nadie mira retiene lo que nadie puede borrar.
El segundo daño es más sutil.probar en el laboratorio El CDC copia las filas de Cuentas tal y como son, así que cualquiera que lo consuma depende de su esquema interno. El equipo de Cuentas renombra la columna concepto a descripcion, una limpieza que para ellos no cambia nada, y el conector se para con un error: la columna que leía ya no existe. A la izquierda no pasa nada, porque el código del dual write se cambió en el mismo despliegue. El esquema de una base de datos era un detalle de implementación de Cuentas; con CDC se ha convertido en un contrato con otros, lo sepa Cuentas o no.
Criterio
La regla de BCPS Bank sale de ese último daño: CDC dentro de un contexto o hacia el warehouse; eventos entre contextos. El buscador de movimientos es de Cuentas, lo mantiene el mismo equipo y puede cambiar a la vez que el esquema: CDC. El warehouse de Reporting recibe filas porque su trabajo es precisamente guardarlas y transformarlas, con un contrato de datos acordado. Pero Antifraudever en el mapa, que aprende patrones de cada movimiento, sigue escuchando el evento MovimientoRegistrado: un evento de dominio que Cuentas publica a propósito, con los campos que decide publicar, y que no cambia porque alguien renombre una columna.
| Dual write | Outbox | CDC | |
|---|---|---|---|
| Qué se copia | Lo que el código decida | Eventos de dominio, escritos en una tabla en la misma transacción | Filas, tal y como cambian |
| ¿Atómico con la escritura? | No | Sí | Sí: solo lo confirmado llega al WAL |
| ¿Toca el código de Cuentas? | Sí, por cada destino | Sí, una vez por evento | No |
| Contrato con quien lo lee | El que haya en el código | El evento, versionado (serie de eventos, episodio 6) | El esquema interno de las tablas |
| Para | Casi nunca | Otros contextos | Copias propias y el warehouse |
Outbox y CDC no compiten tanto como parece: la forma habitual de sacar los eventos de la tabla outbox es, precisamente, un conector de CDC que lee esa tabla. La diferencia está en qué se publica: filas de las tablas de siempre, o eventos escritos a propósito para otros.
-
¿Quien lo lee es otro contexto, con otro equipo y otro ritmo de cambios?Antifraude aprende de cada movimiento.evento de dominio (outbox)
-
¿Es otra forma de los mismos datos, dentro del contexto o hacia el warehouse?El buscador de Cuentas; los movimientos para Reporting.CDC, vigilando el slot
- Quizá no hace falta copiar: una réplica de lectura (episodio 1), o una consulta al propio Cuentas.
En el mundo real
La herramienta de CDC de código abierto más extendida es Debezium, que funciona como conector de Kafka Connect: lee el WAL de PostgreSQL con descodificación lógica (pgoutput), el binlog de MySQL en formato de filas o el oplog de MongoDB, y publica cada cambio en un tema de Kafka, del que leen el buscador (normalmente Elasticsearch u OpenSearch), el warehouse y quien haga falta. Tiene además un modo específico para el patrón outbox. En la nube, AWS Database Migration Service, Google Datastream y servicios como Fivetran o Airbyte hacen lo mismo como producto gestionado.
El problema del slot es de los más conocidos entre quienes administran PostgreSQL. Desde la versión 13, max_slot_wal_keep_size pone un tope al WAL que un slot puede retener: si se supera, el slot se invalida y el primario sobrevive, a cambio de que el consumidor tenga que empezar de cero con una copia completa. Es la misma elección de siempre: el primario o la copia.
LinkedIn construyó Databus para esto mismo hace más de una década, y Netflix publicó DBLog, que combina el flujo de cambios con copias completas por trozos, para poder rehacer un destino sin parar el origen. Y Martin Kleppmann popularizó la idea de fondo en su charla y artículo Turning the database inside out: el log de cambios de una base de datos es la fuente de la que se pueden derivar todas las demás vistas.
Siguiente episodio
El conector ya manda cada movimiento al warehouse de Reporting, en segundos. Hoy es día 1, y Reporting cierra el mes: importes por tipo de movimiento y mes, del último año, para el regulador. ¿Por qué no lanzar ese informe sobre una réplica de Cuentas, que ya tiene todo, y ahorrarse el warehouse?
Episodio 5 El informe del mes OLTP frente a OLAP: filas y columnas, cargar en batch o en streaming, y el espectro entero con cada lectura del día de cobro en su sitio.