Dominando as Configurações de Prefetch do RabbitMQ para Desempenho Ótimo do Consumidor

Ajuste o prefetch do RabbitMQ para que os consumidores fiquem ocupados sem acumular mensagens ou esconder processamento lento.

Dominando as Configurações de Prefetch do RabbitMQ para Desempenho Ótimo do Consumidor

O prefetch do RabbitMQ é uma daquelas configurações que parecem pequenas e mudam tudo. Ele controla quantas mensagens não confirmadas o RabbitMQ permitirá que um consumidor mantenha de uma só vez. Defina-o muito baixo e consumidores rápidos gastam muito tempo esperando pela próxima entrega. Defina-o muito alto e consumidores lentos acumulam trabalho silenciosamente, aumentam a latência e fazem com que os gráficos de profundidade da fila mintam.

A maneira útil de pensar sobre prefetch é trabalho não concluído. Um prefetch de 20 significa que um consumidor pode ter 20 mensagens entregues, mas ainda não confirmadas. Essas mensagens não estão mais prontas na fila. Elas estão não confirmadas, aguardando com o consumidor até que ele confirme, rejeite, ou desconecte.

Isso significa que prefetch não é apenas um botão de throughput. É um botão de justiça, um botão de memória e um botão de recuperação de falhas.

O que basic.qos faz no RabbitMQ

Os consumidores definem prefetch com basic.qos. Na maioria das bibliotecas de cliente, você define prefetch_count; prefetch_size raramente é usado e geralmente é deixado em zero.

Em Python com Pika:

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

Em Node.js com 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 });

A confirmação manual é importante. Se você usar confirmações automáticas, o RabbitMQ considera a mensagem completa assim que é entregue. O prefetch não protege mais a confiabilidade do processamento da mesma forma, porque não há uma janela não confirmada para gerenciar.

O RabbitMQ aplica prefetch por consumidor por padrão no uso moderno, embora a redação original do AMQP seja orientada a canais. Alguns clientes expõem um sinalizador global. Tenha cuidado com ele. Um limite compartilhado de canal ou conexão pode criar interações confusas entre consumidores. A maioria dos serviços é mais fácil de raciocinar quando cada consumidor tem seu próprio canal e sua própria contagem de prefetch.

Por que o prefetch muda a latência

Imagine uma fila com dois consumidores. O Consumidor A recebe um lote de 100 mensagens e então atinge uma API externa lenta. O Consumidor B é saudável e rápido, mas essas 100 mensagens já estão atribuídas a A. O RabbitMQ não as dará a B a menos que A as rejeite ou seu canal feche.

Do ponto de vista da fila, essas mensagens não estão prontas. Do ponto de vista do usuário, elas estão atrasadas. É por isso que um prefetch alto pode fazer um sistema parecer melhor nos gráficos do broker enquanto piora a latência real.

Prefetch baixo dá ao RabbitMQ mais chances de distribuir o trabalho de forma justa. Prefetch alto dá aos consumidores mais trabalho local e menos viagens de ida e volta ao broker. Nenhum dos dois está sempre correto.

Valores iniciais que fazem sentido

Para trabalhos lentos, comece pequeno. Se cada mensagem chama uma API de terceiros, escreve várias linhas no banco de dados ou faz transformação pesada de CPU, tente prefetch_count=1 a 10. Você quer que um consumidor com falha ou lento mantenha apenas uma pequena quantidade de trabalho.

Para trabalhos médios que levam dezenas ou centenas de milissegundos e são executados em workers estáveis, valores como 10, 20 ou 50 são pontos de partida comuns. Meça antes de ir mais alto.

Para handlers muito rápidos onde o broker e o consumidor estão em uma rede de baixa latência, um prefetch mais alto pode reduzir as viagens de ida e volta e melhorar o throughput. Mesmo assim, evite escolher um número enorme só porque fez um benchmark parecer bom por cinco minutos. Observe a memória do consumidor e a latência final.

Uma regra prática simples é dimensionar o prefetch em torno da quantidade de trabalho que um consumidor pode manter confortavelmente por uma janela curta. Se um worker processa cerca de 20 mensagens por segundo e você se sente confortável com aproximadamente um segundo de trabalho em buffer local, um prefetch próximo de 20 é um experimento razoável.

Como saber se o prefetch está muito alto

O prefetch provavelmente está muito alto quando:

  • messages_unacknowledged é grande em comparação com consumidores ativos.
  • Alguns consumidores têm muitas mensagens não confirmadas enquanto outros estão ociosos.
  • A latência da mensagem é alta mesmo quando messages_ready é baixo.
  • A memória do consumidor aumenta durante picos.
  • Uma falha do consumidor causa uma grande onda de reentregas.

