Files d'attente de messages morts : Gestion des messages toxiques dans les systèmes distribués

Empêchez les messages corrompus de bloquer les files d'attente

Sommaire

Une file d’attente de messages morts (dead-letter queue) est le filet de sécurité qui capture les messages que vos consommateurs ne peuvent pas traiter, afin qu’un seul payload défectueux ne bloque pas ou ne fasse pas disparaître silencieusement tous les messages suivants dans la file d’attente.

Tout système axé sur les messages finit par recevoir un message qu’il ne peut pas gérer : un payload mal formé, un schéma qui a changé sous le consommateur, ou un appel à un service en aval qui échoue malgré de multiples tentatives de renvoi. Sans file d’attente de messages morts, ce message bloque soit indéfiniment la tête de la file d’attente, soit est discrètement ignoré, et les deux issues sont pire que d’avoir connaissance de l’échec.

Une file d’attente de messages morts transforme une erreur invisible en une erreur visible et inspectable. Elle vous donne un endroit pour mettre en quarantaine le message, déclencher une alerte, et décider — délibérément, et non par accident — s’il faut le corriger et le rejouer, ou le supprimer définitivement.

dead letter queue routing failed messages away from the main queue

Les mécaniques diffèrent selon les courtiers (brokers), mais le schéma sous-jacent est le même partout : un compteur de tentatives de livraison, un seuil, et une destination pour les messages qui le dépassent. Ce guide explique ce qu’une file d’attente de messages morts fait réellement, comment distinguer un message empoisonné d’une erreur transitoire, quand renvoyer le message plutôt que l’ignorer, et comment rejouer en toute sécurité une fois la cause racine corrigée. Pour un contexte plus large sur les modèles d’intégration dans lesquels ce modèle s’inscrit, consultez Architecture de l’application.

Qu’est-ce qu’une file d’attente de messages morts ?

Une file d’attente de messages morts est une file d’attente ordinaire et distincte vers laquelle un courtier ou un consommateur achemine un message après que ce dernier a échoué trop souvent lors du traitement. Il ne s’agit pas d’un construit spécial : la file d’attente de messages morts de RabbitMQ est une file d’attente régulière liée à un échange régulier, et une file d’attente de messages morts SQS est une file d’attente standard ou FIFO ordinaire. Ce qui fait qu’une file d’attente est une « file d’attente de messages morts », c’est uniquement le fait qu’un autre composant y adresse les messages en échec.

flowchart LR P[Producteur] --> Q[File d'attente principale] Q --> C[Consommateur] C -- ack : succès --> Supprimé[Message supprimé] C -- échec / nack / timeout --> Q Q -- budget de renvoi épuisé --> DLQ[File d'attente de messages morts] DLQ --> I[Inspecter / alerter] I -- corriger la cause racine --> R[Rejouer sur la file principale] I -- irrécupérable --> D[Archiver / supprimer]

Chaque courtier implémente la redirection différemment :

  • Amazon SQS utilise une politique de redémarrage avec un maxReceiveCount. Une fois qu’un message a été reçu ce nombre de fois sans être supprimé, SQS le déplace vers l’deadLetterTargetArn configuré. AWS recommande explicitement de conserver une période de rétention des messages pour la file d’attente de messages morts plus longue que celle de la file source, car l’horodatage initial de l’enregistrement — et non l’heure du déplacement — régit toujours l’expiration.
  • RabbitMQ met un message en file d’attente de messages morts lorsqu’il est rejeté avec requeue=false, que son TTL par message expire, que la file atteint une limite de longueur, ou qu’une file de quorum dépasse son delivery-limit. Vous configurez cela avec les arguments de file x-dead-letter-exchange (et éventuellement x-dead-letter-routing-key), et RabbitMQ attache des en-têtes x-death enregistrant la raison, la file d’origine et le nombre de fois où cela s’est produit.
  • Apache Kafka ne dispose pas de file d’attente de messages morts native au niveau du courtier. Kafka ne suit que les décalages (offsets) ; il n’a pas de concept de message « échoué ». Le modèle de topic de messages morts est quelque chose que vous construisez dans le consommateur, dans une topologie Kafka Streams, ou dans un connecteur Kafka Connect — couramment associé à un niveau de topics de renvoi avant le DLT terminal, comme le font @RetryableTopic et DeadLetterPublishingRecoverer de Spring Kafka.
  • Azure Service Bus met automatiquement en file d’attente de messages morts une fois que le compte de livraison d’un message dépasse MaxDeliveryCount (10 par défaut), et également pour quelques raisons système telles que TTLExpiredException, HeaderSizeExceeded et MaxTransferHopCountExceeded, chacune enregistrée dans la propriété DeadLetterReason du message.

