Introduction
La planification de capacité pour Node.js consiste à estimer et provisionner les ressources CPU, mémoire et la concurrence dont votre application a besoin pour atteindre ses objectifs de performance sous une charge attendue. Contrairement aux serveurs multithreads traditionnels, Node.js exécute JavaScript sur une boucle d'événements monothread, ce qui crée des goulots d'étranglement et des modèles de mise à l'échelle uniques. Bien dimensionner les capacités évite les réponses lentes, les plantages et une infrastructure surprovisionnée.
Ce guide présente un flux de travail pratique, sensible aux versions, pour la planification de capacité Node.js. Vous apprendrez à inventorier votre environnement, dimensionner les ressources en toute sécurité, vérifier les performances, gérer les modes de défaillance et utiliser une liste de contrôle opérationnelle. Chaque section comprend des commandes concrètes, les sorties attendues et les étapes de récupération, avec des espaces réservés que vous pouvez adapter à votre pile.
Nous supposons Node.js 18 ou ultérieur sur Linux (Ubuntu 22.04 LTS dans les exemples) pour un comportement déterministe. Les commandes qui diffèrent selon les versions sont signalées.
Inventaire des versions et de l'environnement
Avant tout réglage de capacité, capturez votre version de Node.js, le système d'exploitation et la topologie de déploiement. Cette base de référence est essentielle pour reproduire les problèmes et savoir quels indicateurs et API sont disponibles.
Identifier la version installée et la plateforme
Exécutez les commandes en lecture seule suivantes :
node --version
# v18.19.0 (exemple)
npm --version
# 10.2.3
uname -a
# Linux app-server-01 5.15.0-91-generic #101-Ubuntu SMP ... x86_64 x86_64 x86_64 GNU/Linux
Vérifiez si votre binaire Node.js est une version de production ou de débogage, car les versions de débogage peuvent être beaucoup plus lentes :
node -p "process.release"
# { name: 'node', lts: 'Hydrogen', sourceUrl: '...', headersUrl: '...', libUrl: '...' }
Si vous utilisez un gestionnaire de processus comme systemd, PM2 ou Docker, enregistrez-le également. Exemple pour systemd :
systemctl show node-app -p Environment,ExecStart,MemoryMax,CPUQuota
# Environment= NODE_ENV=production PORT=3000
# ExecStart={ path=/usr/bin/node ; argv[]=/usr/bin/node /opt/app/server.js ; ... }
# MemoryMax=infinity
# CPUQuota=infinity
Prérequis
- Accès au serveur avec sudo ou des autorisations appropriées pour l'inspection au niveau du système.
- Node.js 18+ installé à partir des paquets officiels ou de nvm.
- Votre code d'application disponible localement ou sur le serveur.
- Un outil de test de charge plus tard (nous utiliserons
autocannonpour les benchmarks HTTP). - Des outils de surveillance tels que Prometheus et Grafana sont recommandés mais pas obligatoires.
Rayon d'impact et récupération pour l'inventaire
Les commandes d'inventaire sont en lecture seule et ne modifient pas l'état du système. Il n'y a pas de rayon d'impact. Si une commande est introuvable, installez le paquet manquant ou utilisez une alternative. Par exemple, uname fait partie de coreutils et devrait toujours exister. Aucune récupération n'est nécessaire, mais enregistrez la sortie dans votre runbook.
Chemin de configuration sûr
Dimensionner Node.js commence par comprendre la boucle d'événements, puis appliquer des limites correspondant à votre charge de travail. Un chemin sûr consiste à mesurer d'abord, définir des limites prudentes, puis ajuster un paramètre à la fois.
Bases de la boucle d'événements et du pool de threads
Node.js utilise une boucle d'événements monothread pour l'exécution de JavaScript. Les tâches intensives en CPU bloquent la boucle et retardent toutes les autres requêtes. Certaines opérations (fs, crypto, dns) utilisent un pool de threads libuv avec une taille par défaut de 4. Vous pouvez inspecter la taille du pool :
node -p "process.env.UV_THREADPOOL_SIZE || 'default (4)'"
Si elle est définie, la variable d'environnement apparaît. Pour la modifier, définissez-la avant de démarrer le processus :
UV_THREADPOOL_SIZE=8 node server.js
Rayon d'impact : affecte uniquement les opérations de système de fichiers, DNS et crypto. Augmentez uniquement si le profilage montre une saturation du pool de threads. Récupération : désactivez la variable ou remettez-la à 4.
Appliquer des limites de mémoire
Node.js a une taille maximale d'ancien espace par défaut d'environ 2 Go sur les systèmes 64 bits. Vous pouvez définir une limite avec --max-old-space-size (en Mo). Pour un serveur avec 4 Go de RAM exécutant uniquement Node, vous pouvez allouer 3 Go au tas :
node --max-old-space-size=3072 server.js
Vérifiez la limite dans l'application :
console.log(v8.getHeapStatistics().heap_size_limit);
// 3221225472 (3 Go)
Rayon d'impact : uniquement ce processus. Si la limite est trop basse, l'application plante avec JavaScript heap out of memory. Récupération : redémarrez avec une limite plus élevée ou corrigez les fuites de mémoire.
Définir une configuration spécifique à l'environnement
Utilisez des variables d'environnement pour les paramètres réglables comme le port, la taille du pool de base de données et les délais d'attente. Exemple de fichier .env (committé dans un gestionnaire de secrets, pas dans le contrôle de version) :
PORT=3000
DB_POOL_MAX=20
HTTP_TIMEOUT_MS=5000
Chargez et validez avec dotenv (ou le --env-file intégré de Node 20) :
node --env-file=.env server.js
Dans l'application :
const port = parseInt(process.env.PORT, 10);
if (!port || port < 1 || port > 65535) {
throw new Error(`Invalid PORT: ${process.env.PORT}`);
}
Rayon d'impact : affecte toutes les connexions entrantes. Récupération : annulez le changement de variable d'environnement et redémarrez.
Choisir un gestionnaire de processus
Pour la production, utilisez systemd, PM2 ou Docker pour gérer les redémarrages et les limites de ressources. Exemple d'unité systemd avec des limites :
[Service]
ExecStart=/usr/bin/node /opt/app/server.js
Restart=on-failure
MemoryMax=3G
CPUQuota=200%
Environment=NODE_ENV=production PORT=3000
Vérifiez après le rechargement :
sudo systemctl daemon-reload
sudo systemctl restart node-app
systemctl show node-app -p MemoryMax,CPUQuota
# MemoryMax=3221225472
# CPUQuota=200%
Rayon d'impact : service entier. Récupération : revenez au fichier d'unité précédent et redémarrez.
Vérification et diagnostics
La planification de capacité n'est pas complète sans test de charge pour vérifier que vos limites et votre topologie choisies gèrent le trafic attendu. Utilisez des charges de travail réalistes et mesurez la latence, le débit et l'utilisation des ressources.
Mesure de base
Tout d'abord, mesurez les performances de votre application sans limites artificielles. Utilisez autocannon pour tester en charge un point de terminaison HTTP simple. Installez-le globalement ou utilisez npx :
npm install -g autocannon
Démarrez votre serveur (par exemple, sur le port 3000). Ensuite, exécutez un test de base avec 100 connexions pendant 30 secondes :
autocannon -c 100 -d 30 http://localhost:3000/api/health
Exemple de sortie (tronquée) :
Running 30s test @ http://localhost:3000/api/health
100 connections
Stat Avg Stdev Max
Latency (ms) 12.34 5.67 45.67
Req/Sec 8234.56 123.45 8567.89
Bytes/Sec 2.1 MB
Requests per second: 8234.56
Enregistrez ceci comme votre référence. Si le point de terminaison est léger en CPU, vous pouvez voir des milliers de requêtes par seconde.
Tester avec un point de terminaison gourmand en CPU
Créez un point de terminaison temporaire qui effectue un calcul intensif en CPU (par exemple, Fibonacci) pour voir le blocage de la boucle d'événements :
app.get('/cpu', (req, res) => {
const n = 40;
const fib = (n) => (n < 2 ? n : fib(n - 1) + fib(n - 2));
res.json({ fib: fib(n) });
});
Exécutez le même test de charge. Vous verrez la latence p99 grimper à plusieurs secondes et le débit tomber à un chiffre. Cela démontre pourquoi le travail CPU doit être déchargé (worker threads, service séparé ou algorithme optimisé).
Mesurer l'utilisation des ressources
Pendant le test de charge, surveillez le CPU, la mémoire et le retard de la boucle d'événements. Utilisez pidstat (de sysstat) et clinic doctor ou node --prof.
Exemple pidstat :
pidstat -p $(pgrep -f 'node server.js') 1
# Linux ...
# 09:00:01 UID PID %usr %system %guest %CPU CPU minflt/s majflt/s VSZ RSS %MEM
# 09:00:02 1000 1234 45.0 5.0 0.0 50.0 0 0.0 0.0 700000 120000 3.0
Pour le retard de la boucle d'événements, utilisez clinic doctor :
npx clinic doctor --autocannon [ -c 100 -d 30 http://localhost:3000 ] -- node server.js
Cela produit un rapport HTML montrant l'utilisation de la boucle d'événements et les lacunes.
Vérifier la mise à l'échelle avec cluster
Si vous utilisez le module cluster, vérifiez que les workers sont équilibrés. Exemple de server.js avec cluster :
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').availableParallelism();
if (cluster.isPrimary) {
console.log(`Primary ${process.pid} is running`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`);
});
} else {
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world\n');
}).listen(3000);
console.log(`Worker ${process.pid} started`);
}
Exécutez le serveur et vérifiez les processus workers :
pgrep -af 'node server.js'
# 4567 node server.js (primary)
# 4568 /usr/bin/node server.js (worker 1)
# 4569 /usr/bin/node server.js (worker 2)
# ...
Testez à nouveau en charge ; vous devriez voir un débit total plus élevé et une latence plus faible sous forte charge par rapport à un processus unique.
Modes de défaillance et récupération
Comprendre comment Node.js échoue sous pression de ressources vous aide à concevoir des procédures de récupération.
Mémoire insuffisante (OOM)
Symptôme : Le processus se termine avec le code 134 ou 137, le journal affiche FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory.
Diagnostic : Consultez les journaux système (journalctl -u node-app -n 50) et utilisez des instantanés de tas avant le plantage si possible.
Récupération :
- Augmentez
--max-old-space-sizesi la limite est artificielle et que le serveur a de la RAM libre. - Corrigez la fuite de mémoire (par exemple, utilisez
--inspectet le profileur de tas Chrome DevTools). - Ajoutez plus de swap comme soulagement temporaire, mais ne vous y fiez pas.
- Redémarrez le service :
sudo systemctl restart node-app.
Blocage de la boucle d'événements
Symptôme : Latence élevée, les contrôles de santé expirent, mais l'utilisation du CPU peut être faible si bloqué sur les E/S.
Diagnostic : Utilisez clinic doctor ou surveillez le retard de la boucle d'événements via un échantillonnage process.hrtime() :
setInterval(() => {
const start = process.hrtime.bigint();
setImmediate(() => {
const lag = Number(process.hrtime.bigint() - start) / 1e6;
console.log(`Event loop lag: ${lag.toFixed(2)} ms`);
});
}, 1000);
Récupération :
- Déchargez les tâches CPU vers des worker threads ou une file d'attente de travaux.
- Optimisez le code bloquant (par exemple, remplacez les appels fs synchrones par des asynchrones).
- Mettez à l'échelle horizontalement avec cluster ou un équilibreur de charge.
Épuisement des ports
Symptôme : Les requêtes sortantes échouent avec EADDRNOTAVAIL ou EADDRINUSE lors de l'utilisation de nombreux services externes.
Diagnostic : Vérifiez le nombre de sockets en TIME_WAIT : ss -s.
Récupération :
- Réutilisez les connexions avec des agents keep-alive.
- Augmentez la plage de ports éphémères et réduisez TIME_WAIT (Linux :
net.ipv4.ip_local_port_rangeetnet.ipv4.tcp_tw_reuse). - Réduisez le renouvellement des connexions.
Disque plein à cause des journaux
Symptôme : L'application plante ou les écritures échouent. Vérifiez df -h.
Récupération :
- Faites pivoter les journaux avec logrotate ou une bibliothèque de journalisation.
- Déchargez les journaux vers un système central.
- Supprimez les anciens journaux :
sudo find /var/log/myapp -name '*.log' -mtime +7 -delete.
Liste de contrôle opérationnelle
Utilisez cette liste de contrôle pour les examens de capacité de routine et avant les changements majeurs. Remplacez les exemples entre crochets par vos valeurs réelles.
- [ ] Inventaire : Exécutez
node --version,uname -aet enregistrez les paramètres du gestionnaire de processus. Exemple de sortie stocké dans le runbook. - [ ] Test de charge de base : Exécutez
autocannon -c 100 -d 30 http://localhost:3000/healthet enregistrez la latence p95 et le RPS. Exemple : p95=25 ms, RPS=8000. - [ ] Vérifier la limite de mémoire : Confirmez que
v8.getHeapStatistics().heap_size_limitcorrespond à la valeur prévue. Exemple : 3221225472 octets. - [ ] Examiner le retard de la boucle d'événements : Assurez-vous que le retard reste inférieur à 50 ms en fonctionnement normal. Exemple : retard moyen 5 ms.
- [ ] Vérifier les workers du cluster : Pour une configuration multicœur, assurez-vous d'avoir un worker par CPU. Exemple : 8 workers sur une machine à 8 cœurs.
- [ ] Tester la récupération après défaillance : Simulez un OOM en démarrant avec une limite de mémoire basse et vérifiez que systemd redémarre. Exemple :
node --max-old-space-size=64 server.jsdevrait planter et redémarrer automatiquement. - [ ] Vérifier les alertes de surveillance : Assurez-vous que des alertes sont définies pour CPU > 80 %, mémoire > 80 %, retard de boucle d'événements > 100 ms et taux d'erreur > 1 %.
- [ ] Examiner les dépendances : Vérifiez que les tailles de pool de connexions de base de données et les délais d'attente correspondent à la capacité. Exemple : pool PostgreSQL max 20, délai d'attente 5 s.
Lorsqu'un contrôle échoue, documentez la défaillance et prenez des mesures correctives avant de procéder aux changements de capacité.
Conclusion
La planification de capacité Node.js est un processus itératif de mesure, de définition de limites et de vérification des performances sous charge. En suivant l'inventaire spécifique à la version, la configuration sûre, la vérification et les étapes de récupération de ce guide, vous pouvez éviter les pièges courants comme le blocage de la boucle d'événements et les plantages par manque de mémoire.
Commencez par une vérification à faible risque : exécutez un test de charge de base sur un environnement de préproduction, enregistrez les métriques et comparez-les à vos objectifs de production. Ensuite, appliquez progressivement des limites de ressources et des stratégies de mise à l'échelle, un changement à la fois, toujours avec un plan de retour en arrière. Intégrez la liste de contrôle opérationnelle dans votre processus de publication pour faire de la planification de capacité une discipline régulière plutôt qu'une réaction aux incidents.
Avec les bonnes commandes et les bons diagnostics, vous pouvez dimensionner en toute confiance votre application Node.js pour la demande actuelle et future.