...

Optimisation du pool de threads pour les serveurs Web : comparaison entre Apache, NGINX et LiteSpeed

Cet article montre comment la pool de threads serveur web Configuration pour Apache, NGINX et LiteSpeed Contrôle du parallélisme, de la latence et des besoins en mémoire. J'explique quels paramètres sont importants en cas de charge et dans quels cas l'auto-ajustement suffit, avec des différences claires au niveau des requêtes par seconde.

Points centraux

  • Architecture: Processus/threads (Apache) vs événements (NGINX/LiteSpeed)
  • Auto-réglage: L'ajustement automatique réduit la latence et les arrêts
  • Ressources: les cœurs CPU et la RAM déterminent la taille optimale des threads
  • Charge de travail: les tâches gourmandes en E/S nécessitent davantage de threads, celles gourmandes en CPU en nécessitent moins.
  • Tuning: Les petits paramètres ciblés ont plus d'effet que les valeurs forfaitaires.

Comparaison des architectures de pool de threads

Je commence par la Architecture, car elle définit les limites de l'espace de réglage. Apache s'appuie sur des processus ou des threads par connexion, ce qui coûte plus de RAM et augmente la latence aux heures de pointe [1]. NGINX et LiteSpeed suivent un modèle événementiel dans lequel quelques travailleurs multiplexent de nombreuses connexions, ce qui réduit les changements de contexte et diminue la charge [1]. Lors des tests, NGINX a traité 6 025,3 requêtes/s, Apache a atteint 826,5 requêtes/s dans le même scénario et LiteSpeed s'est hissé en tête avec 69 618,5 requêtes/s [1]. Si vous souhaitez approfondir la comparaison des architectures, vous trouverez d'autres données clés sous Apache contre NGINX, que j'utilise pour une première classification.

La manière dont chaque moteur traite les tâches bloquantes est également importante. NGINX et LiteSpeed dissocient la boucle d'événements du système de fichiers ou des E/S en amont via des interfaces asynchrones et des threads auxiliaires limités. Dans le modèle classique, Apache lie un thread/processus par connexion ; avec MPM event, Keep-Alive peut être déchargé, mais l'empreinte mémoire par connexion reste plus élevée. En pratique, cela signifie que plus il y a de clients lents simultanés ou de téléchargements volumineux, plus le modèle événementiel est rentable.

Comment fonctionne réellement l'auto-réglage

Les serveurs modernes contrôlent les fil de discussion-Le nombre est souvent automatique. Le contrôleur vérifie la charge en cycles courts, compare les valeurs actuelles aux valeurs historiques et augmente ou diminue la taille du pool [2]. Si une file d'attente est bloquée, l'algorithme raccourcit son cycle et ajoute des threads supplémentaires jusqu'à ce que le traitement redevienne stable [2]. Cela évite les interventions, empêche la surallocation et réduit le risque de blocages en tête de ligne. Je me réfère au comportement documenté d'un contrôleur auto-adaptatif dans Open Liberty, qui décrit clairement le mécanisme [2].

Je veille à trois leviers : une Hystérésis contre le flapping (pas de réaction immédiate à chaque pic), un limite supérieure stricte contre les dépassements de RAM et une taille minimale, afin que les coûts de préchauffage ne soient pas générés à chaque rafale. Il est également judicieux de définir une valeur cible distincte pour actif Threads (coreThreads) vs. threads maximaux (maxThreads). Cela permet au pool de rester actif sans monopoliser les ressources lorsqu'il est inactif [2]. Dans les environnements partagés, je réduis le taux d'expansion afin que le serveur Web ne monopolise pas de manière agressive les slots CPU par rapport aux services voisins [4].

Chiffres clés issus de benchmarks

