Saltar al contenido
Volver a Insights
Ledger10 min de lectura

Por qué las bases de datos de propósito general son el fundamento equivocado para OLTP financiero

La ley de Amdahl y los límites de las bases de datos de propósito general para cargas de liquidación de alto rendimiento. El caso para motores especializados.

Por Qué las Bases de Datos de Propósito General Fallan en OLTP Financiero


PostgreSQL es una base de datos excelente. Maneja consultas complejas, soporta JSON, tiene extensiones maduras, escala horizontalmente con Citus, y tiene un ecosistema que la convierte en la opción predeterminada para la mayoría de las startups. Es la elección correcta para datos de clientes, configuración de productos, sesiones KYC, trilhas de auditoría, logs de eventos y la mayoría de los datos operativos en un sistema financiero.

No es la elección correcta para el ledger de core banking. Específicamente: no es la elección correcta para workloads OLTP financieros de alto throughput donde múltiples transacciones concurrentes compiten por las mismas cuentas.

Esto no es una opinión sobre PostgreSQL. Es una propiedad del workload.

El Problema de la Cuenta Caliente

Un ledger financiero tiene un patrón de acceso específico que difiere de la mayoría de las cargas de trabajo de aplicaciones. Considere una institución de dinero electrónico con 50.000 cuentas de clientes. El 99% de las transferencias tocan exactamente dos cuentas: la cuenta del emisor y la cuenta del receptor. Ambas son diferentes en cada transferencia. La contención es baja. PostgreSQL maneja esto sin incidentes.

Pero: ciertas cuentas se tocan de forma desproporcionada. La cuenta de cobro de comisiones recibe una partida por cada transferencia que cobra comisión. La cuenta de settlement agrega todos los pagos salientes. La cuenta de ingresos de plataforma recibe cada split de comisión. Estas "cuentas calientes" son tocadas por una fracción significativa de todas las transferencias.

Bajo el modelo de bloqueo de filas de PostgreSQL (especialmente bajo aislamiento SERIALIZABLE), las escrituras concurrentes a la misma fila se serializan. Una transferencia adquiere el bloqueo de fila, actualiza el saldo, commitea, y libera el bloqueo. La siguiente transferencia espera. Bajo carga, las esperas se acumulan.

La ley de Amdahl cuantifica el techo. Si el 5% de su workload es serializado (porque el 5% de las transferencias tocan una cuenta caliente), la aceleración máxima teórica de la paralelización es 20x. No importa cuántos cores, conexiones o réplicas añada. El techo es una propiedad del workload, no del hardware.

Aceleración máxima = 1 / (S + (1-S)/N)

Donde S = fracción serializada, N = grado de paralelismo

S = 0.05 (5% de transferencias tocan cuenta caliente)
N = ∞ (hardware ilimitado)
Aceleración máxima = 1 / 0.05 = 20x

A 100 TPS base, el techo teórico es 2.000 TPS. El techo práctico es menor, porque la detección de conflictos de PostgreSQL bajo aislamiento SERIALIZABLE añade overhead que escala con la tasa de conflictos. La degradación real es no lineal, el throughput cae más rápido de lo que Amdahl predice.

Los Workarounds y Sus Costos

Los equipos de ingeniería con experiencia llegan a los mismos workarounds. Cada uno resuelve el problema inmediato e introduce uno nuevo.

Advisory locks. Usar pg_advisory_lock(account_id) para serializar el acceso fuera de MVCC de PostgreSQL. Reduce la detección de conflictos del motor, pero ahora tiene un gestor de bloqueos externo. Si la aplicación se cae mientras mantiene un advisory lock, el lock se libera cuando se cierra la conexión, pero cualquier in-flight state que dependía del lock es inconsistente. Necesita manejo de deadlocks, timeouts de locks y monitoreo de locks. El lock pool se convierte en su propio cuello de botella bajo alta concurrencia.

Sharding. Dividir el espacio de cuentas entre múltiples bases de datos PostgreSQL. Transferencias dentro del mismo shard son transacciones locales. Transferencias cross-shard requieren two-phase commit (2PC) o consistencia eventual. 2PC es lento y operativamente complejo. La consistencia eventual significa que los saldos pueden ser temporalmente incorrectos, inaceptable para un ledger financiero que muestra saldos en tiempo real.

Colas de escritura. Encolar todas las escrituras a una cuenta caliente a un solo worker. El worker procesa secuencialmente, eliminando contención. Pero: el worker es un punto único de fallo. Si se cae, las escrituras se encolan. Si la cola se llena, aplica backpressure. El sistema ha cambiado contención de base de datos por complejidad de colas. La latencia ahora incluye el tiempo en cola más el tiempo de procesamiento, y la distribución de latencia se vuelve bimodal (rápida para cuentas frías, impredecible para cuentas calientes).

Counters pre-materializados. Mantener un campo balance en la fila de la cuenta, actualizado atómicamente con cada transferencia. Esto es el modelo "zero-entry", rápido, pero pierde la trilha de auditoría. El saldo es un número mutable sin proveniencia. Cuando el saldo está mal (y eventualmente lo estará, por un bug, una race condition, un crash parcial), no hay registro de cómo llegó a estar mal.

