Saltar al contenido principal

Por qué esto es importante


Cada transacción en Midaz genera operaciones de saldo que necesitan ser persistidas. En el modo predeterminado (sincrónico), cada mensaje desencadena una inserción individual en la base de datos. Eso está bien para volúmenes moderados, pero a escala — miles de transacciones por segundo — se convierte en el cuello de botella. El Bulk Recorder cambia esto acumulando mensajes y escribiéndolos en lotes. Menos viajes de ida y vuelta a PostgreSQL, menos contención de bloqueos y un rendimiento significativamente mayor. Para operaciones de alto volumen como pagos masivos, liquidaciones por lotes o procesamiento de pagos en tiempo real, esta es la diferencia entre un sistema que mantiene el ritmo y uno que no. Para estrategias más amplias sobre cómo escalar Midaz, consulta Estrategias de escalabilidad.

Cómo funciona


El Bulk Recorder se sitúa entre el consumidor de RabbitMQ y la capa de base de datos. En lugar de insertar cada mensaje inmediatamente, los recopila en un búfer y los descarga bajo dos condiciones:
  1. Tamaño de lote alcanzado — el búfer se llena hasta el tamaño configurado.
  2. Tiempo de espera agotado — expira el tiempo de espera de descarga configurado, incluso si el búfer no está lleno.
Lo que ocurra primero desencadena la descarga. Esto garantiza tanto el rendimiento (lotes grandes bajo carga) como la latencia (ningún mensaje espera indefinidamente durante períodos de poca actividad).
Diagrama de secuencia que muestra a RabbitMQ entregando mensajes al BulkCollector, el cual los almacena en un búfer hasta alcanzar el tamaño de lote o expirar el tiempo de espera, y luego envía un INSERT en lote fragmentado a PostgreSQL y confirma todos los mensajes de vuelta a RabbitMQ.
Aquí está el flujo completo, paso a paso:
  1. RabbitMQ entrega los mensajes al BulkCollector uno a la vez — Mensaje 1, Mensaje 2, y así hasta el Mensaje N.
  2. El BulkCollector los mantiene en memoria en lugar de escribir cada uno inmediatamente. Sigue recopilando hasta que se alcanza el tamaño del lote o expira el tiempo de espera de descarga.
  3. El BulkCollector envía un INSERT en lote fragmentado a PostgreSQL. Los lotes grandes se dividen automáticamente en fragmentos que respetan los límites de parámetros de PostgreSQL. Cada fragmento utiliza ON CONFLICT (id) DO NOTHING, por lo que los reintentos y las entregas duplicadas se manejan de forma segura.
  4. PostgreSQL confirma la escritura, asegurando que los datos fueron persistidos.
  5. El BulkCollector confirma todos los mensajes de vuelta a RabbitMQ en un único ACK, liberándolos de la cola en conjunto.

Habilitar el Bulk Recorder


El modo en lote requiere que dos condiciones estén activas simultáneamente:
RABBITMQ_TRANSACTION_ASYNC=true
BULK_RECORDER_ENABLED=true
Si alguna de las condiciones no se cumple, Midaz procesa los mensajes individualmente — el mismo comportamiento de antes. Sin cambios de código, sin migración necesaria.
BULK_RECORDER_ENABLED tiene como valor predeterminado true cuando la variable de entorno no está configurada. Por lo tanto, si ya estás ejecutando con RABBITMQ_TRANSACTION_ASYNC=true, es probable que el modo en lote esté activo. Revisa los logs de tu aplicación buscando Bulk mode is ACTIVE al inicio para confirmarlo.

Configuración


VariableDescripciónPredeterminado
BULK_RECORDER_ENABLEDHabilita o deshabilita el modo en lote.true
BULK_RECORDER_SIZENúmero de mensajes a acumular antes de descargar. 0 = calculado automáticamente.0 (automático)
BULK_RECORDER_FLUSH_TIMEOUT_MSTiempo máximo (ms) de espera antes de descargar un lote incompleto.100
BULK_RECORDER_MAX_ROWS_PER_INSERTMáximo de filas por sentencia INSERT enviada a PostgreSQL.1000

Tamaño de lote calculado automáticamente

