Optimisation des performances de Node.js pour l'hébergement VPS en production

Optimisation des performances de Node.js pour l'hébergement VPS en production - Image principale

Les problèmes de performances de Node.js en production proviennent presque toujours des mêmes causes : une application à processus unique bloquée sur un seul cœur de processeur, l'absence de couches de mise en cache, une boucle d'événements bloquée par des tâches synchrones, et un gestionnaire de processus qui plante sans prévenir à 2 heures du matin.

Cet article traite des réglages qui permettent réellement d'améliorer la latence du p95 et la capacité de traitement des requêtes simultanées sur un VPS en production, en présentant des modèles de configuration qui font leurs preuves face à un trafic réel.

Pourquoi l'optimisation des performances de Node.js est importante sur un VPS en production

Un VPS t'offre un accès root, une allocation de vCPU dédiée et un contrôle permanent des processus, ce que l'hébergement mutualisé ne peut pas offrir, et c'est exactement ce dont a besoin un processus Node.js qui tourne en continu. La configuration par défaut de `server.js` exécute ton application sous la forme d'un seul processus sur un seul thread ; ainsi, un VPS à 4 vCPU exécutant une application non optimisée n'utilise qu'environ 25 % de la puissance matérielle pour laquelle tu paies. L'optimisation permet de combler cet écart.

L'autre raison d'optimiser au niveau de la couche VPS, c'est que Node.js est, par conception, monothread pour le code applicatif. Le runtime utilise une boucle d'événements et un pool de threads libuv pour gérer les E/S, mais toute tâche gourmande en ressources CPU que tu écris bloque quand même toutes les requêtes sur ce thread. L'optimisation en production consiste surtout à retirer les tâches gourmandes en ressources CPU de la boucle d'événements. Ça implique aussi de placer des couches moins gourmandes en ressources devant Node, pour que le runtime ne gère que ce qu'il doit gérer.

Quels sont les principaux goulots d'étranglement en matière de performances de Node.js ?

Les véritables problèmes de production se concentrent autour d'une poignée de causes profondes :

  • Déploiements à processus unique. Un processus One Node ne peut pas utiliser plus d'un cœur de processeur pour le code de l'application ; par conséquent, un VPS multicœur reste inactif en cas de charge importante.
  • Boucle d'événements bloquée. Les lectures de fichiers synchrones , l'analyse JSON.parse sur des charges utiles volumineuses, le hachage bcrypt sur le thread principal ou les expressions régulières sans limite bloquent chaque requête simultanée.
  • Les fuites de mémoire dues aux références conservées. Les fermetures à longue durée de vie , les caches en mémoire qui grossissent sans éviction et les écouteurs d'événements attachés sans nettoyage font discrètement grimper l'utilisation du tas au-delà du plafond par défaut de 1,5 Go.
  • Pas de mise en cache au niveau HTTP. Chaque requête passe par le code de l'application, même pour les réponses qui changent toutes les heures.
  • Exposition directe à Internet. Si tu fais tourner un nœud sur le port 80 ou 443 sans Nginx amont, c'est ton application qui doit se charger de la terminaison TLS, de la mise à disposition des fichiers statiques et de la mise en mémoire tampon pour les clients lents.
  • Les allers-retours vers la base de données sur le « hot path ». L'absence d'index et les requêtes N+1 apparaissent comme des problèmes de performance du nœud, même si le temps réel est consacré à attendre la base de données.

Pour savoir de quel cas il s'agit, il faut mesurer, pas deviner. Commence par vérifier le décalage de la boucle d'événements et l'utilisation du tas avant de modifier quoi que ce soit.

Comment définir le nombre adéquat de nœuds et de workers pour un cluster Node.js ?

Dans le modèle de cluster, un processus Node est exécuté par cœur de processeur, et un processus maître répartit les connexions entre les processus de travail. Le module cluster de Node.js est intégré au moteur d'exécution et constitue la base sur laquelle PM2 et la plupart des gestionnaires de processus s'appuient en arrière-plan.

La règle générale :

  • Charges de travail dépendantes du processeur ou équilibrées : nombre de workers = nombre de vCPU. Sur un VPS à 4 vCPU, lance 4 workers.
  • Tâches gourmandes en E/S : le rapport « workers = vCPU » reste le bon point de départ. En ajouter davantage n'apporte que rarement une amélioration, car le goulot d'étranglement se situe au niveau de la base de données ou de l'API externe, et non au niveau de Node.
  • Offres VPS à mémoire limitée : nombre de workers = floor(mémoire RAM disponible / taille du tas par worker). Si chaque worker occupe 400 Mo de mémoire, et qu'il te reste 2 Go de mémoire libre après le système d'exploitation, le nombre maximal de workers est de quatre, quel que soit le nombre de cœurs.