Lo Que una Engine Diseñada Hace Diferente

Una engine de settlement diseñada para OLTP financiero toma tres decisiones arquitectónicas que una base de datos de propósito general no puede:

Records de tamaño fijo. Cada cuenta: 128 bytes. Cada transferencia: 128 bytes. Alineados con línea de caché. Sin campos de longitud variable. La CPU puede predecir patrones de acceso a memoria. La engine de almacenamiento localiza cualquier record por aritmética, sin traversal de B-tree.

Procesamiento por lotes. En vez de un lock-per-transferencia, la engine acumula transferencias en lotes (hasta 8.190 por lote) y las procesa en un solo paso. Sin locks. El lote es la unidad atómica de trabajo. La contención es irrelevante porque no hay modelo de concurrencia, solo procesamiento secuencial de lotes.

El throughput se invierte: más carga concurrente significa lotes más llenos. Lotes más llenos significa mejor amortización del overhead por lote. El sistema se vuelve más rápido bajo carga, hasta los límites físicos de ancho de banda de memoria.

Invariantes impuestas por la engine. La partida doble no se impone por código de aplicación, triggers ni constraints. La engine misma rechaza transferencias donde débitos ≠ créditos. Rechaza transferencias que exceden el saldo disponible (cuando se configura). Rechaza transferencias con IDs duplicados. El protocolo valida antes de que la transferencia llegue al almacenamiento. Un bug en la aplicación que envía una transferencia malformada es rechazado, no registrado.

PropiedadPostgreSQL (OLTP general)Engine de Settlement Diseñada
Acceso a recordsLookup de índice (B-tree)Aritmética (cálculo de offset)
Modelo de concurrenciaBloqueo de fila, MVCC, detección de conflictosProcesamiento por lotes, sin locks
Throughput bajo contenciónSe degrada no linealmenteMejora con lotes más llenos
Gestión de memoriaAlocación dinámica, shared_buffers, vacuumAlocación estática, zero GC
Imposición de invariantesCódigo de aplicación / triggers / constraintsNivel de protocolo de la engine
Tail latency (p99)Impredecible (vacuum, checkpoints, lock waits)Determinista

Dónde PostgreSQL Sí Pertenece

La demarcación no es "PostgreSQL malo, engine diseñada buena." Es: diferentes datos tienen diferentes requisitos de acceso.

PostgreSQL es la elección correcta para:

  • Datos de clientes (nombre, dirección, estado KYC), baja contención, consultas complejas
  • Configuración de productos (comisiones, reglas, perfiles de jurisdicción), esquema flexible, consultas JOIN
  • Sesiones KYC (datos de proveedor, resultados, scores de similitud), JSON, longitud variable
  • Trilha de auditoría (eventos hash-chained, logs de compliance), append-heavy, range queries
  • Datos operativos (mandatos SEPA, calendario de festivos, datos de referencia), estático relacional

Una engine diseñada es la elección correcta para:

  • Saldos de cuentas y transferencias, alta contención en cuentas calientes, throughput > corrección
  • Procesamiento de settlement, throughput predecible bajo carga, zero tail-latency spikes

La arquitectura usa ambas. PostgreSQL para todo lo relacional. La engine de settlement para el ledger financiero. Las dos se comunican a través del servicio financiero, que coordina lecturas/escrituras a ambos stores.

Tres Preguntas para Evaluar Su Almacenamiento de Ledger

1. ¿Qué pasa cuando 100 transferencias concurrentes tocan la misma cuenta?

Si la respuesta es "reintentos y degradación de throughput", tiene contención de cuentas calientes. El workaround existe en código de aplicación, no en la engine de almacenamiento. Si la respuesta es "se incluyen en el siguiente lote y se procesan atómicamente", tiene una engine diseñada para el workload.

2. ¿Puede un bug de aplicación crear una transferencia desbalanceada?

Si la respuesta es "no, porque tenemos tests y revisión de código", la invariante es convencional. Si la respuesta es "no, porque la engine de almacenamiento rechaza transferencias donde débitos ≠ créditos a nivel de protocolo", la invariante es estructural. Solamente la segunda sobrevive al crecimiento del equipo y la rotación de código.

3. ¿Cuál es su latencia p99 bajo carga pico?

Si la respuesta incluye "depende de la frecuencia de vacuum" o "ocasionalmente vemos spikes de checkpoint", su tail latency es no determinista. Si la respuesta es un número fijo que no varía con la carga, tiene rendimiento predecible.


Leer más: El Ledger | Settlement Determinista


Fuentes:

  • Amdahl, Gene M. "Validity of the single processor approach to achieving large scale computing capabilities." AFIPS '67, 1967.
  • Documentación de PostgreSQL: Row Locking, Serializable Isolation (https://www.postgresql.org/docs/current/transaction-iso.html)
  • Jim Gray, "A Measure of Transaction Processing Power" (1985)
  • "Gray Failure: The Achilles' Heel of Cloud-Scale Systems" (Microsoft Research, 2017)