Skip to main content
Voltar para Insights
Ledger10 min de leitura

Por que bancos de dados de propósito geral são a fundação errada para OLTP financeiro

A lei de Amdahl e os limites de bancos de dados de propósito geral para cargas de liquidação de alto throughput. O caso para motores especializados.

Por Que Bancos de Dados de Propósito Geral Falham em OLTP Financeiro


PostgreSQL é um banco de dados excelente. Lida com consultas complexas, suporta JSON, tem extensões maduras, escala horizontalmente com Citus, e tem um ecossistema que o torna a escolha padrão para a maioria das startups. É a escolha certa para dados de clientes, configuração de produtos, sessões KYC, trilhas de auditoria, logs de eventos e a maioria dos dados operacionais em um sistema financeiro.

Não é a escolha certa para o ledger de core banking. Especificamente: não é a escolha certa para workloads OLTP financeiros de alto throughput onde múltiplas transações concorrentes competem pelas mesmas contas.

Isso não é uma opinião sobre PostgreSQL. É uma propriedade do workload.

O Problema da Conta Quente

Um ledger financeiro tem um padrão de acesso específico que difere da maioria das cargas de trabalho de aplicações. Considere uma instituição de dinheiro eletrônico com 50.000 contas de clientes. 99% das transferências tocam exatamente duas contas: a conta do remetente e a conta do destinatário. Ambas são diferentes em cada transferência. A contenção é baixa. PostgreSQL lida com isso sem incidentes.

Mas: certas contas são tocadas de forma desproporcional. A conta de cobrança de comissões recebe uma entrada para cada transferência que cobra comissão. A conta de settlement agrega todos os pagamentos enviados. A conta de receita de plataforma recebe cada split de comissão. Essas "contas quentes" são tocadas por uma fração significativa de todas as transferências.

Sob o modelo de bloqueio de linhas do PostgreSQL (especialmente sob isolamento SERIALIZABLE), escritas concorrentes na mesma linha são serializadas. Uma transferência adquire o bloqueio de linha, atualiza o saldo, commita e libera o bloqueio. A próxima transferência espera. Sob carga, as esperas se acumulam.

A Lei de Amdahl quantifica o teto. Se 5% do seu workload é serializado (porque 5% das transferências tocam uma conta quente), a aceleração máxima teórica da paralelização é 20x. Não importa quantos cores, conexões ou réplicas você adicione. O teto é uma propriedade do workload, não do hardware.

Aceleração máxima = 1 / (S + (1-S)/N)

Onde S = fração serializada, N = grau de paralelismo

S = 0.05 (5% das transferências tocam conta quente)
N = ∞ (hardware ilimitado)
Aceleração máxima = 1 / 0.05 = 20x

A 100 TPS base, o teto teórico é 2.000 TPS. O teto prático é menor, porque a detecção de conflitos do PostgreSQL sob isolamento SERIALIZABLE adiciona overhead que escala com a taxa de conflitos.

Os Workarounds e Seus Custos

Equipes de engenharia experientes chegam aos mesmos workarounds. Cada um resolve o problema imediato e introduz um novo.

Advisory locks. Usar pg_advisory_lock(account_id) para serializar o acesso fora do MVCC do PostgreSQL. Reduz a detecção de conflitos do motor, mas agora tem um gerenciador de bloqueios externo. Se a aplicação crashar enquanto mantém um advisory lock, o lock é liberado quando a conexão fecha, mas qualquer estado em voo que dependia do lock é inconsistente.

Sharding. Dividir o espaço de contas entre múltiplos bancos de dados PostgreSQL. Transferências dentro do mesmo shard são transações locais. Transferências cross-shard requerem two-phase commit (2PC) ou consistência eventual. 2PC é lento e operacionalmente complexo. Consistência eventual significa que saldos podem ser temporariamente incorretos, inaceitável para um ledger financeiro que exibe saldos em tempo real.

Filas de escrita. Enfileirar todas as escritas a uma conta quente para um único worker. O worker processa sequencialmente, eliminando contenção. Mas: o worker é um ponto único de falha. Se cai, as escritas são enfileiradas. Se a fila enche, aplica backpressure. O sistema trocou contenção de banco de dados por complexidade de filas.

Contadores pré-materializados. Manter um campo balance na linha da conta, atualizado atomicamente com cada transferência. Isso é o modelo "zero lançamentos", rápido, mas perde a trilha de auditoria. O saldo é um número mutável sem proveniência.