Pour une vue plus large de la façon dont les courtiers et les plateformes de streaming s’articulent opérationnellement plutôt que comme un modèle de fiabilité, Apache Kafka Quickstart et RabbitMQ sur AWS EKS vs SQS couvrent l’aspect infrastructure de l’exécution de ces courtiers.

Messages empoisonnés

Un message empoisonné est un message qui ne réussira jamais, peu importe le nombre de tentatives de renvoi effectuées par un consommateur : un payload JSON mal formé, un champ de schéma renommé par un producteur, une violation d’une règle métier, ou un bug qui lance une exception sur une entrée spécifique à chaque fois. Cela est différent d’une erreur transitoire, où le message est correct mais l’environnement ne l’est pas brièvement : un timeout en aval, une micro-coupure de connexion à la base de données, une réponse de limite de débit.

Traiter les deux types d’échec de la même manière est l’erreur la plus courante concernant les files d’attente de messages morts. Si vous mettez en file d’attente de messages morts dès le premier échec, vous punissez les erreurs transitoires qui auraient réussi lors du renvoi. Si vous renvoyez des messages empoisonnés des dizaines de fois avant d’abandonner, vous gaspillez de la capacité de calcul, retardez les messages non pertinents situés derrière eux (sur les files ordonnées et les partitions), et inondez vos logs de la même trace de pile.

Quelques signaux de détection aident à séparer les deux cas :

  • Type d’exception. Les erreurs de désérialisation, les erreurs de validation et les échecs de type ClassCastException sont presque toujours permanents. Le DefaultErrorHandler de Spring Kafka traite explicitement certaines exceptions comme fatales et saute les tentatives de renvoi pour celles-ci au lieu d’épuiser le budget de renvoi en premier.
  • Compte de répétitions sans variance. Le tableau d’en-têtes x-death de RabbitMQ vous permet de voir exactement combien de fois un message a été mis en file d’attente de messages morts et pourquoi ; un message avec un compte croissant et une x-first-death-reason identique à chaque cycle est empoisonné, pas simplement malchanceux.
  • Échec cohérent sur les réplicas. Si chaque instance de consommateur échoue sur le même message tout en réussissant sur tout ce qui l’entoure, le message lui-même est le problème, et non l’infrastructure.

Pour distinguer les erreurs renvoyables des erreurs non renvoyables au niveau du code — la même classification dont dépend la politique de file d’attente de messages morts — consultez Architecture de gestion des erreurs Go : Frontières et Modèles.

Renvoi vs Suppression

La décision de politique principale derrière toute file d’attente de messages morts est le seuil de renvoi : combien de tentatives de livraison un message obtient avant d’être mis en quarantaine. Si ce seuil est trop bas, vous mettez en file d’attente de messages morts des messages qui auraient réussi après une brève hésitation en aval. S’il est trop élevé, un message empoisonné reste dans la file principale pendant longtemps, consommant la capacité des travailleurs et — sur les systèmes ordonnés — bloquant tout ce qui est en attente derrière lui.

Les recommandations actuelles convergent vers des chiffres similaires sur les principaux courtiers :

Courtier Mécanisme Seuil typique
Amazon SQS maxReceiveCount dans la politique de redémarrage 3–5 pour des charges de travail mixtes (transitoires/permanentes)
RabbitMQ (files de quorum) Argument de politique delivery-limit 3–5, ajusté par file
Azure Service Bus MaxDeliveryCount 10 par défaut, souvent réduit pour les files sensibles à la latence
Kafka (via topics de renvoi) En-tête de compte de renvoi + niveau de topic de renvoi 3–4 sauts vers le topic de renvoi avant le DLT terminal

Un terrain d’entente pratique sur lequel beaucoup d’équipes tombent est : commencer de manière conservatrice (2–3 tentatives), observer la composition réelle des échecs en production, et augmenter le seuil uniquement pour les files où vous pouvez montrer que la plupart des échecs se résolvent en quelques renvois. Associez le compte de renvoi à une remultiplication exponentielle avec bruit (jitter) entre les tentatives afin qu’une panne en aval ne se transforme pas en tempête de renvoi — la même discipline couverte dans la conception de la remultiplication et du disjoncteur. Un disjoncteur à la frontière d’intégration complète ce dispositif : il empêche d’envoyer des requêtes vers une dépendance en difficulté au lieu de laisser chaque message dans la file découvrir individuellement la panne et se mettre en file d’attente de messages morts un par un.

Une fois qu’un message est dans la file d’attente de messages morts, la « suppression » doit toujours être une action délibérée, et non une négligence. Définissez une période de rétention sur la file d’attente de messages morts elle-même — assez longue pour permettre l’enquête (AWS recommande que la rétention de la file d’attente de messages morts dépasse celle de la file source ; une semaine est un plancher courant pour les files de messages morts RabbitMQ) — et déclenchez des alertes sur la profondeur et l’âge de la file d’attente de messages morts afin que les échecs soient triés plutôt que d’expirer silencieusement. Un message qui expire de la file d’attente de messages morts sans être examiné est un message que vous avez décidé de perdre sans avoir choisi de le perdre.