Esse último ponto é fácil de perder. Se um worker mantém 1.000 mensagens não confirmadas e falha, o RabbitMQ pode reentregar essas mensagens. Esse é um comportamento correto, mas pode criar pressão duplicada em sistemas downstream se o handler não for idempotente.

Reduzir o prefetch geralmente melhora a justiça e o comportamento de recuperação. Pode reduzir um pouco o throughput de pico, mas pode melhorar a latência que os usuários realmente sentem.

Como saber se o prefetch está muito baixo

O prefetch provavelmente está muito baixo quando:

  • Os consumidores têm baixo uso de CPU e baixo uso de memória enquanto messages_ready continua crescendo.
  • O tempo de processamento é muito curto, mas a taxa de entrega é limitada.
  • A latência da rede entre consumidores e RabbitMQ é perceptível.
  • Aumentar o prefetch melhora o throughput sem aumentar a latência final ou a pressão na memória.

O exemplo clássico é um worker rápido que faz um pequeno cálculo em memória e confirma imediatamente. Com prefetch_count=1, pode gastar muito tempo esperando pela próxima mensagem. Aumentar o prefetch dá a ele um pequeno buffer local e o mantém ocupado.

Não esconda gargalos downstream

O ajuste de prefetch não vai consertar um banco de dados lento. Ele só pode mudar como o trabalho é distribuído e armazenado em buffer. Se cada mensagem espera na mesma API sobrecarregada, um prefetch mais alto pode fazer o throughput parecer melhor brevemente enquanto aumenta timeouts e novas tentativas.

Meça dentro do consumidor. Registre ou emita métricas para o tempo gasto decodificando a mensagem, esperando no banco de dados, chamando serviços externos e confirmando. O RabbitMQ pode mostrar contagens de prontas e não confirmadas, mas não pode dizer por que seu handler leva oito segundos.

Quando um serviço downstream tem limite de taxa, o prefetch geralmente deve ser menor, não maior. Deixe a fila absorver o backlog visivelmente em vez de esconder milhares de chamadas em andamento dentro dos workers.

Prefetch e concorrência são diferentes

Um prefetch de 50 não significa automaticamente que seu consumidor processa 50 mensagens em paralelo. Significa apenas que o RabbitMQ pode entregar 50 mensagens antes de receber confirmações. Se elas são executadas concorrentemente depende do seu código de consumidor.

Um consumidor single-threaded com prefetch 50 pode processar uma mensagem de cada vez enquanto 49 esperam na memória. Um pool de workers com concorrência 10 e prefetch 50 pode manter dez tarefas ativas e quarenta em buffer. Às vezes, esse buffer é útil. Às vezes, é apenas latência.

Combine prefetch com a concorrência real. Se seu processo pode executar cinco handlers de uma vez, um prefetch de 5 a 20 é mais fácil de raciocinar do que 500.

Compensações de ordenação e justiça

As filas do RabbitMQ preservam a ordem no nível da fila, mas o comportamento do consumidor pode mudar a ordem em que o trabalho é concluído. Com vários consumidores e prefetch maior que 1, a mensagem 20 pode terminar antes da mensagem 3 porque foi para um worker mais rápido ou teve trabalho mais fácil.

Para a maioria das filas de trabalho, a ordem de conclusão não importa. Para atualizações de conta, alterações de inventário ou fluxos de trabalho que devem ser processados em sequência, pode importar muito. Nesses casos, usar uma fila por chave de ordenação, fragmentar por chave ou manter o prefetch baixo pode ser mais seguro do que buscar o throughput máximo.

A justiça tem uma compensação semelhante. Um prefetch baixo permite que o RabbitMQ distribua o trabalho de forma mais uniforme porque os consumidores voltam para buscar mensagens com mais frequência. Um prefetch alto recompensa os consumidores que recebem mensagens primeiro. Se as mensagens têm tempos de processamento desiguais, isso pode levar a um worker segurando uma pilha de trabalhos lentos enquanto outro worker termina seu lote rapidamente.

Quando as pessoas dizem "o balanceamento de carga do RabbitMQ é desigual", o prefetch é uma das primeiras coisas a verificar. O broker só pode balancear mensagens que ainda não foram entregues.

O comportamento de falha é importante

O prefetch muda o que acontece quando um consumidor morre. Com prefetch_count=1, uma entrega não confirmada retorna quando o canal fecha. Com prefetch_count=500, centenas podem retornar de uma vez. Se o consumidor realizou efeitos colaterais parciais antes de falhar, essas reentregas podem desencadear gravações duplicadas, e-mails duplicados ou chamadas de API duplicadas, a menos que o handler seja idempotente.

Isso não significa que prefetch alto está errado. Significa que prefetch alto pertence a handlers idempotentes, regras claras de nova tentativa e monitoramento de taxas de reentrega. Se o processamento duplicado seria perigoso, mantenha a janela não confirmada pequena até que a aplicação seja construída para lidar com isso.

