Resolvendo Problemas Comuns de Configuração do RabbitMQ
Encontre e corrija erros de exchange, fila, binding, confirmação e permissão no RabbitMQ sem seguir pistas falsas.
Resolvendo Problemas Comuns de Configuração do RabbitMQ
A maioria dos problemas de configuração do RabbitMQ parece, à primeira vista, ser um bug na aplicação. Um publisher diz que enviou a mensagem. Um consumer diz que nunca a viu. O gráfico da fila está vazio ou, pior, está cheio e ninguém sabe por quê. A maneira mais rápida de sair dessa situação é parar de adivinhar e seguir o caminho da mensagem: publisher, exchange, binding, fila, consumer, confirmação.
O RabbitMQ é rigoroso quanto à topologia. Uma exchange do tipo direct não corresponde "aproximadamente" a uma chave de roteamento. Uma fila declarada como exclusiva não se comportará como uma fila de trabalho compartilhada. Uma mensagem publicada como obrigatória pode ser retornada, enquanto a mesma mensagem não roteável sem mandatory pode simplesmente ser descartada pela exchange. Esses detalhes são pequenos até custarem uma tarde inteira.
Comece pela rota real
Em uma publicação AMQP normal, o produtor envia uma mensagem para uma exchange com uma chave de roteamento. A exchange usa seu tipo e bindings para decidir quais filas devem receber a mensagem. Os consumers então retiram as entregas das filas e as confirmam após o processamento.
Quando uma mensagem desaparece, faça quatro perguntas:
- O produtor publicou na exchange e no virtual host que você pensa?
- Essa exchange existe e é do tipo que você pensa?
- Existe um binding dessa exchange para a fila desejada?
- A chave de roteamento corresponde a esse binding para o tipo de exchange?
Isso parece básico, mas resolve muitos incidentes reais. Os ambientes de staging e produção geralmente usam virtual hosts diferentes. Um script de implantação pode declarar orders.created em um ambiente e order.created em outro. Uma fila pode estar vinculada a um padrão de tópico que perde uma palavra extra.
Use a interface de gerenciamento ou a CLI para inspecionar o broker ativo, não o código que você espera que esteja em execução:
rabbitmqctl list_exchanges name type durable auto_delete
rabbitmqctl list_bindings source_name source_kind destination_name destination_kind routing_key
rabbitmqctl list_queues name durable auto_delete exclusive messages_ready messages_unacknowledged consumers
Se você usar vários virtual hosts, inclua -p:
rabbitmqctl -p production list_bindings source_name destination_name routing_key
Incompatibilidades de chave de roteamento
Exchanges do tipo direct exigem uma correspondência exata da chave de binding. Se uma fila estiver vinculada com invoice.created, uma mensagem publicada com invoices.created não chegará. O RabbitMQ não corrigirá pluralização, maiúsculas/minúsculas, pontos ou traços.
Exchanges do tipo topic usam * para uma palavra e # para zero ou mais palavras. O separador de palavras é o ponto. Um binding de logs.* corresponde a logs.info, mas não a logs.app.info. Um binding de logs.# corresponde a ambos.
Um truque útil de depuração é adicionar uma fila de diagnóstico temporária com um binding amplo e, em seguida, publicar uma mensagem de teste conhecida:
rabbitmqadmin declare queue name=debug.routing durable=false auto_delete=true
rabbitmqadmin declare binding source=events destination=debug.routing destination_type=queue routing_key='#'
Faça isso com cuidado em produção e remova o binding de diagnóstico quando terminar. O objetivo é provar se as mensagens estão chegando à exchange.
Para publishers importantes, habilite os retornos de publisher com a flag mandatory para que mensagens não roteáveis sejam visíveis para o publisher. As confirmações de publisher informam que o broker aceitou a publicação; os retornos informam que a exchange não conseguiu roteá-la para nenhuma fila. Eles respondem a perguntas diferentes.
Erros de tipo de exchange
Mudanças no tipo de exchange são uma fonte comum de confusão porque declarar uma exchange existente com propriedades diferentes falha. Se um serviço declara events como topic e outro declara events como direct, a segunda declaração deve resultar em uma falha de pré-condição.
Essa falha é boa. Ela impede que dois aplicativos discordem silenciosamente sobre o roteamento. A correção não é capturar e ignorar a exceção. A correção é tornar a propriedade da topologia clara. Geralmente, uma etapa de implantação ou módulo de infraestrutura deve declarar exchanges e filas compartilhadas, enquanto os aplicativos declaram apenas filas de resposta privadas ou afirmam idempotentemente a topologia esperada.
Exchanges do tipo fanout ignoram chaves de roteamento. Exchanges do tipo headers roteiam por cabeçalhos, não por chaves de roteamento. Se sua mensagem de teste tem a chave de roteamento correta, mas nenhuma fila a recebe, verifique o tipo da exchange antes de editar cada binding.
Propriedades de fila que surpreendem as pessoas
Durable significa que a definição da fila sobrevive a uma reinicialização do broker. Isso não faz com que todas as mensagens dentro da fila sobrevivam. Para que as mensagens sobrevivam a uma reinicialização, a fila deve ser durável e a mensagem deve ser publicada como persistente. Mesmo assim, os publishers devem usar confirmações se precisarem saber quando o RabbitMQ aceitou a mensagem com segurança.
Filas auto-delete são removidas após a saída do último consumidor. Elas são úteis para assinaturas temporárias, mas são inadequadas para filas de trabalho compartilhadas. Filas exclusivas têm escopo limitado à conexão que as declara e desaparecem quando essa conexão é fechada. Elas são úteis para filas de resposta e consumers privados, não para múltiplas instâncias de worker.
Se uma fila parece "desaparecer aleatoriamente", verifique estas flags:
rabbitmqctl list_queues name durable auto_delete exclusive consumers
Verifique também se o código do aplicativo declara a fila na inicialização com argumentos diferentes da fila existente. O RabbitMQ trata os argumentos da fila, como tipo de fila, dead-letter exchange, comprimento máximo e algumas configurações relacionadas à durabilidade, como parte do contrato de declaração. Uma incompatibilidade pode fechar o canal com uma falha de pré-condição.
Mensagens estão prontas, mas os consumers não fazem nada
Se messages_ready está alto e consumers é zero, o RabbitMQ está esperando. O aplicativo consumer pode estar inativo, conectado ao virtual host errado, usando o nome de fila errado ou bloqueado por permissões.
Se os consumers estão conectados, mas as entregas não estão acontecendo, verifique o prefetch e a capacidade do consumidor:
rabbitmqctl list_consumers queue_name channel_pid consumer_tag ack_required prefetch_count active
Um consumer com confirmações manuais e uma janela de prefetch cheia não receberá mais mensagens até que faça ack ou nack de algumas das mensagens que já possui. Isso geralmente parece que o RabbitMQ parou de entregar, quando na verdade o consumer está segurando trabalho não confirmado.
Se messages_unacknowledged está alto, examine os logs do consumidor e os sistemas downstream. Um banco de dados lento, uma dependência HTTP travada ou um handler que captura exceções sem fazer ack podem criar uma parede de mensagens não confirmadas.
Erros de confirmação
Confirmações manuais são a escolha normal para processamento confiável. O consumidor deve fazer ack somente após a conclusão do trabalho. Se falhar, deve rejeitar ou fazer nack com uma decisão deliberada de reenfileiramento.
O padrão perigoso é auto_ack=true para trabalhos que podem falhar. Com confirmações automáticas, o RabbitMQ considera a mensagem tratada assim que é entregue. Se o consumidor falhar após recebê-la, a mensagem desaparece da fila.
O bug oposto é nunca fazer ack. O consumidor processa a mensagem com sucesso, talvez até escreva em um banco de dados, mas esquece o basic_ack. O RabbitMQ mantém a entrega não confirmada até que o canal seja fechado e, em seguida, a reentrega. Isso cria trabalho duplicado e contagens crescentes de não confirmados.
Um formato de handler simples é mais fácil de auditar:
def handle(ch, method, properties, body):
try:
process(body)
except RetryableError:
ch.basic_nack(method.delivery_tag, requeue=True)
except Exception:
ch.basic_nack(method.delivery_tag, requeue=False)
else:
ch.basic_ack(method.delivery_tag)
Se você reenfileirar toda falha para sempre, uma mensagem ruim pode entrar em loop infinito. Use uma dead-letter exchange ou um design de repetição para mensagens problemáticas.
Permissões e virtual hosts
As permissões do RabbitMQ têm escopo por virtual host. Um usuário pode conseguir se conectar, mas ainda assim não ter permissões de configurar, escrever ou ler para uma fila ou exchange. Isso pode aparecer como uma exceção de canal nos logs do cliente, nem sempre como um erro amigável do aplicativo.
Verifique as permissões diretamente:
rabbitmqctl list_permissions -p production
rabbitmqctl list_user_permissions app_user
Para um serviço que apenas publica, conceda permissões de escrita ao padrão de exchange que ele precisa e evite direitos amplos de configuração. Para um consumidor, conceda leitura na fila e escrita se ele precisar publicar nacks em caminhos de dead-letter ou usar padrões de resposta. Permissões excessivamente amplas facilitam a depuração hoje, mas dificultam as revisões de segurança amanhã.
Erros de configuração de dead-letter
Dead-letter exchanges devem tornar as falhas visíveis. Uma configuração incorreta de dead-letter faz o oposto: as mensagens falham, são rejeitadas e desaparecem em uma exchange que não tem binding.
Verifique os argumentos da fila, não apenas o nome da fila:
rabbitmqctl list_queues name arguments
Para uma fila que deve dead-letter trabalhos com falha, você deve ver argumentos como x-dead-letter-exchange e, às vezes, x-dead-letter-routing-key. Em seguida, inspecione essa exchange e seus bindings da mesma forma que inspeciona a rota principal.
Um erro comum é configurar uma dead-letter exchange chamada jobs.dlx, mas vincular a fila de dead-letter a jobs.failed em uma exchange diferente. Outro é definir x-dead-letter-routing-key para um valor que nenhum binding corresponde. O RabbitMQ roteará a mensagem dead-lettered através da dead-letter exchange como qualquer outra publicação. Se nada corresponder, a mensagem não terá para onde ir.
Filas de repetição precisam do mesmo cuidado. Se você construir repetição com TTL mais dead-lettering, desenhe a rota no papel:
fila principal -> rejeitar -> exchange de repetição -> fila de repetição -> TTL expira -> exchange principal -> fila principal
Em seguida, verifique cada exchange, fila, binding e chave de roteamento. Loops de repetição são fáceis de criar acidentalmente. Coloque um limite de tentativas nos cabeçalhos da mensagem ou no estado do aplicativo para que uma carga útil quebrada não gire para sempre.
Surpresas com políticas
As políticas podem alterar o comportamento da fila sem que o código do aplicativo as mencione. Uma política pode definir tipo de fila, comprimento máximo, TTL, dead-letter exchange ou outros argumentos opcionais. Isso é útil para operações, mas pode confundir a depuração quando uma fila se comporta de maneira diferente da declaração do código.
Liste as políticas durante a depuração:
rabbitmqctl list_policies
Observe o padrão e a prioridade. Uma política ampla como .* pode afetar filas criadas posteriormente por equipes não relacionadas. Se uma fila está descartando mensagens mais antigas, verifique as configurações de max-length ou overflow. Se as mensagens expiram mais cedo do que o esperado, verifique o TTL no nível da fila e a expiração por mensagem.
Quando o aplicativo declara um conjunto de argumentos e uma política aplica outro, as regras do RabbitMQ dependem da configuração. Alguns argumentos opcionais podem ser controlados por política; outros devem corresponder à declaração. O hábito operacional seguro é manter o comportamento da fila em um lugar óbvio e documentar qualquer política que substitua intencionalmente os padrões do aplicativo.
Quando produtores e consumidores declaram topologia
Muitas bibliotecas de cliente facilitam para cada serviço declarar exchanges, filas e bindings na inicialização. Isso pode ser conveniente no desenvolvimento. Em produção, pode criar problemas de propriedade.
Se tanto o produtor quanto o consumidor declararem a mesma fila, eles devem concordar em todas as propriedades importantes. Se uma implantação alterar uma fila de auto-delete para durável, ou alterar um argumento de dead-letter, o próximo serviço a iniciar pode falhar com um erro de pré-condição. Isso é melhor do que uma deriva silenciosa, mas ainda pode quebrar uma implantação.
Para topologia compartilhada, prefira um proprietário: Terraform, Ansible, um job de migração ou um serviço claramente responsável. A inicialização do aplicativo ainda pode afirmar que a topologia esperada existe, mas não deve criar casualmente filas compartilhadas com padrões que ninguém revisou.
Topologia privada é diferente. Um serviço que cria uma fila de resposta temporária ou uma fila de assinatura exclusiva pode possuir essa fila diretamente. A diferença é se outro serviço depende do nome e do comportamento da fila.
Mantenha um caminho de publicação conhecido e funcional
Para sistemas importantes, mantenha um pequeno publisher de diagnóstico ou um comando runbook que envie uma mensagem inofensiva através da exchange, chave de roteamento e virtual host esperados. Ele deve usar a mesma classe de credenciais do aplicativo real, ou pelo menos um conjunto de permissões próximo o suficiente para detectar problemas de roteamento e acesso.
Esse caminho conhecido e funcional é útil durante as implantações. Se a mensagem de diagnóstico for roteada, mas a mensagem do aplicativo não, compare a chave de roteamento, os cabeçalhos e o virtual host reais do aplicativo. Se a mensagem de diagnóstico também falhar, o problema provavelmente é topologia, permissões ou estado do broker.
Uma lista de verificação prática para incidentes
Quando as mensagens estão faltando ou presas, colete o estado ativo antes de reiniciar tudo:
rabbitmqctl -p production list_queues name messages_ready messages_unacknowledged consumers state
rabbitmqctl -p production list_bindings source_name destination_name routing_key
rabbitmqctl -p production list_exchanges name type durable
rabbitmqctl -p production list_connections name user vhost state
Em seguida, envie uma mensagem de teste conhecida com um ID único e rastreie-a através dos logs. Não teste com uma mensagem de produção aleatória cujo caminho já não está claro.
A maioria dos problemas de configuração do RabbitMQ não é misteriosa depois que você alinha a topologia declarada com o comportamento do publisher e do consumidor. O broker geralmente está fazendo exatamente o que lhe foi dito. O trabalho é encontrar o lugar onde o que lhe foi dito difere do que a equipe pretendia.