Cuando BULK_RECORDER_SIZE se establece en 0 (el valor predeterminado), el tamaño del lote se deriva automáticamente:
tamaño de lote = RABBITMQ_NUMBERS_OF_WORKERS × RABBITMQ_NUMBERS_OF_PREFETCH
Esto alinea la capacidad del recopilador con el flujo real de mensajes desde RabbitMQ, evitando descargas parciales o presión de memoria.
Si configuras BULK_RECORDER_SIZE manualmente, asegúrate de que esté alineado con tu configuración de prefetch. Un tamaño mucho mayor que workers × prefetch significa que el recopilador rara vez se llenará y dependerá principalmente de descargas basadas en tiempo de espera.

Ajuste para tu carga de trabajo


Las dos palancas principales son el tamaño del lote y el tiempo de espera de descarga. El equilibrio correcto depende de si priorizas la latencia o el rendimiento.

Baja latencia (procesamiento en tiempo real)

Mantén los lotes pequeños y los tiempos de espera cortos. Los mensajes se persisten rápidamente, incluso si los lotes no están llenos.
RABBITMQ_NUMBERS_OF_WORKERS=5
RABBITMQ_NUMBERS_OF_PREFETCH=10
BULK_RECORDER_SIZE=0          # automático: 5 × 10 = 50
BULK_RECORDER_FLUSH_TIMEOUT_MS=50

Alto rendimiento (operaciones por lotes)

Lotes más grandes y tiempos de espera más largos maximizan la eficiencia de la base de datos. Ideal para pagos masivos, liquidaciones de fin de día o cargas de trabajo de migración.
RABBITMQ_NUMBERS_OF_WORKERS=10
RABBITMQ_NUMBERS_OF_PREFETCH=20
BULK_RECORDER_SIZE=0          # automático: 10 × 20 = 200
BULK_RECORDER_FLUSH_TIMEOUT_MS=300
Comienza con los valores predeterminados y ajusta según el comportamiento observado. Monitorea el log Bulk mode configured for consumer al inicio para confirmar que tu configuración se aplicó.

Garantías de seguridad


El Bulk Recorder está diseñado para ser seguro en todas las condiciones:

Idempotencia

Cada inserción en lote utiliza ON CONFLICT (id) DO NOTHING. Si un mensaje se entrega dos veces — debido a un reintento, una re-entrega o un problema de red — el duplicado se descarta silenciosamente. Sin corrupción de datos, sin violaciones de restricciones.

Prevención de deadlocks

Antes de cada inserción en lote, los IDs de los registros se ordenan. Esto garantiza que todos los escritores concurrentes adquieran los bloqueos en el mismo orden, eliminando la fuente más común de deadlocks de PostgreSQL en escenarios de alta concurrencia.

Fragmentación interna

Los lotes grandes se dividen automáticamente en fragmentos que se ajustan al límite de 65.535 parámetros por consulta de PostgreSQL:
Tipo de registroColumnas por filaFilas por fragmentoParámetros por fragmento
Transaction151.00015.000
Operation301.00030.000
Esta fragmentación se maneja internamente. Solo necesitas configurar BULK_RECORDER_MAX_ROWS_PER_INSERT si deseas ajustar el tamaño del fragmento — el valor predeterminado de 1.000 filas es óptimo para la mayoría de los deployments.

Cuándo usar


Usa el Bulk Recorder cuando:
  • Procesas altos volúmenes de transacciones (cientos o miles por segundo).
  • Tu carga de trabajo incluye operaciones por lotes como pagos masivos, liquidaciones o migraciones de datos.
  • Ya estás utilizando procesamiento asíncrono de transacciones (RABBITMQ_TRANSACTION_ASYNC=true).
  • Deseas reducir la carga de la base de datos y la presión de conexión.
Mantenlo deshabilitado cuando:
  • Tu volumen es lo suficientemente bajo como para que las inserciones individuales no sean un cuello de botella.
  • Necesitas garantías estrictas de ordenamiento por mensaje que el procesamiento por lotes rompería.
  • Estás depurando el procesamiento de transacciones y quieres un flujo más simple, mensaje por mensaje.