Les valeurs réelles aident à Décisions. Dans les scénarios de rafales, NGINX se distingue par une très faible latence et une grande stabilité [3]. En cas de parallélisme extrême, Lighttpd affiche le nombre de requêtes par seconde le plus élevé lors des tests, suivi de près par OpenLiteSpeed et LiteSpeed [3]. NGINX réussit les transferts de fichiers volumineux avec jusqu'à 123,26 Mo/s, OpenLiteSpeed le suit de près, ce qui souligne l'efficacité de l'architecture événementielle [3]. J'utilise ces indicateurs pour évaluer où les ajustements de threads sont vraiment utiles et où les limites proviennent de l'architecture.

Serveur Modèle/Fils de discussion Exemple de taux message clé
Apache Processus/thread par connexion 826,5 requêtes/s [1] Flexible, mais besoins en RAM plus élevés
NGINX Événement + peu de travailleurs 6 025,3 requêtes/s [1] Faible Latence, économique
LiteSpeed Événement + LSAPI 69 618,5 requêtes/s [1] Très rapide, réglage de l'interface graphique
Lighttpd Événement + Asynchrone 28 308 requêtes/s (hautement parallèle) [3] Échelle en Pointes très bon

Le tableau indique les valeurs relatives. Avantages, pas d'engagements fermes. Je les évalue toujours dans le contexte de mes propres charges de travail : réponses dynamiques courtes, nombreux petits fichiers statiques ou flux volumineux. Les écarts peuvent provenir du réseau, du stockage, du déchargement TLS ou de la configuration PHP. C'est pourquoi je corrèle des métriques telles que le vol de CPU, la longueur de la file d'attente d'exécution et le RSS par travailleur avec le nombre de threads. Seule cette approche permet de distinguer les véritables goulots d'étranglement des threads des limites d'E/S ou d'application.

Pour obtenir des chiffres fiables, j'utilise des phases de montée en puissance et je compare les latences p50/p95/p99. Une courbe p99 raide avec des valeurs p50 constantes, cela indique plutôt des files d'attente qu'une saturation pure du processeur. Les profils de charge ouverts (contrôlés par RPS) plutôt que fermés (contrôlés uniquement par la concurrence) montrent également mieux où le système commence à rejeter activement les requêtes. Je peux ainsi définir le point où les augmentations de threads ne sont plus utiles et où la contre-pression ou les limites de débit sont plus appropriées.

Pratique : dimensionner les travailleurs et les connexions

Je commence par les CPU-Cœurs : les worker_processes ou LSWS-Worker ne doivent pas dépasser les cœurs, sinon le changement de contexte augmente. Pour NGINX, j'ajuste worker_connections de manière à ce que la somme des connexions et des descripteurs de fichiers reste inférieure à ulimit-n. Pour Apache, j'évite les MaxRequestWorkers trop élevés, car le RSS par enfant consomme rapidement la RAM. Sous LiteSpeed, je maintiens l'équilibre entre les pools de processus PHP et les travailleurs HTTP afin que PHP ne devienne pas un goulot d'étranglement. Si vous souhaitez comprendre les différences de vitesse entre les moteurs, vous tirerez profit de la comparaison suivante LiteSpeed vs. Apache, que j'utilise comme arrière-plan de tuning.

Une règle empirique simple : je calcule d'abord le budget FD (ulimit-n moins la réserve pour les journaux, les flux ascendants et les fichiers), je le divise par le nombre de connexions simultanées prévues par travailleur et je vérifie si le total est suffisant pour HTTP + flux ascendant + tampon TLS. Ensuite, je dimensionne modérément la file d'attente du backlog – suffisamment grande pour les pics, suffisamment petite pour ne pas masquer la surcharge. Enfin, je règle les valeurs Keep-Alive de manière à ce qu'elles correspondent aux modèles de requêtes : les pages courtes avec de nombreux actifs bénéficient de délais d'expiration plus longs, tandis que le trafic API avec peu de requêtes par connexion bénéficie plutôt de valeurs plus faibles.

Réglage fin LiteSpeed pour charge élevée

