Résolution des problèmes de configuration courants de RabbitMQ

Trouvez et corrigez les erreurs d'échange, de file d'attente, de liaison, d'accusé de réception et d'autorisation RabbitMQ sans suivre de fausses pistes.

Résolution des problèmes de configuration courants de RabbitMQ

La plupart des problèmes de configuration RabbitMQ ressemblent d'abord à des bugs d'application. Un éditeur dit avoir envoyé le message. Un consommateur dit ne jamais l'avoir vu. Le graphique de la file d'attente est vide, ou pire, il est plein et personne ne sait pourquoi. Le moyen le plus rapide de s'en sortir est d'arrêter de deviner et de suivre le chemin du message : éditeur, échange, liaison, file d'attente, consommateur, accusé de réception.

RabbitMQ est strict concernant la topologie. Un échange direct ne correspond "presque" pas à une clé de routage. Une file d'attente déclarée comme exclusive ne se comportera pas comme une file d'attente de travail partagée. Un message publié comme obligatoire peut être retourné, tandis que le même message non routable sans mandatory peut simplement être supprimé par l'échange. Ces détails sont mineurs jusqu'à ce qu'ils vous coûtent un après-midi.

Commencez par la route réelle

Pour une publication AMQP normale, le producteur envoie un message à un échange avec une clé de routage. L'échange utilise son type et ses liaisons pour décider quelles files d'attente doivent recevoir le message. Les consommateurs récupèrent ensuite les livraisons des files d'attente et les accusent après traitement.

Lorsqu'un message disparaît, posez quatre questions :

  • Le producteur a-t-il publié sur l'échange et l'hôte virtuel que vous pensez ?
  • Cet échange existe-t-il et est-il du type que vous pensez ?
  • Existe-t-il une liaison de cet échange vers la file d'attente prévue ?
  • La clé de routage correspond-elle à cette liaison pour le type d'échange ?

Cela semble basique, mais cela permet de détecter de nombreux incidents réels. Les environnements de staging et de production utilisent souvent des hôtes virtuels différents. Un script de déploiement peut déclarer orders.created dans un environnement et order.created dans un autre. Une file d'attente peut être liée à un modèle de sujet qui manque un mot supplémentaire.

Utilisez l'interface de gestion ou la CLI pour inspecter le courtier en direct, pas le code que vous espérez voir fonctionner :

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

Si vous utilisez plusieurs hôtes virtuels, incluez -p :

rabbitmqctl -p production list_bindings source_name destination_name routing_key

Décalages de clé de routage

Les échanges directs nécessitent une correspondance exacte de la clé de liaison. Si une file d'attente est liée avec invoice.created, un message publié avec invoices.created n'arrivera pas. RabbitMQ ne corrigera pas la pluralisation, la casse, les points ou les tirets.

Les échanges de sujet utilisent * pour un mot et # pour zéro ou plusieurs mots. Le séparateur de mots est un point. Une liaison de logs.* correspond à logs.info, mais pas à logs.app.info. Une liaison de logs.# correspond aux deux.

Une astuce de dépannage utile consiste à ajouter une file d'attente de diagnostic temporaire avec une liaison large, puis à publier un message de test connu :

rabbitmqadmin declare queue name=debug.routing durable=false auto_delete=true
rabbitmqadmin declare binding source=events destination=debug.routing destination_type=queue routing_key='#'

Faites cela avec précaution en production et supprimez la liaison de diagnostic une fois terminé. Le but est de prouver si les messages atteignent l'échange.

Pour les éditeurs importants, activez les retours d'éditeur avec le drapeau mandatory afin que les messages non routables soient visibles par l'éditeur. Les confirmations d'éditeur vous indiquent que le courtier a accepté la publication ; les retours vous indiquent que l'échange n'a pas pu le router vers une file d'attente. Ils répondent à des questions différentes.

Erreurs de type d'échange

Les changements de type d'échange sont une source courante de confusion car déclarer un échange existant avec des propriétés différentes échoue. Si un service déclare events comme topic et un autre déclare events comme direct, la deuxième déclaration devrait obtenir un échec de condition préalable.

Cet échec est bon. Il empêche deux applications de ne pas être d'accord silencieusement sur le routage. La correction n'est pas d'attraper et d'ignorer l'exception. La correction est de clarifier la propriété de la topologie. Habituellement, une étape de déploiement ou un module d'infrastructure doit déclarer les échanges et files d'attente partagés, tandis que les applications ne déclarent que des files d'attente de réponse privées ou affirment de manière idempotente la topologie attendue.

Les échanges de type fanout ignorent les clés de routage. Les échanges d'en-têtes routent par en-têtes, pas par clés de routage. Si votre message de test a la bonne clé de routage mais qu'aucune file d'attente ne le reçoit, vérifiez le type d'échange avant de modifier chaque liaison.

Propriétés de file d'attente qui surprennent

