Les pools de threads dans NGINX multiplient les performances par 9 !

NGINX | 19 juin 2015

Il est bien connu que NGINX utilise une approche asynchrone et événementielle pour gérer les connexions. Cela signifie qu’au lieu de créer un processus ou un thread dédié supplémentaire pour chaque requête (comme le font les serveurs dotés d’une architecture traditionnelle), il gère plusieurs connexions et requêtes au sein d’un même processus de travail. Pour ce faire, NGINX utilise des sockets en mode non bloquant et des méthodes efficaces telles que epoll et kqueue.

Comme le nombre de processus complets est faible (généralement un seul par cœur de processeur) et constant, la consommation de mémoire est nettement réduite et aucun cycle processeur n’est gaspillé en changements de contexte. Les avantages d’une telle approche sont bien connus, comme l’illustre NGINX lui-même. Il gère avec succès des millions de requêtes simultanées et offre une excellente évolutivité.

Chaque processus consomme de la mémoire supplémentaire, et chaque basculement de l’un à l’autre mobilise des cycles CPU et perturbe les caches L

Mais l’approche asynchrone pilotée par les événements présente encore un problème. Ou, comme j’aime à le voir, un « ennemi ». Et cet ennemi porte un nom : le blocage. Malheureusement, de nombreux modules tiers utilisent des appels bloquants, et les utilisateurs (et parfois même les développeurs de ces modules) n’en connaissent pas les inconvénients. Les opérations bloquantes peuvent nuire aux performances de NGINX et doivent être évitées à tout prix.

Même dans le code officiel actuel de NGINX, il n’est pas toujours possible d’éviter les opérations bloquantes. Pour résoudre ce problème, le nouveau mécanisme de « pools de threads » a été implémenté dans NGINX version 1.7.11 et NGINX Plus version 7. Nous verrons plus loin de quoi il s’agit et comment il est censé être utilisé. Pour l’heure, allons à la rencontre de notre ennemi.

Note de la rédaction – Pour une présentation de NGINX Plus R7, consultez « Présentation de NGINX Plus R7 » sur notre blog.

Pour une présentation détaillée des autres nouvelles fonctionnalités de NGINX Plus R7, consultez les articles de blog connexes suivants :

Le problème

Tout d’abord, pour mieux comprendre le problème, quelques mots sur le fonctionnement de NGINX.

De manière générale, NGINX est un gestionnaire d’événements, un contrôleur qui reçoit du noyau des informations sur tous les événements survenant au niveau des connexions, puis indique au système d’exploitation ce qu’il doit faire. En réalité, NGINX effectue tout le travail complexe en orchestrant le système d’exploitation, tandis que ce dernier se charge des tâches courantes de lecture et d’envoi des octets. Il est donc essentiel que NGINX réagisse rapidement et en temps voulu.

Boucle d’événements NGINX 2
Le processus worker écoute et traite les événements provenant du noyau

Les événements peuvent être des expirations de délai, des notifications indiquant que des sockets sont prêts pour la lecture ou l’écriture, ou encore des notifications signalant qu’une erreur s’est produite. NGINX reçoit un ensemble d’événements, puis les traite un par un en effectuant les actions nécessaires. L’ensemble du traitement s’effectue donc dans un seul thread, au moyen d’une simple boucle parcourant une file d’attente. NGINX retire un événement de la file, puis y réagit, par exemple en écrivant dans un socket ou en lisant depuis celui-ci. Dans la plupart des cas, cette opération est extrêmement rapide (elle ne nécessite parfois que quelques cycles de processeur pour copier des données en mémoire), et NGINX traite en un instant tous les événements de la file d’attente.

Cycle de traitement de la file d’attente des événements
Tout le traitement est effectué par un seul thread au sein d’une boucle simple

Mais que se passera-t-il si une opération longue et lourde se produit ? L’ensemble du cycle de traitement des événements restera bloqué en attendant que cette opération se termine.

Ainsi, par « opération bloquante », nous entendons toute opération qui interrompt le cycle de traitement des événements pendant une durée significative. Une opération peut être bloquante pour diverses raisons. Par exemple, NGINX peut être occupé par un traitement long et gourmand en ressources processeur, ou devoir attendre de pouvoir accéder à une ressource (comme un disque dur, un mutex ou un appel à une fonction de bibliothèque qui récupère de manière synchrone les réponses d’une base de données, etc.). Le point essentiel est que, pendant le traitement de telles opérations, le processus de travail ne peut rien faire d’autre ni traiter d’autres événements, même si des ressources système supplémentaires sont disponibles et que certains événements de la file d’attente pourraient les utiliser.