Observe o sinalizador de reentregado no consumidor. Não é um contador completo de novas tentativas, mas é um sinal útil de que a mensagem foi entregue antes. Para limites robustos de novas tentativas, rastreie tentativas em cabeçalhos ou no estado da aplicação e encaminhe mensagens exauridas para uma fila de mensagens mortas.

Múltiplas filas e cargas de trabalho mistas

Um valor de prefetch raramente se ajusta a todas as filas. Um serviço que consome thumbnail.generate e email.send pode precisar de configurações diferentes para cada um. A geração de thumbnails pode ser pesada em CPU e melhor com baixa concorrência. O envio de e-mail pode ser limitado pela rede e tolerar mais mensagens em andamento.

Se um único processo consome várias filas em um canal, o comportamento de QoS pode se tornar mais difícil de raciocinar. Prefira canais separados para cargas de trabalho significativamente diferentes. Isso torna o prefetch, o monitoramento e o tratamento de falhas mais óbvios.

Tamanhos de mensagem mistos são outro sinal de alerta. Se uma fila contém tanto eventos minúsculos quanto payloads enormes, um prefetch baseado em contagem não reflete bem a pressão na memória. Dez mensagens pequenas e dez mensagens grandes não têm o mesmo custo. Nessa situação, divida a carga de trabalho ou mova payloads grandes para fora do RabbitMQ e passe referências.

Observe o não confirmado por consumidor, não apenas por fila

Uma contagem não confirmada no nível da fila informa que há trabalho não concluído, mas pode esconder assimetria. Um consumidor pode manter a maioria das mensagens não confirmadas enquanto o resto está quase vazio. Isso geralmente aponta para prefetch alto, custo de mensagem desigual ou um worker não saudável.

Use métricas no nível do consumidor da interface de gerenciamento, Prometheus ou rabbitmqctl list_consumers durante um teste. Se a distribuição for desigual, reduzir o prefetch ou dividir tipos de mensagens lentas pode melhorar a latência real mesmo quando o throughput total muda apenas um pouco.

Revisite o prefetch após implantações

Os valores de prefetch envelhecem. Um valor que funcionava quando um handler escrevia apenas uma linha no banco de dados pode estar errado após o próximo lançamento adicionar uma chamada de API, validação extra ou um payload maior. Trate o prefetch como parte da configuração de desempenho, não um número que você define uma vez e esquece.

Após um lançamento de consumidor, compare a latência de processamento, contagens não confirmadas, reentregas e memória do consumidor com a versão anterior. Se a latência aumentar, mas a CPU não estiver saturada, o handler pode estar esperando por algo externo e um prefetch mais baixo pode manter o sistema mais justo. Se a CPU estiver alta e cada mensagem for limitada por CPU, adicionar workers ou reduzir o trabalho por mensagem pode importar mais do que mudar o prefetch.

Documente o motivo do valor escolhido próximo à configuração do consumidor. Mantenedores futuros devem saber se prefetch_count=5 foi escolhido por justiça, memória, ordenação, limites de taxa downstream ou apenas como um padrão temporário.

Teste com formas reais de mensagens

Não ajuste o prefetch com mensagens falsas minúsculas se as mensagens de produção são grandes payloads JSON ou incluem consultas caras ao banco de dados. O tamanho da mensagem e o custo do handler importam.

Um loop de teste útil é:

  1. Escolha um valor de prefetch.
  2. Execute uma taxa de publicação realista por tempo suficiente para ver um comportamento estável.
  3. Observe messages_ready, messages_unacknowledged, CPU do consumidor, memória do consumidor, latência de processamento e taxa de erro.
  4. Mate um consumidor e veja quantas mensagens são reentregues.
  5. Aumente ou diminua o prefetch e repita.

O melhor valor raramente é aquele com o maior throughput de benchmark curto. É o valor que mantém os consumidores ocupados, mantém a latência aceitável e falha de uma forma que seu sistema pode lidar.

Um padrão prático

Se você ainda não tem dados, comece com confirmações manuais e prefetch_count=10 para filas de trabalho comuns. Use 1 para processamento lento, caro ou estritamente justo. Tente 20 ou 50 para handlers rápidos e estáveis após medir. Vá mais alto apenas quando as métricas mostrarem que as viagens de ida e volta de entrega são o gargalo e os consumidores têm margem de memória.

O ajuste de prefetch do RabbitMQ não é uma configuração única. Revisite-o quando o tamanho da mensagem mudar, o código do consumidor mudar, as dependências downstream mudarem ou você adicionar mais instâncias de worker. O valor de prefetch certo é aquele que corresponde à forma atual do trabalho.