Pular para o conteúdo principal

Por que isso importa


Cada transação no Midaz gera operações de saldo que precisam ser persistidas. No modo padrão (síncrono), cada mensagem dispara uma inserção individual no banco de dados. Isso é aceitável para volumes moderados, mas em escala — milhares de transações por segundo — torna-se o gargalo. O Bulk Recorder muda isso acumulando mensagens e gravando-as em lotes. Menos idas e vindas ao PostgreSQL, menos contenção de lock e throughput significativamente maior. Para operações de alto volume como pagamentos em massa, liquidações em lote ou processamento de pagamentos em tempo real, essa é a diferença entre um sistema que acompanha o ritmo e um que não acompanha. Para estratégias mais amplas sobre como escalar o Midaz, veja Estratégias de escalabilidade.

Como funciona


O Bulk Recorder fica entre o consumidor do RabbitMQ e a camada de banco de dados. Em vez de inserir cada mensagem imediatamente, ele as coleta em um buffer e descarrega sob duas condições:
  1. Tamanho do lote atingido — o buffer enche até o tamanho configurado.
  2. Timeout expirado — o timeout de flush configurado expira, mesmo se o buffer não estiver cheio.
O que acontecer primeiro dispara o flush. Isso garante tanto throughput (lotes grandes sob carga) quanto latência (nenhuma mensagem espera para sempre durante períodos de baixa atividade).
Diagrama de sequência mostrando o RabbitMQ entregando mensagens ao BulkCollector, que as mantém em buffer até o tamanho do lote ou o timeout serem atingidos, e então envia um INSERT em lote fragmentado em chunks ao PostgreSQL e confirma todas as mensagens de volta ao RabbitMQ.
Aqui está o fluxo completo, passo a passo:
  1. O RabbitMQ entrega as mensagens ao BulkCollector uma de cada vez — Mensagem 1, Mensagem 2, e assim por diante até a Mensagem N.
  2. O BulkCollector as mantém em memória em vez de gravar cada uma imediatamente. Ele continua coletando até que o tamanho do lote seja atingido ou o timeout de flush expire.
  3. O BulkCollector envia um INSERT em lote em chunks ao PostgreSQL. Lotes grandes são automaticamente divididos em chunks que respeitam os limites de parâmetros do PostgreSQL. Cada chunk usa ON CONFLICT (id) DO NOTHING, então retries e entregas duplicadas são tratados com segurança.
  4. O PostgreSQL confirma a gravação, garantindo que os dados foram persistidos.
  5. O BulkCollector confirma todas as mensagens de volta ao RabbitMQ em um único ACK, liberando-as da fila em conjunto.

Habilitando o Bulk Recorder


O modo bulk requer que duas condições estejam ativas simultaneamente:
RABBITMQ_TRANSACTION_ASYNC=true
BULK_RECORDER_ENABLED=true
Se qualquer uma das condições não for atendida, o Midaz processa mensagens individualmente — o mesmo comportamento de antes. Sem mudanças de código, sem migração necessária.
BULK_RECORDER_ENABLED tem valor padrão true quando a variável de ambiente não está definida. Então, se você já está rodando com RABBITMQ_TRANSACTION_ASYNC=true, o modo bulk provavelmente está ativo. Verifique os logs da sua aplicação por Bulk mode is ACTIVE na inicialização para confirmar.

Configuração


VariávelDescriçãoPadrão
BULK_RECORDER_ENABLEDHabilita ou desabilita o modo bulk.true
BULK_RECORDER_SIZENúmero de mensagens a acumular antes do flush. 0 = calculado automaticamente.0 (auto)
BULK_RECORDER_FLUSH_TIMEOUT_MSTempo máximo (ms) de espera antes de descarregar um lote incompleto.100
BULK_RECORDER_MAX_ROWS_PER_INSERTMáximo de linhas por instrução INSERT enviada ao PostgreSQL.1000

Tamanho de lote calculado automaticamente