Imaginez un vendeur dans un magasin, avec une longue file d’attente devant lui. La première personne dans la file demande un article qui n’est pas disponible dans le magasin, mais qui se trouve dans l’entrepôt. Le vendeur part donc le chercher. Toute la file doit alors attendre quelques heures qu’il revienne avec l’article, au grand mécontentement de chacun. Imaginez la réaction des clients ! Le temps d’attente de chaque personne dans la file s’allonge de plusieurs heures, alors même que les articles qu’elles souhaitent acheter se trouvent peut-être juste là, dans le magasin.

Toutes les personnes dans la file d’attente doivent attendre que la commande de la première personne soit prête

Une situation presque identique se produit avec NGINX lorsqu’il demande à lire un fichier qui n’est pas mis en cache en mémoire et doit donc être lu sur le disque. Les disques durs sont lents (en particulier les disques mécaniques) et, même si les autres requêtes en attente dans la file n’ont pas forcément besoin d’accéder au disque, elles sont tout de même contraintes d’attendre. Par conséquent, les temps de latence augmentent et les ressources du système ne sont pas pleinement exploitées.

Une seule opération bloquante peut retarder considérablement toutes les opérations suivantes

Certains systèmes d’exploitation proposent une interface asynchrone pour la lecture et l’envoi de fichiers, et NGINX peut l’utiliser (voir la directive aio). FreeBSD en est un bon exemple. Malheureusement, on ne peut pas en dire autant de Linux. Bien que Linux propose une forme d’interface asynchrone pour la lecture de fichiers, celle-ci présente quelques inconvénients majeurs. L’un d’eux concerne les contraintes d’alignement pour l’accès aux fichiers et les tampons, mais NGINX les gère correctement. Le second problème est toutefois plus grave. L’interface asynchrone exige que l’indicateur O_DIRECT soit défini sur le descripteur de fichier, ce qui signifie que tout accès au fichier contournera le cache en mémoire et augmentera la charge sur les disques durs. Cette solution est donc loin d’être optimale dans de nombreux cas.

Pour résoudre ce problème en particulier, les pools de threads ont été introduits dans NGINX 1.7.11 et NGINX Plus version 7.

Voyons maintenant plus en détail ce que sont les pools de threads et comment ils fonctionnent.

Pools de threads

Revenons à notre pauvre vendeur qui va chercher des marchandises dans un entrepôt éloigné. Mais il est devenu plus malin (peut-être après avoir été passé à tabac par une foule de clients furieux ?) et a fait appel à un service de livraison. Désormais, lorsque quelqu’un demande un article provenant de cet entrepôt, au lieu d’aller le chercher lui-même, il transmet simplement la commande au service de livraison, qui s’en charge pendant que notre vendeur continue de servir les autres clients. Ainsi, seuls les clients dont les articles ne sont pas en magasin doivent attendre la livraison, tandis que les autres peuvent être servis immédiatement.

Confier une commande au service de livraison débloque la file d’attente

Dans le contexte de NGINX, le pool de threads remplit les fonctions du service de livraison. Il se compose d’une file d’attente de tâches et de plusieurs threads qui traitent cette file. Lorsqu’un processus worker doit effectuer une opération potentiellement longue, au lieu de la traiter lui-même, il place une tâche dans la file d’attente du pool, où elle peut être récupérée et traitée par n’importe quel thread disponible.

Les pools de threads contribuent à améliorer les performances des applications en confiant les opérations lentes à un ensemble de tâches distinct.
Le processus de travail délègue les opérations bloquantes au pool de threads

Il semble donc que nous ayons une autre file d’attente. En effet. Mais dans ce cas, la file d’attente est limitée par une ressource spécifique. Nous ne pouvons pas lire les données d’un disque plus vite que celui-ci ne peut les fournir. Désormais, au moins, le disque ne retarde pas le traitement des autres événements et seules les requêtes qui doivent accéder à des fichiers sont mises en attente.