Avec PM2, tu configures ça de manière déclarative :

pm2 start app.js -i max –name api

Le -i max L'option `-c` lance un processus par cœur disponible. Indique un nombre précis, par exemple -i 4, lorsque tu souhaites laisser de la marge pour un processus de base de données ou de mise en cache sur le même VPS.

Quels paramètres de PM2 et du gestionnaire de processus permettent d'améliorer la stabilité ?

PM2 est le gestionnaire de processus de production le plus courant pour Node, mais ses paramètres par défaut ne correspondent pas à la configuration dont tu as besoin à grande échelle. Un système prêt pour la production ecosystem.config.js ça ressemble plutôt à ça :

module.exports = {
  apps: [{
    name: ‘api’,
    script: ‘./server.js’,
    instances: ‘max’,
    exec_mode: ‘cluster’,
    max_memory_restart: ‘500M’,
    node_args: ‘–max-old-space-size=460’,
    env_production: {
      NODE_ENV: ‘production’,
      PORT: 3000
    },
    error_file: ‘/var/log/pm2/api-err.log’,
    out_file: ‘/var/log/pm2/api-out.log’,
    merge_logs: true,
    time: true
  }]
};

Quelques détails importants pour la production :

  • max_memory_restart déclenche un redémarrage en douceur avant qu'un thread n'atteigne la limite de mémoire du tas V8 et ne soit tué par le mécanisme OOM du système d'exploitation. Régle-le entre 5 et 10 % en dessous --max-old-space-size.
  • exec_mode: cluster C'est ce qui permet en fait l'équilibrage de charge entre les workers. Le mode « fork » exécute des processus indépendants sans liaison de port partagée.
  • Rotation des fichiers journaux n'est pas activé par défaut. Installe pm2-logrotate et configure pm2 set pm2-logrotate:max_size 50M et pm2 set pm2-logrotate:retain 14 pour que les fichiers journaux ne saturent pas le disque lors d'un pic de trafic.
  • Persistance au démarrage. Courir pm2 startup systemd et pm2 save pour que les services redémarrent automatiquement après un redémarrage ou une mise à jour du noyau.

Pour des rechargements sans interruption lors des déploiements, utilise pm2 reload api plutôt que restart. La fonction « Reload » remplace les nœuds un par un tout en maintenant le cluster en ligne.

Comment configurer Nginx proxy inverse pour Node.js ?

Placer Nginx Node est le changement le plus significatif pour la plupart des déploiements en production. Nginx la terminaison TLS, Nginx la diffusion des ressources statiques, Nginx la compression gzip et Brotli, Nginx la mise en mémoire tampon des requêtes pour les clients lents et du multiplexage HTTP/2, ce qui permet à Node de se concentrer uniquement sur les tâches requises par le code de ton application.

Un bloc de serveur de production minimal :

uupstream node_api {
    server 127.0.0.1:3000;
    keepalive 64;
}
 
server {
    listen 443 ssl http2;
    server_name api.example.com;
 
    ssl_certificate     /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
 
    gzip on;
    gzip_types application/json text/css application/javascript;
 
    location /static/ {
        alias /var/www/api/public/;
        expires 30d;
        add_header Cache-Control “public, immutable”;
    }
 
    location / {
        proxy_pass http://node_api;
        proxy_http_version 1.1;
        proxy_set_header Connection “”;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 60s;
    }
}

Les deux détails que les développeurs oublient le plus souvent : la configuration proxy_http_version 1.1 plus le vide Connection L'en-tête permet de réutiliser la connexion à partir du pool de keepalive en amont, ce qui réduit considérablement la charge liée à la négociation TCP en cas de forte affluence. La gestion /static/ directement depuis Nginx un long Cache-Control Les en-têtes détournent également des milliers de requêtes par minute de tes workers Node pour des fichiers qu'ils n'auraient jamais dû traiter.

Quels paramètres de mémoire et de collecte des déchets faut-il régler ?

Node utilise V8 en arrière-plan, et la taille par défaut du tas de la vieille génération de V8 est d'environ 1,5 Go sur les systèmes 64 bits, quelle que soit la quantité de RAM dont dispose réellement le VPS. Sur un VPS de 4 Go exécutant quatre workers, ce paramètre par défaut laisse environ 10 Go de capacité théorique du tas que tu ne peux pas utiliser, car chaque worker s'auto-limite.

