Meilleures pratiques pour la gestion de la mémoire RabbitMQ et le débit élevé

Réglez la mémoire, les limites de disque, les files d'attente et les consommateurs RabbitMQ pour qu'un débit élevé ne se transforme pas en pression sur le courtier.

Meilleures pratiques pour la gestion de la mémoire RabbitMQ et le débit élevé

RabbitMQ peut déplacer beaucoup de messages, mais il n'est pas content lorsque la mémoire devient le plan de débordement. Le courtier a besoin de mémoire pour les connexions, les canaux, les processus de file d'attente, les métadonnées des messages, les livraisons non acquittées, les plugins, les métriques et le runtime Erlang lui-même. Si les éditeurs sont plus rapides que les consommateurs pendant assez longtemps, la question cesse d'être « À quelle vitesse RabbitMQ peut-il aller ? » et devient « Où la pression se manifestera-t-elle en premier ? »

Une bonne gestion de la mémoire consiste principalement à garder cette pression visible et contrôlée. Vous voulez que RabbitMQ applique une contre-pression avant que le système d'exploitation ne commence à tuer des processus. Vous voulez également suffisamment de marge sur le disque pour que les messages persistants et l'état interne puissent être écrits en toute sécurité.

Commencez par l'alarme mémoire, mais ne la traitez pas comme une solution magique de réglage

RabbitMQ utilise vm_memory_high_watermark pour décider quand l'utilisation de la mémoire est trop élevée. Lorsque le seuil est franchi, RabbitMQ déclenche une alarme mémoire et bloque les éditeurs jusqu'à ce que la mémoire baisse. Ce comportement est intentionnel. Un éditeur bloqué est ennuyeux ; un courtier à court de mémoire est pire.

Un point de départ courant est un seuil relatif d'environ 40 % de la mémoire disponible :

vm_memory_high_watermark.relative = 0.40

Ce nombre n'est pas sacré. Une petite machine virtuelle avec d'autres services peut avoir besoin d'un seuil plus bas. Un courtier dédié avec des charges de travail bien comprises peut tolérer une valeur différente. Le but est de laisser de la place pour le cache de pages du système d'exploitation, l'activité du système de fichiers, les agents de surveillance et les pics qui se produisent avant que vos graphiques ne rattrapent leur retard.

Vous pouvez également définir une valeur absolue, ce qui est souvent plus facile dans les conteneurs ou les environnements où la « mémoire disponible » peut être mal comprise :

vm_memory_high_watermark.absolute = 6GiB

Utilisez un style qui correspond au déploiement du nœud. Dans les conteneurs, vérifiez que RabbitMQ voit la limite de conteneur que vous attendez, et non la mémoire totale de l'hôte. Un seuil basé sur un total de mémoire erroné est un moyen silencieux de créer un incident de production.

La limite d'espace disque libre est l'autre barrière de sécurité

Le paramètre de protection du disque de RabbitMQ est disk_free_limit. Il est basé sur l'espace libre, et non sur un pourcentage disk_high_watermark. Lorsque l'espace disque libre tombe en dessous de la limite configurée, RabbitMQ déclenche une alarme disque et bloque les éditeurs.

Pour de nombreux nœuds de production, une limite absolue est plus claire qu'une limite relative :

disk_free_limit.absolute = 20GB

La bonne valeur dépend de la taille des messages, du taux de publication, de la persistance, de la rotation des journaux et de la rapidité avec laquelle votre équipe peut ajouter de l'espace ou vider les files d'attente. Un nœud recevant de gros messages persistants a besoin d'une marge beaucoup plus grande qu'un nœud traitant de minuscules événements transitoires.

Ne définissez pas cela à une valeur minuscule simplement pour éviter les alarmes. Les alarmes disque sont là pour protéger le courtier. Si un disque atteint zéro octet libre, vous pouvez vous retrouver avec des écritures échouées, une disponibilité endommagée et une récupération beaucoup plus compliquée.

Comprenez ce qui utilise réellement la mémoire

