Dominando la Configuración de Prefetch en RabbitMQ para un Rendimiento Óptimo del Consumidor

Ajusta el prefetch de RabbitMQ para que los consumidores estén ocupados sin acaparar mensajes ni ocultar un procesamiento lento.

Dominando la Configuración de Prefetch en RabbitMQ para un Rendimiento Óptimo del Consumidor

El prefetch de RabbitMQ es una de esas configuraciones que parece pequeña pero lo cambia todo. Controla cuántos mensajes no confirmados RabbitMQ permite que un consumidor tenga a la vez. Si se establece demasiado bajo, los consumidores rápidos pasan demasiado tiempo esperando la siguiente entrega. Si se establece demasiado alto, los consumidores lentos acumulan trabajo silenciosamente, aumentan la latencia y hacen que los gráficos de profundidad de cola mientan.

La forma útil de pensar en el prefetch es como trabajo no terminado. Un prefetch de 20 significa que un consumidor puede tener 20 mensajes entregados pero aún no confirmados. Esos mensajes ya no están listos en la cola. Están no confirmados, en manos del consumidor hasta que los confirme, rechace, o se desconecte.

Eso significa que el prefetch no es solo un ajuste de rendimiento. Es un ajuste de equidad, un ajuste de memoria y un ajuste de recuperación ante fallos.

Qué hace basic.qos en RabbitMQ

Los consumidores establecen el prefetch con basic.qos. En la mayoría de las bibliotecas cliente se establece prefetch_count; prefetch_size rara vez se usa y generalmente se deja en cero.

En Python con Pika:

channel.basic_qos(prefetch_count=10)
channel.basic_consume(
    queue="jobs",
    on_message_callback=handle_message,
    auto_ack=False,
)

En Node.js con amqplib:

await channel.prefetch(10);
await channel.consume("jobs", async (msg) => {
  try {
    await handleMessage(msg.content);
    channel.ack(msg);
  } catch (err) {
    channel.nack(msg, false, false);
  }
}, { noAck: false });

La confirmación manual importa. Si usas confirmaciones automáticas, RabbitMQ considera el mensaje completo tan pronto como se entrega. El prefetch ya no protege la fiabilidad del procesamiento de la misma manera, porque no hay una ventana de no confirmados que gestionar.

RabbitMQ aplica el prefetch por consumidor de forma predeterminada en el uso moderno, aunque la redacción original de AMQP está orientada al canal. Algunos clientes exponen un indicador global. Ten cuidado con él. Un límite compartido a nivel de canal o conexión puede crear interacciones confusas entre consumidores. La mayoría de los servicios son más fáciles de razonar cuando cada consumidor tiene su propio canal y su propio recuento de prefetch.

Por qué el prefetch cambia la latencia

Imagina una cola con dos consumidores. El consumidor A recibe un lote de 100 mensajes y luego se encuentra con una API externa lenta. El consumidor B está sano y es rápido, pero esos 100 mensajes ya están asignados a A. RabbitMQ no se los dará a B a menos que A los rechace o su canal se cierre.

Desde el punto de vista de la cola, esos mensajes no están listos. Desde el punto de vista del usuario, están retrasados. Esta es la razón por la que un prefetch alto puede hacer que un sistema se vea mejor en los gráficos del broker mientras empeora la latencia real.

Un prefetch bajo le da a RabbitMQ más oportunidades para distribuir el trabajo de manera justa. Un prefetch alto les da a los consumidores más trabajo local y menos viajes de ida y vuelta al broker. Ninguno es siempre correcto.

Valores iniciales que tienen sentido

Para trabajos lentos, comienza pequeño. Si cada mensaje llama a una API de terceros, escribe varias filas en la base de datos o realiza transformaciones pesadas de CPU, prueba con prefetch_count=1 a 10. Quieres que un consumidor fallido o lento tenga solo una pequeña cantidad de trabajo.

Para trabajos medianos que toman decenas o cientos de milisegundos y se ejecutan en trabajadores estables, valores como 10, 20 o 50 son puntos de partida comunes. Mide antes de ir más alto.

Para manejadores muy rápidos donde el broker y el consumidor están en una red de baja latencia, un prefetch más alto puede reducir los viajes de ida y vuelta y mejorar el rendimiento. Incluso entonces, evita elegir un número enorme solo porque hizo que un benchmark se viera bien durante cinco minutos. Observa la memoria del consumidor y la latencia de cola.

Una regla general simple es dimensionar el prefetch alrededor de la cantidad de trabajo que un consumidor puede manejar cómodamente en una ventana corta. Si un trabajador procesa alrededor de 20 mensajes por segundo y te sientes cómodo con aproximadamente un segundo de trabajo almacenado localmente, un prefetch cercano a 20 es un experimento razonable.

Cómo saber si el prefetch es demasiado alto

El prefetch probablemente sea demasiado alto cuando:

  • messages_unacknowledged es grande en comparación con los consumidores activos.
  • Algunos consumidores tienen muchos mensajes no confirmados mientras que otros están inactivos.
  • La latencia de los mensajes es alta incluso cuando messages_ready es baja.
  • La memoria del consumidor aumenta durante los picos.
  • Un fallo del consumidor causa una gran ola de reentregas.