L’opération de « lecture depuis le disque » est souvent citée comme l’exemple le plus courant d’une opération bloquante, mais l’implémentation des pools de threads dans NGINX peut en réalité être utilisée pour toutes les tâches qu’il n’est pas approprié de traiter dans le cycle de travail principal.

À l’heure actuelle, le déport vers les pools de threads n’est mis en œuvre que pour trois opérations essentielles : l’appel système read() sur la plupart des systèmes d’exploitation, sendfile() sous Linux et aio_write() sous Linux, utilisé lors de l’écriture de certains fichiers temporaires, tels que ceux du cache. Nous continuerons à tester l’implémentation et à en mesurer les performances. Dans les prochaines versions, nous pourrons déporter d’autres opérations vers les pools de threads si cela présente un avantage manifeste.

Note de la rédaction – La prise en charge de l’ aio_write() appel système a été ajoutée dans NGINX 1.9.13 et NGINX Plus R9.

Analyse comparative des performances

Il est temps de passer de la théorie à la pratique. Pour illustrer l’effet de l’utilisation de pools de threads, nous allons réaliser un benchmark synthétique qui simule la pire combinaison d’opérations bloquantes et non bloquantes.

Cela nécessite un jeu de données dont la taille dépasse assurément la capacité de la mémoire. Sur une machine dotée de 48 Go de RAM, nous avons généré 256 Go de données aléatoires réparties dans des fichiers de 4 Mo, puis configuré NGINX 1.9.0 pour les servir.

La configuration est assez simple :

worker_processes 16;
events {
    accept_mutex off;
}

http {
    include mime.types;
    default_type application/octet-stream;

    access_log off;
    sendfile on;
    sendfile_max_chunk 512k;

    server {
        listen 8000;

        location / {
            root /storage;
        }
    }
}

Comme vous pouvez le constater, certains réglages ont été effectués afin d’améliorer les performances : logging et accept_mutex ont été désactivés, sendfile a été activé et sendfile_max_chunk a été défini. Cette dernière directive permet de réduire la durée maximale des appels bloquants à sendfile() : NGINX n’essaiera pas d’envoyer le fichier entier en une seule fois, mais procédera par blocs de 512 Ko.

La machine est équipée de deux processeurs Intel Xeon E5645 (soit 12 cœurs et 24 threads avec l’Hyper-Threading au total) et d’une interface réseau à 10 Gbit/s. Le sous-système de stockage se compose de quatre disques durs Western Digital WD1003FBYX configurés en RAID 10. L’ensemble fonctionne sous Ubuntu Server 14.04.1 LTS.

Configuration des générateurs de charge et de NGINX pour le benchmark

Les clients sont représentés par deux machines aux caractéristiques identiques. Sur l’une d’elles, wrk génère une charge à l’aide d’un script Lua. Ce script demande des fichiers à notre serveur dans un ordre aléatoire au moyen de 200 connexions parallèles, et chaque requête est susceptible d’entraîner un défaut de cache et une lecture bloquante sur le disque. Appelons cette charge la charge aléatoire.

Sur la deuxième machine cliente, nous exécuterons une autre instance de wrk qui demandera plusieurs fois le même fichier au moyen de 50 connexions parallèles. Comme ce fichier sera fréquemment consulté, il restera en permanence en mémoire. Dans des circonstances normales, NGINX traiterait ces requêtes très rapidement, mais les performances diminueront si les processus de travail sont bloqués par d’autres requêtes. Appelons cette charge la charge constante.

Les performances seront mesurées en surveillant le débit de la machine serveur à l’aide de ifstat et en récupérant les résultats de wrk sur le second client.

À présent, le premier test sans pools de threads ne donne pas de résultats très encourageants :

% ifstat -bi eth2eth2
Kbps in  Kbps out
5531.24  1.03e+06
4855.23  812922.7
5994.66  1.07e+06
5476.27  981529.3
6353.62  1.12e+06
5166.17  892770.3
5522.81  978540.8
6208.10  985466.7
6370.79  1.12e+06
6123.33  1.07e+06

Comme vous pouvez le constater, avec cette configuration, le serveur est capable de générer au total environ 1 Gbit/s de trafic. Dans la sortie de « top », nous pouvons voir que tous les processus de travail passent la majeure partie de leur temps à effectuer des E/S bloquantes (ils sont dans l’état « D ») :