L’idempotence est tout aussi importante ici que partout ailleurs : des doublons peuvent survenir ; un message qui est redirigé d’une file d’attente de messages morts vers la file principale est, fonctionnellement, une livraison en double. Si votre consommateur n’est pas sûr d’être exécuté deux fois sur le même message, la redirection depuis une file d’attente de messages morts peut créer le bug exact d’effet secondaire en double que vous cherchiez à éviter. Consultez Idempotence dans les systèmes distribués qui fonctionnent réellement pour les modèles côté consommateur qui rendent la redirection sûre.

Stratégies de rejeu

Sortir correctement un message de la file d’attente de messages morts est sa propre discipline, distincte de la manière de l’y introduire.

  1. Corrigez d’abord la cause racine. Déployer la correction du consommateur avant de rejouer fait la différence entre une récupération propre et le re-empoisonnement de la file avec la même erreur une seconde fois.
  2. Redirigez délibérément, pas automatiquement. SQS prend en charge une fonctionnalité de redirection vers la source qui déplace les messages vers leur file d’origine (ou une autre destination) à la demande ; RabbitMQ et Kafka exigent que vous construisiez vous-même l’équivalent du consommateur ou des outils. Dans les deux cas, traitez le rejeu comme une action déclenchée par un opérateur avec un enregistrement de ce qui a été rejoué et quand.
  3. Préservez l’ordre là où il importe. Pour Kafka, le topic de messages morts doit avoir au moins autant de partitions que le topic source et doit conserver la clé de message d’origine, afin que les messages rejoués atterrissent sur la bonne partition et préservent l’ordre par clé.
  4. Limitez les tentatives de rejeu. Un message qui échoue à nouveau après un cycle de correction et de rejeu n’est pas transitoire ; routez-le vers une archive permanente (une table de base de données, un compartiment de stockage d’objets) au lieu de le faire boucler indéfiniment dans la file d’attente de messages morts. Les propres documents de RabbitMQ avertissent qu’un message mis en file d’attente de messages morts ne peut être routé entre les files qu’un nombre limité de fois (16) avant que la mise en file d’attente de messages morts basée sur le TTL ne soit désactivée.
  5. Ne laissez jamais une file d’attente de messages morts se mettre elle-même en file d’attente de messages morts. Si votre file d’attente de messages morts possède son propre x-dead-letter-exchange (RabbitMQ) ou sa propre politique de redémarrage (SQS) pointant vers la même chaîne, un échec de rejeu peut créer une boucle infinie. Gardez la configuration de mise en file d’attente de messages morts de la file d’attente de messages morts vide, ou pointez-la vers une archive strictement terminale.
  6. Alertez sur le volume, pas seulement sur la présence. Un seul message dans une file d’attente de messages morts est un point de données ; un pic soudain est un incident. Connectez la profondeur et l’âge des messages de la file d’attente de messages morts au même pipeline d’alerte que vous utilisez pour tout le reste — consultez Conception des systèmes d’alerte modernes pour les équipes d’observabilité pour les pratiques de routage et de réduction de bruit qui s’appliquent directement aux alertes de file d’attente de messages morts.

Si votre flux de travail implique des processus longs et multi-étapes plutôt que des messages uniques, la même réflexion sur les files d’attente de messages morts s’applique au niveau du workflow — la logique de compensation d’une saga a besoin de la même discipline « mettre en quarantaine, inspecter, décider » lorsqu’une étape échoue définitivement au lieu d’être transitoire. Et lorsque les événements eux-mêmes proviennent d’une écriture de base de données, le modèle de boîte de sortie transactionnelle intègre déjà la gestion des files d’attente de messages morts dans le travailleur de relais, de sorte que le modèle apparaît une couche plus tôt que le courtier.

Où s’insèrent les files d’attente de messages morts dans le tableau général

Une file d’attente de messages morts ne fait pas disparaître les échecs — elle les rend survivables et inspectables plutôt que silencieux. Elle fonctionne au mieux aux côtés des renvois avec remultiplication pour le cas transitoire, des consommateurs idempotents afin que la redirection soit sûre, et d’un disjoncteur pour qu’une dépendance en difficulté n’inonde pas la file principale (et, éventuellement, la file d’attente de messages morts) avec la même erreur des milliers de fois. Traitez le seuil, la rétention et l’alerte de la file d’attente de messages morts comme des décisions de configuration de première classe, et non comme des valeurs par défaut que vous laissez inchangées, et les messages morts deviennent un outil de diagnostic plutôt qu’un endroit où les données disparaissent silencieusement.

Liens utiles

S'abonner

Recevez de nouveaux articles sur les systèmes, l'infrastructure et l'ingénierie IA.