Ese último punto es fácil de pasar por alto. Si un trabajador tiene 1,000 mensajes no confirmados y falla, RabbitMQ puede reentregar esos mensajes. Eso es un comportamiento correcto, pero puede crear presión duplicada en los sistemas posteriores si el manejador no es idempotente.

Reducir el prefetch a menudo mejora la equidad y el comportamiento de recuperación. Puede reducir un poco el rendimiento máximo, pero puede mejorar la latencia que los usuarios realmente sienten.

Cómo saber si el prefetch es demasiado bajo

El prefetch probablemente sea demasiado bajo cuando:

  • Los consumidores tienen un uso bajo de CPU y memoria mientras messages_ready sigue creciendo.
  • El tiempo de procesamiento es muy corto, pero la tasa de entrega está limitada.
  • La latencia de red entre los consumidores y RabbitMQ es notable.
  • Aumentar el prefetch mejora el rendimiento sin aumentar la latencia de cola o la presión de memoria.

El ejemplo clásico es un trabajador rápido que realiza un pequeño cálculo en memoria y confirma inmediatamente. Con prefetch_count=1, puede pasar demasiado tiempo esperando el siguiente mensaje. Aumentar el prefetch le da un pequeño búfer local y lo mantiene ocupado.

No ocultes cuellos de botella posteriores

El ajuste del prefetch no solucionará una base de datos lenta. Solo puede cambiar cómo se distribuye y almacena el trabajo. Si cada mensaje espera en la misma API sobrecargada, un prefetch más alto puede hacer que el rendimiento se vea mejor brevemente mientras aumentan los tiempos de espera y los reintentos.

Mide dentro del consumidor. Registra o emite métricas para el tiempo dedicado a decodificar el mensaje, esperar en la base de datos, llamar a servicios externos y confirmar. RabbitMQ puede mostrarte los recuentos de listos y no confirmados, pero no puede decirte por qué tu manejador tarda ocho segundos.

Cuando un servicio posterior tiene limitación de velocidad, el prefetch a menudo debería ser más bajo, no más alto. Deja que la cola absorba el backlog de manera visible en lugar de ocultar miles de llamadas en curso dentro de los trabajadores.

Prefetch y concurrencia son diferentes

Un prefetch de 50 no significa automáticamente que tu consumidor procese 50 mensajes en paralelo. Solo significa que RabbitMQ puede entregar 50 mensajes antes de recibir confirmaciones. Si se ejecutan concurrentemente depende de tu código de consumidor.

Un consumidor de un solo hilo con prefetch 50 puede procesar un mensaje a la vez mientras 49 esperan en memoria. Un grupo de trabajadores con concurrencia 10 y prefetch 50 puede mantener diez tareas activas y cuarenta almacenadas. A veces ese búfer es útil. A veces es solo latencia.

Empareja el prefetch con la concurrencia real. Si tu proceso puede ejecutar cinco manejadores a la vez, un prefetch de 5 a 20 es más fácil de razonar que 500.

Compensaciones de orden y equidad

Las colas de RabbitMQ preservan el orden a nivel de cola, pero el comportamiento del consumidor puede cambiar el orden en que se completa el trabajo. Con múltiples consumidores y prefetch mayor que 1, el mensaje 20 puede terminar antes que el mensaje 3 porque fue a un trabajador más rápido o tuvo un trabajo más fácil.

Para la mayoría de las colas de trabajo, el orden de finalización no importa. Para actualizaciones de cuenta, cambios de inventario o flujos de trabajo que deben procesarse en secuencia, podría importar mucho. En esos casos, usar una cola por clave de orden, fragmentar por clave o mantener el prefetch bajo puede ser más seguro que perseguir el rendimiento máximo.

La equidad tiene una compensación similar. Un prefetch bajo permite que RabbitMQ distribuya el trabajo de manera más uniforme porque los consumidores regresan por mensajes con más frecuencia. Un prefetch alto recompensa a los consumidores que reciben mensajes primero. Si los mensajes tienen tiempos de procesamiento desiguales, eso puede llevar a que un trabajador tenga un montón de trabajos lentos mientras otro termina su lote rápidamente.

Cuando la gente dice "el equilibrio de carga de RabbitMQ es desigual", el prefetch es una de las primeras cosas a verificar. El broker solo puede equilibrar los mensajes que aún no se han entregado.

El comportamiento ante fallos importa

El prefetch cambia lo que sucede cuando un consumidor muere. Con prefetch_count=1, una entrega no confirmada regresa cuando el canal se cierra. Con prefetch_count=500, cientos pueden regresar a la vez. Si el consumidor realizó efectos secundarios parciales antes de fallar, esas reentregas pueden desencadenar escrituras duplicadas, correos electrónicos duplicados o llamadas API duplicadas a menos que el manejador sea idempotente.