top - 10:40:47 up 11 days,  1:32,  1 user,  load average: 49.61, 45.77 62.89Tasks: 375 total,  2 running, 373 sleeping,  0 stopped,  0 zombie
%Cpu(s):  0.0 us,  0.3 sy,  0.0 ni, 67.7 id, 31.9 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem:  49453440 total, 49149308 used,   304132 free,    98780 buffers
KiB Swap: 10474236 total,    20124 used, 10454112 free, 46903412 cached Mem

  PID USER     PR  NI    VIRT    RES     SHR S  %CPU %MEM    TIME+ COMMAND
 4639 vbart    20   0   47180  28152     496 D   0.7  0.1  0:00.17 nginx
 4632 vbart    20   0   47180  28196     536 D   0.3  0.1  0:00.11 nginx
 4633 vbart    20   0   47180  28324     540 D   0.3  0.1  0:00.11 nginx
 4635 vbart    20   0   47180  28136     480 D   0.3  0.1  0:00.12 nginx
 4636 vbart    20   0   47180  28208     536 D   0.3  0.1  0:00.14 nginx
 4637 vbart    20   0   47180  28208     536 D   0.3  0.1  0:00.10 nginx
 4638 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.12 nginx
 4640 vbart    20   0   47180  28324     540 D   0.3  0.1  0:00.13 nginx
 4641 vbart    20   0   47180  28324     540 D   0.3  0.1  0:00.13 nginx
 4642 vbart    20   0   47180  28208     536 D   0.3  0.1  0:00.11 nginx
 4643 vbart    20   0   47180  28276     536 D   0.3  0.1  0:00.29 nginx
 4644 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.11 nginx
 4645 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.17 nginx
 4646 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.12 nginx
 4647 vbart    20   0   47180  28208     532 D   0.3  0.1  0:00.17 nginx
 4631 vbart    20   0   47180    756     252 S   0.0  0.1  0:00.00 nginx
 4634 vbart    20   0   47180  28208     536 D   0.0  0.1  0:00.11 nginx< 4648 vbart 20 0 25232 1956 1160 R 0.0 0.0 0:00.08 top
25921 vbart    20   0  121956   2232    1056 S   0.0  0.0  0:01.97 sshd
25923 vbart    20   0   40304   4160    2208 S   0.0  0.0  0:00.53 zsh

Dans ce cas, le débit est limité par le sous-système de stockage, tandis que le processeur reste inactif la plupart du temps. Les résultats obtenus avec wrk sont également très faibles :

Running 1m test @ http://192.0.2.1:8000/1/1/1  12 threads and 50 connections
  Thread Stats   Avg    Stdev     Max  +/- Stdev
    Latency     7.42s  5.31s   24.41s   74.73%
    Req/Sec     0.15    0.36     1.00    84.62%
  488 requests in 1.01m, 2.01GB read
Requests/sec:      8.08
Transfer/sec:     34.07MB

Et n’oubliez pas qu’il s’agit du fichier qui devrait être servi depuis la mémoire ! Ces temps de latence excessivement élevés s’expliquent par le fait que tous les processus de travail sont occupés à lire des fichiers sur les disques afin de répondre à la charge aléatoire générée par les 200 connexions du premier client, et ne peuvent donc pas traiter nos requêtes dans les délais impartis.

Il est temps de mettre nos pools de threads à contribution. Pour cela, il suffit d’ajouter la directive « aio threads » au bloc « location »:

location / {    root /storage;
    aio threads;
}

et demandez à NGINX de recharger sa configuration.

Après cela, nous répétons le test :

% ifstat -bi eth2eth2
Kbps in  Kbps out
60915.19  9.51e+06
59978.89  9.51e+06
60122.38  9.51e+06
61179.06  9.51e+06
61798.40  9.51e+06
57072.97  9.50e+06
56072.61  9.51e+06
61279.63  9.51e+06
61243.54  9.51e+06
59632.50  9.50e+06

Notre serveur atteint désormais 9,5 Gbit/s, contre environ 1 Gbit/s sans pools de threads !

Il pourrait probablement fournir un débit encore plus élevé, mais il a déjà atteint la capacité maximale du réseau en pratique. Dans ce test, NGINX est donc limité par l’interface réseau. Les processus de travail passent la majeure partie de leur temps en veille, dans l’attente de nouveaux événements (ils sont à l’état S dans top) :