Le drapeau à activer est --max-old-space-size, exprimé en mégaoctets :

node –max-old-space-size=460 server.js

Conseils pour choisir la bonne taille :

  • Prévois environ 25 % de la mémoire vive totale pour le système d'exploitation, Nginx et toute base de données ou cache fonctionnant sur le même VPS.
  • Divise le reste par le nombre de workers, puis soustrais 10 % pour les frais généraux de V8. Sur un VPS de 2 Go avec 4 workers, ça donne environ 460 Mo par worker.
  • Match max_memory_restart dans PM2 à cette valeur ou légèrement en dessous. Un processus redémarré par PM2 peut être récupéré ; celui qui a été tué par le « OOM killer » du noyau ne peut pas l'être.

Pour les services à très haut débit, voici d'autres options qu'il vaut la peine de tester : --max-semi-space-size pour laisser plus de marge à la jeune génération (en réduisant la fréquence des GC mineurs sur les services qui allouent de manière agressive) et --no-compilation-cache si tu constates une pression sur la mémoire due au code compilé mis en cache dans des workers à courte durée de vie. Teste les modifications en conditions de charge avant de les déployer en production.

Comment analyser les performances d'une application Node.js lente ?

La plupart des efforts d'optimisation échouent parce que l'ingénieur a optimisé le mauvais élément. Commence par analyser les performances, puis modifie le code :

  • node --inspect server.js Avec Chrome DevTools, tu disposes d'un graphique en flamme représentant le temps CPU et d'un outil de capture d'instantané du tas pour repérer les objets retenus. L'onglet « Performance » de DevTools est le moyen le plus rapide d'identifier une boucle d'événements bloquée.
  • clinic doctor (clinicjs.org) exécute ton application en conditions de charge et génère un rapport de diagnostic. Il est particulièrement efficace pour signaler les retards dans la boucle d'événements et une pression excessive sur le ramasse-miettes avant que tu ne te lances dans une analyse plus approfondie.
  • autocannon C'est l'outil de génération de charge vers lequel se tournent la plupart des développeurs Node. Un test de performance de référence avant tout réglage te fournit le point de comparaison dont tu as besoin pour savoir si tes modifications ont été bénéfiques ou néfastes.
  • Surveillance du décalage de la boucle d'événements en production ça va dans ton APM ou dans un simple perf_hooks.monitorEventLoopDelay() exportateur vers Prometheus. Un temps de latence supérieur à 50 ms sous une charge constante indique qu'un processus synchrone bloque les workers.

Si un point de terminaison est lent, mesure le temps d'exécution de la requête de base de données séparément du gestionnaire. Le profileur Node mettra en évidence await pool.query(...) comme étant le goulot d'étranglement, mais le travail s'effectue dans PostgreSQL MySQL, pas dans ton code.

Quels sont les niveaux de mise en cache qui font la plus grande différence ?

La mise en cache est l'optimisation qui offre le meilleur retour sur investissement, mais que la plupart des équipes négligent. Trois niveaux sont importants pour les charges de travail en production sous Node.js :

  • Mise en cache au niveau de l'application avec Redis. Déplace le stockage des sessions, les compteurs de limitation de débit et les résultats des requêtes fréquemment consultés hors de la base de données vers Redis, sur le même VPS ou sur un serveur voisin du réseau privé. Un aller-retour vers Redis local prend moins d'une milliseconde ; la même requête sur PostgreSQL un cache peu utilisé peut prendre entre 20 et 80 ms.
  • Mise en cache des réponses HTTP avec Nginx. Pour les points de terminaison qui renvoient des réponses identiques pour la même URL, proxy_cache Dans Nginx traiter des milliers de requêtes par seconde à partir du disque sans jamais solliciter Node. Même une durée de mise en cache de 10 secondes sur un point de terminaison très fréquenté réduit considérablement la charge en amont.
  • Un CDN en amont de ton VPS. Cloudflare, Bunny ou n'importe quel CDN avec proxy inverse prend en charge le trafic des ressources statiques, termine la connexion TLS en périphérie et protège le serveur d'origine contre le trafic généré par les bots. Pour les utilisateurs répartis dans le monde entier, l'amélioration de la latence est généralement plus importante que n'importe quel réglage au niveau de l'application.

