Le bug Node.js qui échappe à votre monitoring

Votre health check indique que tout va bien. Il prend une milliseconde. Puis le trafic augmente. Soudain, votre latence p99 bondit à 400 ms. Vous regardez vos tableaux de bord. Tout est au vert.

L'utilisation du CPU est modérée. L'event loop lag est stable. La mémoire est saine. Votre APM montre une requête lente mais ne vous dit pas pourquoi. Il n'y a pas d'appels de base de données lents. Il n'y a pas d'erreurs.

Le temps est consommé dans le thread pool de libuv.

L'observabilité standard de Node se concentre sur l'event loop. Le pool est une file d'attente distincte. Il échappe à votre contrôle.

Node exécute le JavaScript sur l'event loop. Il délègue les tâches lourdes au thread pool de libuv. Cela inclut :

  • Les opérations sur le système de fichiers (fs.readFile, fs.writeFile).
  • Les tâches de cryptographie (bcrypt, scrypt, pbkdf2).
  • La compression (zlib gzip, deflate).
  • Les recherches DNS (dns.lookup).

Par défaut, le pool ne dispose que de quatre threads. Cela est vrai, quel que soit le nombre de cœurs CPU de votre machine.

Quatre threads ne suffisent pas. Voici trois façons dont le pool peut saturer :

  1. Bcrypt lors de la connexion. Un seul hachage bcrypt peut prendre 250 ms. Si quatre personnes se connectent en même temps, tous les slots sont occupés. Une cinquième personne attend dans une file d'attente. Elle paie 250 ms juste pour commencer. Votre latence double.

  2. Opérations gzip volumineuses. La compression d'une réponse volumineuse occupe un slot du pool. Si quatre requêtes font cela simultanément, toutes les autres tâches attendent. Les recherches DNS et les lectures de fichiers se retrouvent bloquées dans la file.

  3. Recherches DNS. La plupart des applications Node utilisent dns.lookup. Cela utilise un appel système bloquant. Cela place la tâche sur le pool. Si votre réseau a un problème, ces recherches bloquent l'ensemble du pool.

Une requête bloquée dans la file d'attente du pool est invisible. Elle n'utilise pas le CPU. Elle n'exécute pas de JavaScript. Elle est simplement en attente.

Comment le trouver :

Si votre latence p99 augmente sous la charge mais que l'event loop lag reste stable, vérifiez le pool.

Le test le plus rapide : augmentez UV_THREADPOOL_SIZE. Réglez-le sur 64 dans votre environnement et redémarrez. Si la latence chute, vous avez trouvé le problème.

Comment le corriger correctement :

  • Utilisez worker_threads pour la cryptographie lourde comme bcrypt. Cela permet de les tenir à l'écart du pool libuv.
  • Utilisez dns.resolve au lieu de dns.lookup. C'est un véritable résolveur asynchrone.
  • Utilisez des streams pour les opérations zlib afin de libérer les slots plus rapidement.
  • Évitez les opérations lourdes sur le système de fichiers dans vos chemins de requête principaux.

Arrêtez de fixer des tableaux de bord au vert pendant que vos utilisateurs attendent.

Source : https://dev.to/r9v/the-nodejs-bug-thats-invisible-to-your-monitoring-oo8