top - 10:43:17 up 11 days,  1:35,  1 user,  load average: 172.71, 93.84, 77.90Tasks: 376 total,  1 running, 375 sleeping,  0 stopped,  0 zombie
%Cpu(s):  0.2 us,  1.2 sy,  0.0 ni, 34.8 id, 61.5 wa,  0.0 hi,  2.3 si,  0.0 st
KiB Mem:  49453440 total, 49096836 used,   356604 free,    97236 buffers
KiB Swap: 10474236 total,    22860 used, 10451376 free, 46836580 cached Mem

  PID USER     PR  NI    VIRT    RES     SHR S  %CPU %MEM    TIME+ COMMAND
 4654 vbart    20   0  309708  28844     596 S   9.0  0.1  0:08.65 nginx
 4660 vbart    20   0  309748  28920     596 S   6.6  0.1  0:14.82 nginx
 4658 vbart    20   0  309452  28424     520 S   4.3  0.1  0:01.40 nginx
 4663 vbart    20   0  309452  28476     572 S   4.3  0.1  0:01.32 nginx
 4667 vbart    20   0  309584  28712     588 S   3.7  0.1  0:05.19 nginx
 4656 vbart    20   0  309452  28476     572 S   3.3  0.1  0:01.84 nginx
 4664 vbart    20   0  309452  28428     524 S   3.3  0.1  0:01.29 nginx
 4652 vbart    20   0  309452  28476     572 S   3.0  0.1  0:01.46 nginx
 4662 vbart    20   0  309552  28700     596 S   2.7  0.1  0:05.92 nginx
 4661 vbart    20   0  309464  28636     596 S   2.3  0.1  0:01.59 nginx
 4653 vbart    20   0  309452  28476     572 S   1.7  0.1  0:01.70 nginx
 4666 vbart    20   0  309452  28428     524 S   1.3  0.1  0:01.63 nginx
 4657 vbart    20   0  309584  28696     592 S   1.0  0.1  0:00.64 nginx
 4655 vbart    20   0  30958   28476     572 S   0.7  0.1  0:02.81 nginx
 4659 vbart    20   0  309452  28468     564 S   0.3  0.1  0:01.20 nginx
 4665 vbart    20   0  309452  28476     572 S   0.3  0.1  0:00.71 nginx 5180 vbart 20 0 25232 1952 1156 R 0.0 0.0 0:00.45 top
 4651 vbart    20   0   20032    752     252 S   0.0  0.0  0:00.00 nginx
25921 vbart    20   0  121956   2176    1000 S   0.0  0.0  0:01.98 sshd
25923 vbart    20   0   40304   3840    2208 S   0.0  0.0  0:00.54 zsh

Il reste encore beaucoup de ressources CPU disponibles.

Les résultats de « wrk » :

Running 1m test @ http://192.0.2.1:8000/1/1/1  12 threads and 50 connections
  Thread Stats   Avg      Stdev     Max  +/- Stdev
    Latency   226.32ms  392.76ms   1.72s   93.48%
    Req/Sec    20.02     10.84    59.00    65.91%
  15045 requests in 1.00m, 58.86GB read
Requests/sec:    250.57
Transfer/sec:      0.98GB

Le temps moyen nécessaire pour servir un fichier de 4 Mo est passé de 7,42 secondes à 226,32 millisecondes (soit 33 fois moins), et le nombre de requêtes par seconde a été multiplié par 31 (250 contre 8) !

Cela s’explique par le fait que nos requêtes n’attendent plus d’être traitées dans la file d’attente des événements pendant que les processus de travail sont bloqués par des opérations de lecture, mais sont prises en charge par des threads disponibles. Tandis que le sous-système de disque s’acquitte au mieux de sa tâche en traitant notre charge aléatoire provenant de la première machine cliente, NGINX utilise les ressources processeur et la capacité réseau restantes pour répondre depuis la mémoire aux requêtes du second client.

Toujours pas une solution miracle

Après toutes nos craintes concernant les opérations bloquantes et au vu de certains résultats prometteurs, la plupart d’entre vous envisagent probablement déjà de configurer des pools de threads sur leurs serveurs. Ne vous précipitez pas.