Eso no significa que un prefetch alto sea incorrecto. Significa que un prefetch alto pertenece a manejadores idempotentes, reglas claras de reintento y monitoreo de tasas de reentrega. Si el procesamiento duplicado sería peligroso, mantén la ventana de no confirmados pequeña hasta que la aplicación esté construida para manejarlo.

Mira el indicador redelivered en el consumidor. No es un contador de reintentos completo, pero es una señal útil de que el mensaje ha sido entregado antes. Para límites de reintento robustos, rastrea los intentos en encabezados o en el estado de la aplicación y enruta los mensajes agotados a una cola de mensajes muertos.

Múltiples colas y cargas de trabajo mixtas

Un valor de prefetch rara vez se ajusta a todas las colas. Un servicio que consume thumbnail.generate y email.send puede necesitar configuraciones diferentes para cada uno. La generación de miniaturas puede ser pesada en CPU y mejor con baja concurrencia. El envío de correos electrónicos puede estar limitado por la red y tolerar más mensajes en vuelo.

Si un solo proceso consume varias colas en un canal, el comportamiento de QoS puede volverse más difícil de razonar. Prefiere canales separados para cargas de trabajo significativamente diferentes. Eso hace que el prefetch, el monitoreo y el manejo de fallos sean más obvios.

Los tamaños de mensaje mixtos son otra señal de advertencia. Si una cola contiene tanto eventos pequeños como cargas útiles enormes, un prefetch basado en recuento no refleja bien la presión de memoria. Diez mensajes pequeños y diez mensajes grandes no tienen el mismo costo. En esa situación, divide la carga de trabajo o mueve las cargas útiles grandes fuera de RabbitMQ y pasa referencias en su lugar.

Observa los no confirmados por consumidor, no solo por cola

Un recuento de no confirmados a nivel de cola te dice que hay trabajo no terminado, pero puede ocultar el sesgo. Un consumidor puede tener la mayoría de los mensajes no confirmados mientras que el resto están casi vacíos. Eso a menudo apunta a un prefetch alto, un costo de mensaje desigual o un trabajador no saludable.

Usa métricas a nivel de consumidor desde la interfaz de administración, Prometheus o rabbitmqctl list_consumers durante una prueba. Si la distribución es desigual, reducir el prefetch o dividir los tipos de mensajes lentos puede mejorar la latencia real incluso cuando el rendimiento total cambia solo un poco.

Revisa el prefetch después de los despliegues

Los valores de prefetch envejecen. Un valor que funcionaba cuando un manejador solo escribía una fila en la base de datos puede ser incorrecto después de que la próxima versión agregue una llamada API, validación adicional o una carga útil más grande. Trata el prefetch como parte de la configuración de rendimiento, no como un número que estableces una vez y olvidas.

Después de un lanzamiento de consumidor, compara la latencia de procesamiento, los recuentos de no confirmados, las reentregas y la memoria del consumidor con la versión anterior. Si la latencia aumenta pero la CPU no está saturada, el manejador puede estar esperando algo externo y un prefetch más bajo puede mantener el sistema más justo. Si la CPU es alta y cada mensaje está limitado por CPU, agregar trabajadores o reducir el trabajo por mensaje puede importar más que cambiar el prefetch.

Documenta la razón del valor elegido cerca de la configuración del consumidor. Los futuros mantenedores deben saber si prefetch_count=5 fue elegido por equidad, memoria, orden, límites de velocidad posteriores o solo como un valor predeterminado temporal.

Prueba con formas de mensaje reales

No ajustes el prefetch con mensajes falsos pequeños si los mensajes de producción son cargas útiles JSON grandes o incluyen búsquedas costosas en la base de datos. El tamaño del mensaje y el costo del manejador importan.

Un bucle de prueba útil es:

  1. Elige un valor de prefetch.
  2. Ejecuta una tasa de publicación realista durante el tiempo suficiente para ver un comportamiento estable.
  3. Observa messages_ready, messages_unacknowledged, CPU del consumidor, memoria del consumidor, latencia de procesamiento y tasa de error.
  4. Mata un consumidor y ve cuántos mensajes se reentregan.
  5. Aumenta o disminuye el prefetch y repite.

El mejor valor rara vez es el que tiene el mayor rendimiento de benchmark a corto plazo. Es el valor que mantiene a los consumidores ocupados, mantiene la latencia aceptable y falla de una manera que tu sistema pueda manejar.

Un valor predeterminado práctico

Si aún no tienes datos, comienza con confirmaciones manuales y prefetch_count=10 para colas de trabajo ordinarias. Usa 1 para procesamiento lento, costoso o estrictamente justo. Prueba 20 o 50 para manejadores rápidos y estables después de medir. Ve más alto solo cuando las métricas muestren que los viajes de ida y vuelta de entrega son el cuello de botella y los consumidores tienen margen de memoria.

El ajuste del prefetch de RabbitMQ no es una configuración única. Revísalo cuando cambie el tamaño del mensaje, el código del consumidor, las dependencias posteriores o cuando agregues más instancias de trabajadores. El valor de prefetch correcto es el que coincide con la forma actual del trabajo.