Durable signifie que la définition de la file d'attente survit à un redémarrage du courtier. Cela ne rend pas chaque message à l'intérieur de la file d'attente persistant. Pour que les messages survivent à un redémarrage, la file d'attente doit être durable et le message doit être publié comme persistant. Même dans ce cas, les éditeurs doivent utiliser des confirmations s'ils ont besoin de savoir quand RabbitMQ a accepté le message en toute sécurité.

Les files d'attente à suppression automatique sont supprimées après le départ de leur dernier consommateur. Elles sont utiles pour les abonnements temporaires, mais elles ne conviennent pas aux files d'attente de travail partagées. Les files d'attente exclusives sont limitées à la connexion qui les déclare et disparaissent lorsque cette connexion se ferme. Elles sont utiles pour les files d'attente de réponse et les consommateurs privés, pas pour plusieurs instances de travailleurs.

Si une file d'attente semble "disparaître aléatoirement", vérifiez ces drapeaux :

rabbitmqctl list_queues name durable auto_delete exclusive consumers

Vérifiez également si le code de l'application déclare la file d'attente au démarrage avec des arguments différents de ceux de la file d'attente existante. RabbitMQ traite les arguments de file d'attente tels que le type de file d'attente, l'échange de lettres mortes, la longueur maximale et certains paramètres liés à la durabilité comme faisant partie du contrat de déclaration. Une discordance peut fermer le canal avec un échec de condition préalable.

Les messages sont prêts, mais les consommateurs ne font rien

Si messages_ready est élevé et consumers est zéro, RabbitMQ attend. L'application consommateur peut être en panne, connectée au mauvais hôte virtuel, utilisant le mauvais nom de file d'attente ou bloquée par les autorisations.

Si des consommateurs sont connectés mais que les livraisons ne se produisent pas, vérifiez la prélecture et la capacité du consommateur :

rabbitmqctl list_consumers queue_name channel_pid consumer_tag ack_required prefetch_count active

Un consommateur avec des accusés de réception manuels et une fenêtre de prélecture pleine ne recevra plus de messages jusqu'à ce qu'il accuse ou n'accuse certains des messages qu'il a déjà. Cela ressemble souvent à RabbitMQ qui a arrêté la livraison, alors que le consommateur détient en fait un travail non acquitté.

Si messages_unacknowledged est élevé, regardez les journaux du consommateur et les systèmes en aval. Une base de données lente, une dépendance HTTP bloquée ou un gestionnaire qui attrape des exceptions sans accuser peuvent tous créer un mur de messages non acquittés.

Bugs d'accusé de réception

Les accusés de réception manuels sont le choix normal pour un traitement fiable. Le consommateur ne doit accuser qu'après la fin du travail. En cas d'échec, il doit rejeter ou n'accuser avec une décision délibérée de remettre en file d'attente.

Le modèle dangereux est auto_ack=true pour un travail qui peut échouer. Avec les accusés de réception automatiques, RabbitMQ considère le message comme traité dès qu'il est livré. Si le consommateur plante après l'avoir reçu, le message disparaît de la file d'attente.

Le bug opposé est de ne jamais accuser. Le consommateur traite le message avec succès, peut-être même écrit dans une base de données, mais oublie basic_ack. RabbitMQ conserve la livraison non acquittée jusqu'à la fermeture du canal, puis la redistribue. Cela crée un travail en double et des comptes non acquittés croissants.

Une forme de gestionnaire simple est plus facile à auditer :

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)

Si vous remettez en file d'attente chaque échec indéfiniment, un mauvais message peut boucler sans fin. Utilisez un échange de lettres mortes ou une conception de nouvelle tentative pour les messages empoisonnés.

Autorisations et hôtes virtuels

Les autorisations RabbitMQ sont limitées par hôte virtuel. Un utilisateur peut être capable de se connecter mais manquer toujours des autorisations de configuration, d'écriture ou de lecture pour une file d'attente ou un échange. Cela peut apparaître comme une exception de canal dans les journaux du client, pas toujours comme une erreur d'application conviviale.

Vérifiez les autorisations directement :

rabbitmqctl list_permissions -p production
rabbitmqctl list_user_permissions app_user

Pour un service qui ne fait que publier, accordez des autorisations d'écriture sur le modèle d'échange dont il a besoin et évitez les droits de configuration larges. Pour un consommateur, accordez la lecture sur la file d'attente et l'écriture s'il doit publier des nacks vers des chemins de lettres mortes ou utiliser des modèles de réponse. Des autorisations trop larges facilitent le dépannage aujourd'hui et rendent les examens de sécurité plus difficiles demain.

Erreurs de configuration des lettres mortes

Les échanges de lettres mortes sont censés rendre les échecs visibles. Une configuration incorrecte des lettres mortes fait le contraire : les messages échouent, sont rejetés, puis disparaissent dans un échange qui n'a pas de liaison.

Vérifiez les arguments de la file d'attente, pas seulement le nom de la file d'attente :

rabbitmqctl list_queues name arguments

Pour une file d'attente qui devrait mettre en lettre morte les travaux échoués, vous devriez voir des arguments tels que x-dead-letter-exchange et, parfois, x-dead-letter-routing-key. Ensuite, inspectez cet échange et ses liaisons de la même manière que vous inspectez la route principale.