Lorsque la mémoire augmente, évitez de deviner. RabbitMQ expose une répartition de la mémoire via l'interface de gestion et la CLI :

rabbitmq-diagnostics memory_breakdown
rabbitmqctl status
rabbitmqctl list_queues name type messages_ready messages_unacknowledged memory

La première division la plus utile est entre les messages prêts et les messages non acquittés. Les messages prêts attendent encore dans la file d'attente. Les messages non acquittés ont été livrés aux consommateurs et attendent basic.ack, basic.nack ou la fermeture du canal.

Si les messages prêts augmentent, les producteurs dépassent les consommateurs ou les consommateurs ne sont pas connectés. Si les messages non acquittés augmentent, les consommateurs prennent des messages mais ne les terminent pas. Ce sont des problèmes différents. Augmenter les limites de mémoire ne fera que gagner du temps si le déséquilibre de flux persiste.

Les messages volumineux méritent une attention particulière. Une file d'attente avec un nombre modeste de messages peut toujours consommer beaucoup de mémoire si chaque message transporte une charge utile importante. Si les messages contiennent des images, des documents ou de gros blobs JSON, envisagez de stocker la charge utile ailleurs et d'envoyer une référence via RabbitMQ. Les courtiers de messages sont généralement meilleurs pour déplacer des notifications de travail que pour agir comme des magasins de blobs.

Réglez la pré-extraction pour arrêter les arriérés cachés

La pré-extraction contrôle le nombre de messages non acquittés que RabbitMQ peut livrer à un consommateur. Une valeur de pré-extraction élevée peut améliorer le débit pour les consommateurs rapides, mais elle déplace également l'arriéré de la file d'attente vers la mémoire du consommateur.

Par exemple, dix consommateurs avec prefetch_count=500 peuvent contenir jusqu'à 5 000 messages non acquittés en dehors de la file d'attente prête. Si chaque message est volumineux ou lent à traiter, cela peut créer une pression mémoire et une latence inégale. Un nouveau message peut attendre derrière des centaines de messages plus anciens déjà assis à l'intérieur d'un consommateur lent.

Commencez avec une valeur de pré-extraction qui correspond au travail. Pour les appels API lents ou les écritures en base de données, essayez un petit nombre comme 5 ou 10 et n'augmentez qu'après avoir mesuré. Pour un travail CPU local très rapide, des valeurs plus élevées peuvent aider. Pour une équité stricte, prefetch_count=1 est parfois le bon compromis, même si le débit total est inférieur.

La clé est de mesurer le temps de traitement et le délai d'accusé de réception. RabbitMQ ne peut pas terminer les messages à votre place. Il peut seulement limiter la quantité de travail non terminé qu'il distribue.

Gardez les files d'attente courtes lorsque c'est possible

RabbitMQ fonctionne mieux lorsque les messages traversent le système plutôt que de rester dans les files d'attente pendant des heures. Une file d'attente qui est généralement proche de zéro et qui augmente occasionnellement est saine. Une file d'attente qui croît toute la journée et se vide pendant la nuit est un avertissement de capacité. Une file d'attente qui ne fait que croître est une panne au ralenti.

Pour les longs arriérés, décidez si l'arriéré est attendu. S'il est attendu, utilisez le type de file d'attente et la conception de stockage qui conviennent. Les files d'attente de quorum sont bonnes pour les charges de travail durables répliquées. Les flux peuvent convenir aux charges de travail de type rejeu. Les files d'attente classiques peuvent convenir pour un travail transitoire plus simple. Si l'arriéré n'est pas attendu, corrigez les consommateurs ou les services en aval avant de régler la mémoire du courtier.

Définissez des TTL de message uniquement lorsque le travail expiré est vraiment inutile. Un TTL ne remplace pas la capacité. Il peut protéger un système contre le traitement de messages obsolètes, mais il peut également masquer une perte de données s'il est appliqué avec désinvolture.

Les files d'attente de lettres mortes aident à séparer les messages empoisonnés du flux normal. Sans une stratégie de lettres mortes, une charge utile défectueuse peut être retentée indéfiniment, consommer des ressources et donner l'impression que la file d'attente est plus lente qu'elle ne l'est réellement.