Avec LiteSpeed, je mise sur LSAPI, car cela minimise les changements de contexte. Dès que je remarque que les processus CHILD sont saturés, j'augmente progressivement LSAPI_CHILDREN de 10 à 40, voire jusqu'à 100 si nécessaire, tout en effectuant des contrôles CPU et RAM [6]. L'interface graphique me facilite la création de listeners, les partages de ports, les redirections et la lecture de .htaccess, ce qui accélère les modifications [1]. Sous une charge continue, je teste l'effet de petites étapes plutôt que de grands sauts afin de détecter rapidement les pics de latence. Dans les environnements partagés, je réduis les coreThreads lorsque d'autres services sollicitent le CPU, afin que le Self-Tuner ne conserve pas trop de threads actifs [2][4].

De plus, j'observe Keep-Alive par écouteur et l'utilisation de HTTP/2/HTTP/3 : le multiplexage réduit le nombre de connexions, mais augmente les besoins en mémoire par socket. Je garde donc les tampons d'envoi conservateurs et n'active la compression que lorsque le gain net est évident (nombreuses réponses textuelles, limite CPU quasi inexistante). Pour les fichiers statiques volumineux, je m'appuie sur des mécanismes zero-copy et limite les slots de téléchargement simultanés afin que les workers PHP ne soient pas saturés en cas de pics de trafic.

NGINX : utiliser efficacement le modèle événementiel

Pour NGINX, je définis worker_processes sur voiture ou le nombre de cœurs. Avec epoll/kqueue, accept_mutex actif et des valeurs de backlog adaptées, je maintiens les acceptations de connexion à un niveau constant. Je veille à définir keepalive_requests et keepalive_timeout de manière à ce que les sockets inactifs n'encombrent pas le pool FD. Je transfère les fichiers statiques volumineux avec sendfile, tcp_nopush et un output_buffers adapté. Je n'utilise la limitation de débit et les limites de connexion que lorsque des bots ou des rafales sollicitent indirectement le pool de threads, car chaque limitation génère une gestion d'état supplémentaire.

Dans les scénarios proxy, Keepalive en amont Décisif : trop bas génère une latence d'établissement de connexion, trop haut bloque les FD. Je choisis des valeurs adaptées à la capacité du backend et sépare clairement les délais d'attente pour connect/read/send afin que les backends défectueux ne bloquent pas les boucles d'événements. Avec reuseport et l'affinité CPU en option, je répartis la charge de manière plus uniforme entre les cœurs, tant que les paramètres IRQ/RSS de la carte réseau le permettent. Pour HTTP/2/3, je calibre soigneusement les limites d'en-tête et de contrôle de flux afin que les flux individuels volumineux ne dominent pas l'ensemble de la connexion.

Apache : configurer correctement MPM event

Avec Apache, j'utilise événement au lieu de prefork, afin que les sessions Keep-Alive ne monopolisent pas les workers en permanence. Je règle MinSpareThreads et MaxRequestWorkers de manière à ce que la file d'attente d'exécution reste inférieure à 1 par cœur. Je garde la ThreadStackSize petite afin que davantage de workers puissent tenir dans la RAM disponible ; elle ne doit toutefois pas être trop petite, sinon vous risquez des débordements de pile dans les modules. Avec un KeepAlive-Timeout modéré et des KeepAliveRequests limitées, j'empêche que quelques clients bloquent de nombreux threads. Je transfère PHP vers PHP-FPM ou LSAPI afin que le serveur web reste léger.

Je fais également attention au rapport entre ServerLimit, ThreadsPerChild et MaxRequestWorkers : ces trois éléments déterminent ensemble le nombre de threads qui peuvent réellement être créés. Pour HTTP/2, j'utilise MPM event avec des limites de flux modérées ; des valeurs trop élevées augmentent la consommation de RAM et les coûts de planification. Je ne charge les modules avec de grands caches globaux que lorsqu'ils sont nécessaires, car les avantages du copy-on-write disparaissent dès que les processus s'exécutent pendant longtemps et modifient la mémoire.