Une erreur courante est de configurer un échange de lettres mortes appelé jobs.dlx mais de lier la file d'attente de lettres mortes à jobs.failed sur un échange différent. Une autre est de définir x-dead-letter-routing-key sur une valeur qu'aucune liaison ne correspond. RabbitMQ routera le message mis en lettre morte à travers l'échange de lettres mortes comme toute autre publication. Si rien ne correspond, le message n'a nulle part où aller d'utile.

Les files d'attente de nouvelle tentative nécessitent la même attention. Si vous construisez une nouvelle tentative avec TTL plus mise en lettre morte, dessinez la route sur papier :

file d'attente principale -> rejeter -> échange de nouvelle tentative -> file d'attente de nouvelle tentative -> TTL expire -> échange principal -> file d'attente principale

Ensuite, vérifiez chaque échange, file d'attente, liaison et clé de routage. Les boucles de nouvelle tentative sont faciles à créer accidentellement. Mettez une limite sur les tentatives dans les en-têtes de message ou l'état de l'application afin qu'une charge utile défectueuse ne tourne pas indéfiniment.

Surprises des politiques

Les politiques peuvent modifier le comportement des files d'attente sans que le code de l'application ne le mentionne. Une politique peut définir le type de file d'attente, la longueur maximale, le TTL, l'échange de lettres mortes ou d'autres arguments optionnels. Cela est utile pour les opérations, mais peut compliquer le débogage lorsqu'une file d'attente se comporte différemment de la déclaration de code.

Listez les politiques lors du dépannage :

rabbitmqctl list_policies

Regardez le modèle et la priorité. Une politique large comme .* peut affecter les files d'attente créées plus tard par des équipes non liées. Si une file d'attente supprime les messages plus anciens, vérifiez les paramètres max-length ou de débordement. Si les messages expirent plus tôt que prévu, vérifiez le TTL au niveau de la file d'attente et l'expiration par message.

Lorsque l'application déclare un ensemble d'arguments et qu'une politique en applique un autre, les règles de RabbitMQ dépendent du paramètre. Certains arguments optionnels peuvent être contrôlés par politique ; d'autres doivent correspondre à la déclaration. L'habitude opérationnelle sûre est de garder le comportement de la file d'attente dans un endroit évident et de documenter toute politique qui remplace intentionnellement les valeurs par défaut de l'application.

Quand les producteurs et consommateurs déclarent la topologie

De nombreuses bibliothèques clientes facilitent la déclaration des échanges, files d'attente et liaisons par chaque service au démarrage. Cela peut être pratique en développement. En production, cela peut créer des problèmes de propriété.

Si le producteur et le consommateur déclarent tous deux la même file d'attente, ils doivent être d'accord sur chaque propriété importante. Si un déploiement change une file d'attente de auto-delete à durable, ou change un argument de lettre morte, le prochain service à démarrer peut échouer avec une erreur de condition préalable. C'est mieux qu'une dérive silencieuse, mais cela peut toujours casser un déploiement.

Pour une topologie partagée, préférez un propriétaire : Terraform, Ansible, un travail de migration, ou un service clairement responsable. Le démarrage de l'application peut toujours affirmer que la topologie attendue existe, mais ne devrait pas créer à la légère des files d'attente partagées avec des valeurs par défaut que personne n'a examinées.

La topologie privée est différente. Un service créant une file d'attente de réponse temporaire ou une file d'attente d'abonnement exclusive peut posséder cette file d'attente directement. La différence est de savoir si un autre service dépend du nom et du comportement de la file d'attente.

Gardez un chemin de publication connu et fonctionnel

Pour les systèmes importants, gardez un petit éditeur de diagnostic ou une commande de runbook qui envoie un message inoffensif via l'échange, la clé de routage et l'hôte virtuel attendus. Il doit utiliser la même classe d'identifiants que l'application réelle, ou au moins un ensemble d'autorisations suffisamment proche pour détecter les problèmes de routage et d'accès.

Ce chemin connu est utile lors des déploiements. Si le message de diagnostic est routé mais que le message de l'application ne l'est pas, comparez la clé de routage réelle, les en-têtes et l'hôte virtuel de l'application. Si le message de diagnostic échoue également, le problème est probablement la topologie, les autorisations ou l'état du courtier.

Une liste de contrôle pratique pour les incidents

Lorsque des messages sont manquants ou bloqués, collectez l'état en direct avant de redémarrer quoi que ce soit :

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

Envoyez ensuite un message de test connu avec un ID unique et tracez-le à travers les journaux. Ne testez pas avec un message de production aléatoire dont le chemin est déjà flou.

La plupart des problèmes de configuration RabbitMQ ne sont pas mystérieux une fois que vous alignez la topologie déclarée avec le comportement de l'éditeur et du consommateur. Le courtier fait généralement exactement ce qu'on lui a dit. Le travail consiste à trouver l'endroit où ce qu'on lui a dit diffère de ce que l'équipe voulait.