Le fichier d'échange manquant qui a figé nos serveurs
Le 24 septembre, l'un de nos serveurs de production a cessé de répondre. Le répartiteur de charge l'a marqué comme non fonctionnel, et notre outil de commandes à distance ne pouvait pas l'atteindre — mais rien n'avait planté et aucun processus n'avait été tué pour usage excessif de mémoire. Le serveur était en surcharge : sans espace d'échange et avec des conteneurs autorisés à utiliser plus de mémoire que la machine n'en avait, Linux passait son temps à évincer puis relire les fichiers nécessaires à son fonctionnement, et ne progressait presque plus. L'ajout d'un fichier d'échange de 2 Go sur chaque serveur a réglé le problème. La leçon la plus intéressante concernait les contrôles de santé.
À quoi ressemblait le « blocage »
- Les contrôles de santé du répartiteur de charge expiraient sur ce serveur.
- Les commandes envoyées via AWS Systems Manager restaient en « Pending » et ne s'exécutaient jamais.
- Aucune fin pour manque de mémoire n'apparaissait dans les journaux. Aucun processus n'était mort.
Une semaine plus tôt, nous avions déplacé la production vers des serveurs ARM plus petits disposant de 2 Go de RAM chacun. Notre serveur de préproduction — encore plus petit — s'était figé de la même manière pendant la décompression d'une image de conteneur lors d'un déploiement.
Pourquoi aucune erreur de mémoire ?
Linux ne tue un processus que lorsqu'il manque vraiment de mémoire récupérable. Avant ce point, il récupère de la mémoire dans le cache de pages — les copies de fichiers qu'il garde en RAM, y compris le code de chaque programme en cours d'exécution.
Sur nos serveurs, les limites de mémoire combinées des conteneurs totalisaient environ 3,3 Go sur une machine disposant d'environ 1,8 Go utilisables. Normalement, elles n'atteignent pas toutes leur limite en même temps. Lorsque c'est le cas, le noyau continue d'évincer le code des programmes du cache pour faire de la place, puis doit le relire depuis le disque dès que ce code s'exécute à nouveau. Ce cycle — l'emballement — peut rendre une machine presque totalement inerte sans qu'elle manque techniquement de mémoire. Donc rien n'est tué, et rien ne se rétablit.
Sans swap, le noyau n'a nulle part où placer la mémoire allouée mais rarement utilisée ; il doit donc la prendre dans le cache.
La correction
Le script d'amorçage des serveurs crée maintenant un fichier d'échange de 2 Go avec un réglage de swappiness faible (10) à chaque démarrage ou réparation d'un serveur. Une swappiness faible signifie que le noyau préfère toujours garder la mémoire de travail en RAM, mais que les pages rarement touchées peuvent passer sur le disque au lieu de forcer le code des programmes hors du cache. Nous l'avons appliqué en direct aux deux serveurs de production et à la préproduction, et le pipeline de déploiement republie le script d'amorçage à chaque déploiement en production, afin qu'un serveur remplacé l'obtienne aussi.
Le point qui nous préoccupait davantage : le serveur défaillant restait en service
Notre groupe de serveurs utilisait des contrôles de santé EC2 — « la machine tourne-t-elle ? » — et non des contrôles de santé du répartiteur de charge — « répond-elle aux requêtes ? ». Un serveur que le répartiteur avait abandonné était donc considéré comme sain par le groupe. Il ne serait jamais remplacé. La production aurait discrètement tourné sur un seul serveur jusqu'à ce que quelqu'un le remarque et redémarre l'autre.
Basculer le groupe sur les contrôles de santé du répartiteur semble la solution évidente, mais ce n'est pas simple pour nous : pendant un déploiement, les serveurs répondent délibérément une erreur sur leur point de contrôle de santé afin que le répartiteur les retire de la rotation (voir comment nous avons obtenu zéro 502 lors des déploiements). Un groupe qui remplace des serveurs sur ce signal les remplacerait en plein milieu de chaque déploiement.
Nous avons donc, pour l'instant, ajouté une étape à notre liste de vérification de déploiement : après chaque déploiement, vérifier la santé de chaque cible sur le répartiteur de charge, et pas seulement si la tâche de déploiement a réussi. Si les commandes envoyées à un serveur restent en « Pending », c'est qu'il est en surcharge — redémarrez-le ; il est déjà hors rotation.
Ce que nous en avons retenu
- « Ne pas planter » n'est pas « en santé ». La surcharge ne produit aucune erreur, seulement une lenteur si extrême que la machine cesse de répondre.
- Les petits serveurs ont besoin d'un swap. Même un peu, avec une swappiness faible, donne au noyau un endroit où placer la mémoire froide.
- Des limites de mémoire dont la somme dépasse celle de la machine, c'est un pari. Parfois, on le perd. Sachez quand vous l'avez fait.
- Sachez quel contrôle de santé votre mise à l'échelle automatique utilise. Un contrôle qui demande seulement « la VM est-elle allumée ? » ne remplacera jamais un serveur qui a cessé de fonctionner.