En réalité, heureusement, la plupart des opérations de lecture et d’envoi de fichiers ne sollicitent pas les disques durs lents. Si vous disposez de suffisamment de RAM pour stocker le jeu de données, le système d’exploitation sera assez intelligent pour mettre en cache les fichiers fréquemment utilisés dans ce que l’on appelle un « cache de pages ».

Le cache de pages fonctionne plutôt bien et permet à NGINX d’offrir d’excellentes performances dans presque tous les cas d’utilisation courants. La lecture depuis le cache de pages est assez rapide, et personne ne peut qualifier de telles opérations de « bloquantes ». En revanche, leur délégation à un pool de threads entraîne une certaine surcharge.

Ainsi, si vous disposez d’une quantité raisonnable de RAM et que votre jeu de données de travail n’est pas très volumineux, NGINX fonctionne déjà de manière optimale sans recourir à des pools de threads.

Le transfert des opérations de lecture vers le pool de threads est une technique adaptée à des tâches très spécifiques. Elle est particulièrement utile lorsque le volume de contenu fréquemment demandé dépasse la capacité du cache de mémoire virtuelle du système d’exploitation. Cela peut être le cas, par exemple, d’un serveur de diffusion multimédia basé sur NGINX et soumis à une forte charge. C’est cette situation que nous avons simulée dans notre test de performance.

Ce serait formidable de pouvoir améliorer la délégation des opérations de lecture aux pools de threads. Il nous suffit de disposer d’un moyen efficace de savoir si les données de fichier nécessaires se trouvent ou non en mémoire, et de ne déléguer l’opération de lecture à un thread distinct que dans ce dernier cas.

Pour reprendre notre analogie avec la vente, le vendeur ne peut actuellement pas savoir si l’article demandé est disponible en magasin et doit donc soit toujours transmettre toutes les commandes au service de livraison, soit toujours s’en charger lui-même.

Le problème vient du fait que les systèmes d’exploitation ne disposent pas de cette fonctionnalité. Les premières tentatives visant à l’ajouter à Linux sous la forme de l’appel système fincore() remontent à 2010, mais elles n’ont pas abouti. Par la suite, plusieurs tentatives ont été menées pour l’implémenter sous la forme d’un nouvel appel système preadv2() avec l’indicateur RWF_NONBLOCK (voir Opérations de lecture de fichiers mises en mémoire tampon non bloquantes et Opérations de lecture mises en mémoire tampon asynchrones sur LWN.net pour plus de détails). Le sort de tous ces correctifs reste incertain. Ce qui est regrettable ici, c’est que la principale raison pour laquelle ces correctifs n’ont toujours pas été intégrés au noyau semble être d’interminables débats sur des détails secondaires (« bikeshedding »).

En revanche, les utilisateurs de FreeBSD n’ont aucun souci à se faire. FreeBSD dispose déjà d’une interface asynchrone suffisamment performante pour la lecture de fichiers, qu’il convient d’utiliser à la place des pools de threads.

Configuration des pools de threads

Donc, si vous êtes certain que l’utilisation de pools de threads peut être avantageuse dans votre cas, il est temps d’examiner la configuration en détail.

La configuration est assez simple et flexible. Vous devez tout d’abord disposer de NGINX version 1.7.11 ou ultérieure, compilé avec l’argument --with-threads de la commande configure. Les utilisateurs de NGINX Plus doivent disposer de la version 7 ou ultérieure. Dans le cas le plus simple, la configuration est très basique. Il suffit d’inclure la directive aio threads dans le contexte approprié :

# in the 'http', 'server', or 'location' contextaio threads;

Il s’agit de la configuration minimale possible des pools de threads. En fait, c’est une version abrégée de la configuration suivante :

# in the 'main' contextthread_pool default threads=32 max_queue=65536;
 
# in the 'http', 'server', or 'location' context
aio threads=default;

Il définit un pool de threads appelé « default », comprenant 32 threads de travail et une file d’attente des tâches d’une longueur maximale de 65 536 tâches. Si la file d’attente est saturée, NGINX rejette la requête et consigne l’erreur suivante :

thread pool "NAME" queue overflow: N tasks waiting