La persistance modifie le budget de débit

Les files d'attente durables et les messages persistants sont le bon choix lorsque les messages doivent survivre à un redémarrage du courtier. Ils nécessitent également des écritures sur disque. Les confirmations d'éditeur ajoutent un signal de fiabilité afin que les éditeurs sachent quand le courtier a accepté la responsabilité d'un message.

Le schéma lent consiste à publier un message persistant, à attendre de manière synchrone sa confirmation, puis à publier le suivant. C'est simple et sûr, mais le débit sera limité par le temps d'aller-retour et le comportement du disque. Un meilleur schéma consiste à utiliser des confirmations d'éditeur asynchrones ou de petits lots, tout en gérant les accusés de réception négatifs et les délais d'attente.

Évitez les transactions AMQP pour la publication à haut débit, sauf si vous avez une raison très spécifique. Les confirmations d'éditeur sont l'outil de fiabilité habituel pour les éditeurs RabbitMQ.

Donnez à RabbitMQ une infrastructure prévisible

RabbitMQ aime les machines prévisibles : suffisamment de mémoire, des disques rapides pour les charges de travail persistantes, une latence réseau stable et aucun voisin bruyant ne volant le CPU. Si le courtier partage un hôte avec une base de données, un processeur de journaux et des tâches cron aléatoires, le réglage de la mémoire devient une conjecture.

Utilisez un stockage SSD ou NVMe pour les files d'attente persistantes à haut débit. Surveillez la latence du disque, pas seulement l'utilisation du disque. Un disque peut montrer un débit modéré et avoir toujours une latence d'écriture douloureuse. Dans les environnements cloud, les IOPS provisionnés et les crédits de rafale peuvent être plus importants que l'étiquette du disque.

Limitez le renouvellement des connexions. Les connexions et les canaux de longue durée sont moins chers que d'en ouvrir de nouveaux pour chaque publication. Si une application crée des milliers de connexions de courte durée, l'utilisation de la mémoire et des descripteurs de fichier peut augmenter même lorsque les taux de messages sont ordinaires.

Les conteneurs nécessitent une réflexion explicite

RabbitMQ fonctionne bien dans les conteneurs, mais les limites de mémoire doivent être claires. Le seuil de mémoire du courtier n'est utile que s'il est calculé par rapport à la limite que le conteneur peut réellement utiliser. Si RabbitMQ pense qu'il a la mémoire de l'hôte mais que l'environnement d'exécution du conteneur applique une limite plus petite, le conteneur peut être tué avant que le propre comportement d'alarme de RabbitMQ ne le protège.

Définissez une limite de mémoire de conteneur, puis définissez un seuil RabbitMQ absolu qui laisse de la place à l'intérieur de cette limite :

vm_memory_high_watermark.absolute = 3GiB

Par exemple, sur un conteneur limité à 4 Go, un seuil de courtier de 3 Go peut être raisonnable pour un pod dédié, tandis qu'une valeur plus faible peut être meilleure si des side-cars ou des plugins utilisent une mémoire significative. Ne copiez pas ce nombre aveuglément. Le but est de rendre la relation explicite.

Les données persistantes ont également besoin d'un stockage persistant. Si un redémarrage de conteneur perd le répertoire de données RabbitMQ, les files d'attente durables et les messages persistants ne vous sauveront pas. Utilisez des volumes appropriés, comprenez votre classe de stockage et testez un redémarrage du courtier avant de faire confiance à la configuration.

Files d'attente paresseuses, files d'attente de quorum et attentes en matière de mémoire

Les conseils plus anciens sur RabbitMQ disent souvent « utilisez des files d'attente paresseuses pour les longs arriérés ». Ce conseil a besoin de contexte. Les files d'attente paresseuses classiques ont été conçues pour garder plus de messages sur le disque et réduire la pression mémoire pour les longues files d'attente. Elles peuvent encore être utiles pour les charges de travail de files d'attente classiques où de longs arriérés sont attendus.