O Que uma Engine Projetada Faz de Diferente

Uma engine de settlement projetada para OLTP financeiro toma três decisões arquitetônicas que um banco de dados de propósito geral não pode:

Records de tamanho fixo. Cada conta: 128 bytes. Cada transferência: 128 bytes. Alinhados com linha de cache. Sem campos de comprimento variável. A CPU pode prever padrões de acesso à memória. A engine de armazenamento localiza qualquer record por aritmética, sem traversal de B-tree.

Processamento em lote. Em vez de um lock-por-transferência, a engine acumula transferências em lotes (até 8.190 por lote) e as processa em um único passo. Sem locks. O lote é a unidade atômica de trabalho. A contenção é irrelevante porque não há modelo de concorrência, apenas processamento sequencial de lotes.

O throughput se inverte: mais carga concorrente significa lotes mais cheios. Lotes mais cheios significa melhor amortização do overhead por lote. O sistema se torna mais rápido sob carga, até os limites físicos de largura de banda de memória.

Invariantes impostas pela engine. A partida dobrada não é imposta por código de aplicação, triggers ou constraints. A própria engine rejeita transferências onde débitos ≠ créditos. Rejeita transferências que excedem o saldo disponível (quando configurado). Rejeita transferências com IDs duplicados. O protocolo valida antes que a transferência chegue ao armazenamento.

PropriedadePostgreSQL (OLTP geral)Engine de Settlement Projetada
Acesso a recordsLookup de índice (B-tree)Aritmética (cálculo de offset)
Modelo de concorrênciaBloqueio de linha, MVCC, detecção de conflitosProcessamento em lote, sem locks
Throughput sob contençãoSe degrada não linearmenteMelhora com lotes mais cheios
Gestão de memóriaAlocação dinâmica, shared_buffers, vacuumAlocação estática, zero GC
Imposição de invariantesCódigo de aplicação / triggers / constraintsNível de protocolo da engine
Tail latency (p99)Imprevisível (vacuum, checkpoints, lock waits)Determinística

Onde PostgreSQL Pertence

A demarcação não é "PostgreSQL ruim, engine projetada boa." É: dados diferentes têm requisitos de acesso diferentes.

PostgreSQL é a escolha certa para:

  • Dados de clientes (nome, endereço, status KYC), baixa contenção, consultas complexas
  • Configuração de produtos (comissões, regras, perfis de jurisdição), esquema flexível, consultas JOIN
  • Sessões KYC (dados de provedor, resultados, scores de similaridade), JSON, comprimento variável
  • Trilha de auditoria (eventos hash-chained, logs de compliance), append-heavy, range queries
  • Dados operacionais (mandatos SEPA, calendário de feriados, dados de referência), relacional estático

Uma engine projetada é a escolha certa para:

  • Saldos de contas e transferências, alta contenção em contas quentes, throughput > correção
  • Processamento de settlement, throughput previsível sob carga, zero tail-latency spikes

A arquitetura usa ambos. PostgreSQL para tudo relacional. A engine de settlement para o ledger financeiro. Os dois se comunicam através do serviço financeiro.

Três Perguntas para Avaliar Seu Armazenamento de Ledger

1. O que acontece quando 100 transferências concorrentes tocam a mesma conta? Se a resposta é "retentativas e degradação de throughput", tem contenção de contas quentes. Se a resposta é "são incluídas no próximo lote e processadas atomicamente", tem uma engine projetada para o workload.

2. Um bug de aplicação pode criar uma transferência desbalanceada? Se a resposta é "não, porque temos testes e revisão de código", a invariante é convencional. Se a resposta é "não, porque a engine de armazenamento rejeita transferências onde débitos ≠ créditos no nível de protocolo", a invariante é estrutural.

3. Qual é sua latência p99 sob carga de pico? Se a resposta inclui "depende da frequência de vacuum" ou "ocasionalmente vemos spikes de checkpoint", sua tail latency é não determinística. Se a resposta é um número fixo que não varia com a carga, tem desempenho previsível.


Leia mais: O Ledger | Settlement Determinístico


Fontes:

  • Amdahl, Gene M. "Validity of the single processor approach to achieving large scale computing capabilities." AFIPS '67, 1967.
  • Documentação do 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)