Cette erreur signifie qu’il est possible que les threads ne puissent pas traiter les tâches aussi rapidement qu’elles sont ajoutées à la file d’attente. Vous pouvez essayer d’augmenter la taille maximale de la file d’attente, mais si cela ne résout pas le problème, cela indique que votre système n’est pas en mesure de traiter autant de requêtes.

Comme vous l’avez déjà remarqué, la directive « thread_pool » permet de configurer le nombre de threads, la longueur maximale de la file d’attente et le nom d’un pool de threads spécifique. Ce dernier point signifie que vous pouvez configurer plusieurs pools de threads indépendants et les utiliser à différents endroits de votre fichier de configuration à des fins distinctes :

# in the 'main' context
thread_pool one threads=128 max_queue=0;
thread_pool two threads=32;

http {
    server {
        location /one {
            aio threads=one;
        }

        location /two {
            aio threads=two;
        }

    }
    # ...
}

Si le paramètre « max_queue » n’est pas spécifié, la valeur 65536 est utilisée par défaut. Comme indiqué, il est possible de définir max_queue sur zéro. Dans ce cas, le pool de threads ne pourra traiter qu’autant de tâches qu’il y a de threads configurés ; aucune tâche ne sera mise en attente dans la file.

Imaginons maintenant que vous disposiez d’un serveur équipé de trois disques durs et que vous souhaitiez l’utiliser comme « proxy de mise en cache » afin de mettre en cache toutes les réponses de vos serveurs back-end. Le volume de données à mettre en cache devrait largement dépasser la quantité de RAM disponible. Il s’agit en fait d’un nœud de cache pour votre CDN personnel. Bien entendu, dans ce cas, le plus important est de tirer le maximum de performances des disques.

L’une des possibilités consiste à configurer une grappe RAID. Cette approche présente des avantages et des inconvénients. Avec NGINX, vous disposez désormais d’une autre option :

# We assume that each of the hard drives is mounted on one of these directories:# /mnt/disk1, /mnt/disk2, or /mnt/disk3

# in the 'main' context
thread_pool pool_1 threads=16;
thread_pool pool_2 threads=16;
thread_pool pool_3 threads=16;

http {
    proxy_cache_path /mnt/disk1 levels=1:2 keys_zone=cache_1:256m max_size=1024G 
                     use_temp_path=off;
    proxy_cache_path /mnt/disk2 levels=1:2 keys_zone=cache_2:256m max_size=1024G 
                     use_temp_path=off;
    proxy_cache_path /mnt/disk3 levels=1:2 keys_zone=cache_3:256m max_size=1024G 
                     use_temp_path=off;

    split_clients $request_uri $disk {
        33.3%     1;
        33.3%     2;
        *         3;
    }
    
    server {
        # ...
        location / {
            proxy_pass http://backend;
            proxy_cache_key $request_uri;
            proxy_cache cache_$disk;
            aio threads=pool_$disk;
            sendfile on;
        }
    }
}

Dans cette configuration, les directives thread_pool définissent un pool de threads dédié et indépendant pour chaque disque, tandis que les directives proxy_cache_path définissent un cache dédié et indépendant sur chaque disque.

Le module « split_clients » est utilisé pour répartir la charge entre les caches (et donc entre les disques), ce qui convient parfaitement à cette tâche.

Le paramètre « use_temp_path=off » de la directive proxy_cache_path indique à NGINX d’enregistrer les fichiers temporaires dans les mêmes répertoires que les données de cache correspondantes. Cela permet d’éviter de copier les données de réponse d’un disque dur à l’autre lors de la mise à jour de nos caches.

L’ensemble de ces mesures nous permet de tirer le maximum de performances du sous-système de disques actuel, car NGINX interagit avec les disques en parallèle et de manière indépendante au moyen de pools de threads distincts. Chaque disque est pris en charge par 16 threads indépendants disposant d’une file d’attente de tâches dédiée à la lecture et à l’envoi de fichiers.

Je parie que vos clients apprécient cette approche sur mesure. Soyez certain que vos disques durs l’apprécient eux aussi.

Cet exemple illustre bien la grande souplesse avec laquelle NGINX peut être configuré spécifiquement pour votre matériel. C’est comme si vous indiquiez à NGINX la meilleure façon d’interagir avec la machine et votre jeu de données. En affinant la configuration de NGINX dans l’espace utilisateur, vous pouvez vous assurer que vos logiciels, votre système d’exploitation et votre matériel fonctionnent ensemble de manière optimale afin d’exploiter toutes les ressources du système aussi efficacement que possible.