Les files d'attente de quorum se comportent différemment et sont couramment utilisées pour les charges de travail durables répliquées. Elles peuvent gérer les arriérés, mais elles répliquent également les données et ont leur propre profil de mémoire et de disque. Une file d'attente de quorum est d'abord un choix de fiabilité. Ce n'est pas un raccourci pour un arriéré illimité.

Si l'entreprise s'attend à ce que les messages restent pendant des jours et soient rejoués par de nombreux consommateurs, un flux ou un autre système de type journal peut mieux convenir qu'une file d'attente de travail normale. RabbitMQ est excellent pour répartir le travail. C'est moins agréable lorsqu'il devient la seule couche de stockage à long terme pour de grandes charges utiles historiques.

Séparez les symptômes du courtier des symptômes de la charge de travail

Une alarme mémoire vous indique que RabbitMQ est sous pression. Elle ne vous dit pas si RabbitMQ est la cause profonde. Une API de facturation lente peut amener les consommateurs à cesser d'accuser réception, ce qui fait augmenter les messages non acquittés, ce qui augmente la mémoire du courtier, ce qui bloque les éditeurs. L'alarme du courtier est réelle, mais la première correction peut être en dehors du courtier.

Lors d'une révision, représentez graphiquement ensemble le taux de publication, le taux de livraison, le taux d'accusé de réception, les messages prêts, les messages non acquittés, la mémoire, l'espace disque libre et le temps de traitement du consommateur. L'ordre du mouvement est important. Si le taux d'accusé de réception chute avant que la mémoire n'augmente, regardez les consommateurs. Si la latence du disque augmente avant que les confirmations ne ralentissent, regardez le stockage. Si le taux de publication double après un lancement de produit, regardez la capacité et la contre-pression.

C'est aussi pourquoi les tests de charge doivent inclure les consommateurs et les dépendances en aval. Un benchmark de publication uniquement prouve très peu de choses sur un flux de travail réel. Le courtier peut accepter rapidement des messages pendant un certain temps, mais le système ne fonctionne que si les consommateurs les terminent au rythme requis.

Rendez la contre-pression visible pour les équipes d'application

Le blocage des éditeurs ne doit pas être invisible. Les applications doivent enregistrer les événements de connexion bloqués et débloqués lorsque la bibliothèque cliente les expose, et les éditeurs doivent avoir des délais d'attente autour des chemins de publication qui alimentent les requêtes orientées utilisateur.

Sans cette visibilité, une alarme mémoire devient une plainte vague du type « l'application est lente ». Avec elle, l'équipe peut voir que RabbitMQ a appliqué une contre-pression à un moment précis, puis comparer cet horodatage avec la profondeur de la file d'attente, les erreurs du consommateur, la latence du disque et les événements de déploiement.

Ce que je vérifie lors d'une révision de débit élevé

Je commence par ces questions :

  • Les alarmes mémoire ou disque se déclenchent-elles ?
  • Les messages sont-ils principalement prêts ou non acquittés ?
  • Quelles files d'attente utilisent le plus de mémoire ?
  • Les consommateurs suivent-ils le rythme de publication ?
  • Les confirmations d'éditeur sont-elles asynchrones ou bloquantes une par une ?
  • Les messages sont-ils plus volumineux qu'ils ne devraient l'être ?
  • La latence du disque augmente-t-elle pendant les pics ?
  • Les connexions sont-elles stables ou se reconnectent-elles constamment ?

Ces réponses pointent généralement vers la correction. Parfois, la correction est un changement de configuration. Le plus souvent, il s'agit d'un changement de flux : des consommateurs plus rapides, une pré-extraction plus faible, des messages plus petits, un meilleur traitement par lots, un chemin de lettres mortes ou un type de file d'attente qui correspond à la charge de travail.

Un débit élevé n'est pas seulement un nombre plus grand sur un benchmark. C'est la capacité d'absorber les périodes d'activité sans perdre le contrôle de la mémoire, du disque et de la latence. RabbitMQ vous donne les barrières de sécurité, mais vous devez toujours maintenir le trafic en mouvement.