L'ordre dans lequel il faut les ajouter est celui indiqué ci-dessus. Commence par Redis, car cela modifie la structure de ton application. Passe ensuite à Nginx , car cela ne nécessite aucune modification du code, et termine par un CDN, car celui-ci apporte des avantages même à une application qui n'est pas optimisée.

Comment sécuriser un serveur VPS Node.js de production ?

Les performances et la sécurité sont plus étroitement liées que ne le pensent les développeurs, car il suffit d'un seul scan de botnet pour qu'une application exposée devienne indisponible. Renforcement de base pour un VPS Node.js :

  • Exécute Node en tant qu'utilisateur non root. Utilisation setcap 'cap_net_bind_service=+ep' $(which node) si tu dois t'attacher à des ports inférieurs à 1024 sans être root, ou faire en sorte que Nginx se charge de tout Nginx laisser Node écouter sur le port 3000.
  • Configure le pare-feu de l'hôte. UFW sur Ubuntu ou firewalld Sur AlmaLinux, le serveur est verrouillé et ne laisse passer que les ports que tu as délibérément ouverts, généralement les ports 22, 80 et 443.
  • Veille à ce que les dépendances soient toujours à jour. npm audit dans CI et Dependabot ou Renovate sur le dépôt pour détecter les CVE dans les dépendances transitives avant qu'elles n'atteignent l'environnement de production.
  • Définis les en-têtes de sécurité HTTP. Casque C'est le middleware Express standard pour les en-têtes comme Strict-Transport-Security, Content-Security-Policyet X-Frame-Options. Les en-têtes mal configurés font partie des problèmes les plus fréquemment relevés lors des audits de sécurité.
  • Fais tourner les secrets et utilise des variables d'environnement. Ne mets jamais les fichiers .env en version. Des outils comme Doppler, Vault ou même systemd EnvironmentFile= Ces directives empêchent le stockage des identifiants dans le référentiel.

Quand faut-il passer à une solution plus évolutive qu'un simple VPS ?

Une application Node.js bien optimisée sur un VPS de 4 à 8 vCPU équipé de Nginx Redis peut facilement traiter des millions de requêtes par jour. L'évolutivité horizontale s'avère généralement nécessaire pour l'une des trois raisons suivantes :

  • Une utilisation soutenue du processeur supérieure à 70 % sur tous les workers, même après avoir modifié le profilage et la mise en cache, indique que tu as dépassé les capacités de la solution.
  • Les accords de niveau de service (SLA) exigeant une disponibilité maximale, qui ne tolèrent aucune panne d'un seul serveur, nécessitent au moins deux instances VPS d'application derrière un équilibreur de charge.
  • La séparation des ressources avec gestion de l'état devient rentable lorsque ta base de données, ton cache et les charges de travail de ton application commencent à se disputer les mêmes ressources d'E/S disque ou de mémoire vive sur un VPS partagé.

Les offres Cloud VPS et Managed VPS d'InMotion incluent toutes deux un accès root complet, une allocation de vCPU dédiée et des distributions Linux telles qu'AlmaLinux 9, Ubuntu 22.04 LTS et Debian 12, qui répondent aux exigences d'exécution de toutes les versions LTS actuelles de Node.js. Le SLA garantissant une disponibilité de 99,99 % et l'accès 24 h/24 et 7 j/7 à l'équipe APS sont essentiels dès que ton application cesse d'être un projet secondaire et commence à générer des revenus.

Si tu exécutes une application Node.js en production sur un hébergement mutualisé ou sur un VPS qui n'a pas été optimisé au-delà des paramètres par défaut, les modifications décrites dans cet article devraient réduire de moitié la latence p95. Elles peuvent également doubler le débit de requêtes soutenable sans que tu aies à dépenser un centime de plus en infrastructure. Commence par le mode cluster de PM2 avec Nginx front-end, analyse les performances restantes et ajoute de la mise en cache là où les données le permettent.

Optimisation pratique des performances Node.js pour l'hébergement VPS en production, incluant la mise en cluster, PM2, le proxy Nginx , les paramètres de mémoire et les couches de mise en cache.Prêt à utiliser Node.js en production ?Les offres Cloud VPS et VPS géré d'InMotion t'offrent un accès root, une allocation de vCPU dédiée et le choix entre AlmaLinux 9, Ubuntu 22.04 LTS ou Debian 12. Avec une assistance humaine 24 h/24, 7 j/7 et un SLA garantissant une disponibilité de 99,99 %.

Comparer les plans VPS ?
Partager cet article

Laisser une réponse

Ton adresse e-mail ne sera pas publiée. Les champs obligatoires sont marqués *