RAM et threads : calculer correctement la mémoire

Je compte le RSS par travailleur/enfant fois le nombre maximal prévu et j'ajoute la mémoire tampon du noyau et les caches. S'il ne reste plus de mémoire tampon, je réduis les threads ou n'augmente jamais le swap, car le swap fait exploser la latence. Pour PHP-FPM ou LSAPI, je calcule en outre le PHP-RSS moyen afin que la somme du serveur web et du SAPI reste stable. Je tiens compte des coûts de terminaison TLS, car les handshakes de certificats et les grands tampons sortants augmentent la consommation. Ce n'est que lorsque la gestion de la RAM est cohérente que je continue à resserrer les vis des threads.

Avec HTTP/2/3, je prends en compte les états supplémentaires d'en-tête/contrôle de flux par connexion. GZIP/Brotli met en mémoire tampon les données compressées et non compressées simultanément, ce qui peut représenter plusieurs centaines de Ko supplémentaires par requête. Je prévois également des réserves pour les journaux et les fichiers temporaires. Avec Apache, des valeurs ThreadStackSize plus petites augmentent la densité, tandis qu'avec NGINX et LiteSpeed, ce sont principalement le nombre de sockets parallèles et la taille des tampons d'envoi/réception qui ont un impact. Additionner tous les composants avant le réglage permet d'éviter les mauvaises surprises par la suite.

Quand intervenir manuellement

Je compte sur Auto-réglage, jusqu'à ce que les métriques indiquent le contraire. Si je partage la machine en hébergement mutualisé, je ralentis coreThreads ou MaxThreads afin que les autres processus conservent suffisamment de temps CPU [2][4]. S'il existe une limite stricte de threads par processus, je définis maxThreads de manière conservatrice afin d'éviter les erreurs du système d'exploitation [2]. Si des schémas de type deadlock apparaissent, j'augmente la taille du pool à court terme, j'observe les files d'attente, puis je la réduis à nouveau. Si vous souhaitez comparer des schémas typiques avec des valeurs mesurées, vous trouverez des indications dans le Comparaison de la vitesse des serveurs web, que j'aime utiliser comme contrôle de plausibilité.

J'utilise principalement les signaux d'intervention suivants : pics p99 persistants malgré une faible charge CPU, files d'attente de sockets en augmentation, forte croissance TIME_WAITou une augmentation soudaine des FD ouverts. Dans de tels cas, je réduis d'abord les hypothèses (limites de connexion/taux), je découple les backends avec des délais d'expiration, puis j'augmente prudemment les threads. Cela m'évite de simplement déplacer la surcharge vers l'intérieur et d'aggraver la latence pour tout le monde.

Erreurs courantes et vérifications rapides

Je regarde souvent élevé Les délais d'expiration Keep-Alive qui lient les threads même lorsqu'aucune donnée ne circule. Également courant : MaxRequestWorkers bien au-delà du budget RAM et ulimit-n trop faible pour le parallélisme cible. Dans NGINX, beaucoup sous-estiment l'utilisation FD par les connexions en amont ; chaque backend compte double. Dans LiteSpeed, les pools PHP croissent plus rapidement que les travailleurs HTTP, ce qui signifie que les requêtes sont acceptées, mais traitées trop tard. Grâce à des tests de charge courts, à une comparaison heap/RSS et à un aperçu de la file d'attente d'exécution, je trouve ces modèles en quelques minutes.

Autre problème fréquent : un syn-backlog trop petit, ce qui fait que les connexions rebondissent avant même d'atteindre le serveur web ; des journaux d'accès sans tampon qui s'écrivent de manière synchrone sur un stockage lent ; des journaux de débogage/traçage qui restent actifs par inadvertance et mobilisent le CPU. Lors du passage à HTTP/2/3, des limites de flux et des tampons d'en-tête trop généreux augmentent la consommation de mémoire par connexion, ce qui est particulièrement visible lorsque de nombreux clients transfèrent peu de données. Je vérifie donc la répartition des réponses courtes et longues et ajuste les limites en conséquence.