Conclusion

En résumé, les pools de threads sont une fonctionnalité remarquable qui permet à NGINX d’atteindre de nouveaux niveaux de performances en éliminant l’un de ses ennemis les plus connus et les plus anciens — le blocage —, notamment lorsqu’il s’agit de très gros volumes de contenu.

Et ce n’est pas tout. Comme indiqué précédemment, cette toute nouvelle interface permet potentiellement de déléguer toute opération longue et bloquante sans aucune perte de performances. NGINX ouvre de nouveaux horizons en permettant l’ajout d’une multitude de nouveaux modules et de nouvelles fonctionnalités. De nombreuses bibliothèques populaires ne proposent toujours pas d’interface asynchrone non bloquante, ce qui les rendait jusqu’ici incompatibles avec NGINX. Nous pourrions consacrer beaucoup de temps et de ressources au développement de notre propre prototype non bloquant pour une bibliothèque donnée, mais le jeu en vaut-il toujours la chandelle ? Désormais, grâce aux pools de threads, il est relativement facile d’utiliser de telles bibliothèques et de créer des modules qui les exploitent sans nuire aux performances.

Restez à l’écoute.

Testez vous-même les pools de threads dans NGINX Plus : commencez votre essai gratuit de 30 jours dès aujourd’hui ou contactez-nous pour discuter de vos cas d’utilisation.


Partager
Mots-clés : F5 NGINX, 2015, Tech

Articles de blog associés

Tendances actuelles des technologies cloud natives, de l’ingénierie de plateforme et de l’IA
NGINX | 13/10/2025

Tendances actuelles des technologies cloud natives, de l’ingénierie de plateforme et de l’IA

Découvrez les principaux enseignements tirés des réponses à l’enquête annuelle F5 NGINX sur les dernières tendances en matière de technologies cloud natives, de sécurité, d’ingénierie de plateforme et d’IA.

Quels contrôleurs Ingress NGINX sont affectés par les vulnérabilités CVE-2022-4886, CVE-2023-5043 et CVE-2023-5044 ?
NGINX | 03/11/2023

Quels contrôleurs Ingress NGINX sont affectés par les vulnérabilités CVE-2022-4886, CVE-2023-5043 et CVE-2023-5044 ?

Lorsque de nouvelles CVE sont signalées, il est facile de ne plus savoir si elles concernent un outil de contrôleur Ingress NGINX particulier, car il existe plusieurs projets. Cet article présente les CVE et explique comment distinguer les différents projets de contrôleurs Ingress afin de dissiper toute confusion.

Votre API est-elle en temps réel ? Testez sa latence avec l’outil rtapi de NGINX
NGINX | 11/03/2020

Votre API est-elle en temps réel ? Testez sa latence avec l’outil rtapi de NGINX

Votre API répond-elle aux requêtes en moins de 30 ms ? Si ce n’est pas le cas, il ne s’agit pas d’une API en temps réel offrant la faible latence exigée par la grande majorité des organisations. Mesurez la latence de votre API avec l’outil rtapi de NGINX et découvrez comment NGINX peut l’aider à offrir une réactivité en temps réel.

Un testeur d’expressions régulières pour NGINX et NGINX Plus
NGINX | 11/06/2019

Un testeur d’expressions régulières pour NGINX et NGINX Plus

Le testeur d’expressions régulières (regex) pour NGINX et NGINX Plus vous évite de tâtonner en vous indiquant si une expression régulière utilisée dans un bloc location ou map correspond aux valeurs comme prévu. Il génère la configuration appropriée et affiche les valeurs des groupes de capture.

Caches partagés avec les clusters de cache NGINX Plus, partie 1
NGINX | 24/01/2017

Caches partagés avec les clusters de cache NGINX Plus, partie 1

Optimisez la capacité de votre cache web en le répartissant entre plusieurs serveurs de cache web NGINX ou NGINX Plus. Découvrez comment procéder dans cet article.

Introduction aux microservices
NGINX | 19/05/2015

Introduction aux microservices

Les microservices suscitent actuellement beaucoup d’intérêt. Cet article de blog est le premier d’une série de sept consacrée à la conception, au développement et au déploiement de microservices.