Quando BULK_RECORDER_SIZE é definido como 0 (o padrão), o tamanho do lote é derivado automaticamente:
tamanho do lote = RABBITMQ_NUMBERS_OF_WORKERS × RABBITMQ_NUMBERS_OF_PREFETCH
Isso alinha a capacidade do coletor com o fluxo real de mensagens do RabbitMQ, evitando flushes parciais ou pressão de memória.
Se você definir BULK_RECORDER_SIZE manualmente, certifique-se de que esteja alinhado com suas configurações de prefetch. Um tamanho muito maior que workers × prefetch significa que o coletor raramente vai encher e dependerá principalmente de flushes baseados em timeout.

Ajustando para sua carga de trabalho


As duas principais alavancas são o tamanho do lote e o timeout de flush. O equilíbrio certo depende de priorizar latência ou throughput.

Baixa latência (processamento em tempo real)

Mantenha lotes pequenos e timeouts curtos. Mensagens são persistidas rapidamente, mesmo que os lotes não estejam cheios.
RABBITMQ_NUMBERS_OF_WORKERS=5
RABBITMQ_NUMBERS_OF_PREFETCH=10
BULK_RECORDER_SIZE=0          # auto: 5 × 10 = 50
BULK_RECORDER_FLUSH_TIMEOUT_MS=50

Alto throughput (operações em lote)

Lotes maiores e timeouts mais longos maximizam a eficiência do banco de dados. Ideal para pagamentos em massa, liquidações de fim de dia ou cargas de trabalho de migração.
RABBITMQ_NUMBERS_OF_WORKERS=10
RABBITMQ_NUMBERS_OF_PREFETCH=20
BULK_RECORDER_SIZE=0          # auto: 10 × 20 = 200
BULK_RECORDER_FLUSH_TIMEOUT_MS=300
Comece com os valores padrão e ajuste com base no comportamento observado. Monitore o log Bulk mode configured for consumer na inicialização para confirmar que suas configurações foram aplicadas.

Garantias de segurança


O Bulk Recorder foi projetado para ser seguro em todas as condições:

Idempotência

Cada inserção em lote usa ON CONFLICT (id) DO NOTHING. Se uma mensagem for entregue duas vezes — devido a um retry, redelivery ou falha de rede — a duplicata é descartada silenciosamente. Sem corrupção de dados, sem violações de constraint.

Prevenção de deadlock

Antes de cada inserção em lote, os IDs dos registros são ordenados. Isso garante que todos os escritores concorrentes adquiram locks na mesma ordem, eliminando a fonte mais comum de deadlocks do PostgreSQL em cenários de alta concorrência.

Chunking interno

Lotes grandes são automaticamente divididos em chunks que cabem dentro do limite de 65.535 parâmetros por query do PostgreSQL:
Tipo de registroColunas por linhaLinhas por chunkParâmetros por chunk
Transaction151.00015.000
Operation301.00030.000
Esse chunking é tratado internamente. Você só precisa configurar BULK_RECORDER_MAX_ROWS_PER_INSERT se quiser ajustar o tamanho do chunk — o padrão de 1.000 linhas é ideal para a maioria dos deployments.

Quando usar


Use o Bulk Recorder quando:
  • Você processa altos volumes de transações (centenas ou milhares por segundo).
  • Sua carga de trabalho inclui operações em lote como pagamentos em massa, liquidações ou migrações de dados.
  • Você já está usando processamento assíncrono de transações (RABBITMQ_TRANSACTION_ASYNC=true).
  • Você quer reduzir a carga do banco de dados e a pressão de conexões.
Mantenha desabilitado quando:
  • Seu volume é baixo o suficiente para que inserções individuais não sejam um gargalo.
  • Você precisa de garantias estritas de ordenação por mensagem que o processamento em lote quebraria.
  • Você está depurando o processamento de transações e quer um fluxo mais simples, mensagem por mensagem.