HTTP/2 et HTTP/3 : ce que cela signifie pour les pools de threads

Le multiplexage réduit considérablement le nombre de connexions TCP par client. C'est une bonne chose pour les FD et les coûts d'acceptation, mais cela déplace la pression sur les états par connexion. Je définis donc des limites prudentes pour les flux simultanés pour HTTP/2 et je calibre le contrôle de flux afin que les téléchargements individuels volumineux ne dominent pas la connexion. Avec HTTP/3, les blocages de tête de ligne liés au TCP sont supprimés, mais la charge CPU par paquet augmente. Je compense cela avec une capacité de travail suffisante et de petites tailles de tampon afin que la latence reste faible. Dans tous les cas, il vaut mieux avoir moins de connexions bien utilisées avec des valeurs Keep-Alive raisonnables que des sessions inactives trop longues qui occupent des threads et de la mémoire.

Facteurs liés à la plateforme : noyau, conteneurs et NUMA

En matière de virtualisation, je fais attention à CPU-Steal et limites cgroups : si l'hyperviseur vole des cœurs ou si le conteneur ne dispose que de cœurs partiels, worker_processes=auto peut être trop optimiste. Si nécessaire, j'épingle les workers à des cœurs réels et j'ajuste le nombre en fonction du efficacement budget disponible. Sur les hôtes NUMA, les serveurs web bénéficient d'une allocation de mémoire locale ; j'évite les accès inter-nœuds inutiles en regroupant les workers par socket. Je laisse souvent les pages transparentes géantes désactivées pour les charges de travail critiques en termes de latence afin d'éviter les pics de défauts de page.

Au niveau du système d'exploitation, je contrôle les limites des descripteurs de fichiers, les backlogs de connexion et la plage de ports pour les connexions sortantes. Je n'augmente que ce dont j'ai réellement besoin, je teste le comportement lors du rollover et je respecte strictement les limites de sécurité. Côté réseau, je m'assure que la répartition RSS/IRQ et les paramètres MTU correspondent au profil de trafic, sinon le réglage du serveur web est inutile, car les paquets arrivent trop lentement ou restent bloqués dans la file d'attente NIC.

Mesurer plutôt que deviner : guide pratique pour les tests

Je réalise des tests de charge en trois étapes : échauffement (caches, JIT, sessions TLS), plateau (RPS/concurrence stables) et rafale (pics courts). Des profils distincts pour les fichiers statiques, les appels API et les pages dynamiques permettent d'identifier de manière isolée les limites des threads, des E/S ou des backends. Je note en parallèle les chiffres FD, les files d'attente d'exécution, les changements de contexte, le RSS par processus et les latences p50/p95/p99. Je choisis comme objectif des points de fonctionnement à 70-85 % d'utilisation %, ce qui laisse une marge suffisante pour les fluctuations réelles sans fonctionner en permanence dans la zone de saturation.

Guide décisionnel en bref

Je choisis NGINX, lorsque la latence faible, les ressources économiques et les possibilités de réglage flexibles .conf comptent. Je mise sur LiteSpeed lorsque la charge PHP domine, que l'interface graphique doit simplifier le fonctionnement et que LSAPI réduit les goulots d'étranglement. Je recourt à Apache lorsque je dépends des modules et .htaccess et que je maîtrise parfaitement la configuration MPM-event. Les mécanismes d'auto-ajustement suffisent dans de nombreux cas ; je ne dois intervenir que lorsque les métriques indiquent des blocages, des limites strictes ou une pression sur la RAM [2]. Avec des budgets réalistes en termes de cœur et de RAM, de petits incréments et l'observation des courbes de latence, l'ajustement des threads me permet d'atteindre mon objectif de manière fiable.

Derniers articles