BLOG | NGINX

Les pools de threads dans NGINX augmentent les performances de 9x !

NGINX-Partie-de-F5-horiz-black-type-RGB
Vignette de Valentin Bartenev
Valentin Bartenev
Publié le 19 juin 2015

Il est bien connu que NGINX utilise une approche asynchrone et pilotée par événements pour gérer les connexions . Cela signifie qu'au lieu de créer un autre processus ou thread dédié pour chaque demande (comme les serveurs avec une architecture traditionnelle), il gère plusieurs connexions et demandes dans un seul processus de travail. Pour y parvenir, NGINX fonctionne avec des sockets en mode non bloquant et utilise des méthodes efficaces telles que epoll et kqueue .

Étant donné que le nombre de processus complets est faible (généralement un seul par cœur de processeur) et constant, beaucoup moins de mémoire est consommée et les cycles de processeur ne sont pas gaspillés lors du changement de tâche. Les avantages d’une telle approche sont bien connus à travers l’exemple de NGINX lui-même. Il gère avec succès des millions de requêtes simultanées et s'adapte très bien.

Chaque processus consomme de la mémoire supplémentaire et chaque commutation entre eux consomme des cycles CPU et détruit les caches L.

Mais l’approche asynchrone et axée sur les événements présente toujours un problème. Ou, comme j’aime à le dire, un « ennemi ». Et le nom de l'ennemi est : blocage . Malheureusement, de nombreux modules tiers utilisent des appels bloquants, et les utilisateurs (et parfois même les développeurs des modules) ne sont pas conscients des inconvénients. Les opérations de blocage peuvent ruiner les performances de NGINX et doivent être évitées à tout prix.

Même dans le code officiel NGINX actuel, il n'est pas possible d'éviter les opérations de blocage dans tous les cas, et 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 Release 7 . Nous verrons plus tard ce que c'est et comment l'utiliser. Rencontrons maintenant notre ennemi face à face.

Éditeur – Pour un aperçu de NGINX Plus R7, voir Annonce de NGINX Plus R7 sur notre blog.

Pour des discussions détaillées sur d'autres nouvelles fonctionnalités de NGINX Plus R7, consultez ces articles de blog associés :

 

Le problème

Tout d’abord, pour une meilleure compréhension du problème, quelques mots sur le fonctionnement de NGINX.

En général, NGINX est un gestionnaire d'événements, un contrôleur qui reçoit des informations du noyau sur tous les événements se produisant sur les connexions, puis donne des commandes au système d'exploitation sur ce qu'il doit faire. En fait, NGINX fait tout le travail difficile en orchestrant le système d’exploitation, tandis que le système d’exploitation effectue le travail de routine de lecture et d’envoi d’octets. Il est donc très important pour NGINX de réagir rapidement et dans les meilleurs délais.

Boucle d'événement NGINX2
Le processus de travail écoute et traite les événements du noyau

Les événements peuvent être des délais d'attente, des notifications concernant des sockets prêts à lire ou à écrire, ou des notifications concernant une erreur survenue. NGINX reçoit un ensemble d'événements et les traite ensuite un par un, en effectuant les actions nécessaires. Ainsi, tout le traitement est effectué dans une boucle simple sur une file d'attente dans un seul thread. NGINX retire un événement de la file d'attente, puis réagit en écrivant ou en lisant un socket, par exemple. Dans la plupart des cas, cela est extrêmement rapide (nécessitant peut-être juste quelques cycles CPU pour copier certaines données en mémoire) et NGINX traite tous les événements de la file d'attente en un instant.

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

Mais que se passera-t-il si une opération longue et lourde a eu lieu ? L'ensemble du cycle de traitement des événements restera bloqué en attendant la fin de cette opération.

Ainsi, en disant « une opération de blocage », nous entendons toute opération qui arrête le cycle de gestion des événements pendant une durée significative. Les opérations peuvent être bloquantes pour diverses raisons. Par exemple, NGINX peut être occupé par un traitement long et gourmand en ressources CPU, ou il peut devoir attendre pour accéder à une ressource (comme un disque dur, un appel de fonction mutex ou de bibliothèque qui obtient des réponses d'une base de données de manière synchrone, etc.). Le point clé est que lors du traitement de telles opérations, le processus de travail ne peut rien faire d’autre et ne peut pas gérer d’autres événements, même s’il y a plus de ressources système disponibles et que certains événements dans la file d’attente pourraient utiliser ces ressources.

Imaginez un vendeur dans un magasin avec une longue file d’attente devant lui. Le premier gars dans la file demande quelque chose qui n'est pas dans le magasin mais qui est dans l'entrepôt. Le vendeur se rend à l'entrepôt pour livrer les marchandises. Maintenant, toute la file d'attente doit attendre quelques heures pour cette livraison et tout le monde dans la file d'attente est mécontent. Pouvez-vous imaginer la réaction des gens ? Le temps d'attente de chaque personne dans la file d'attente est augmenté par ces heures, mais les articles qu'ils ont l'intention d'acheter peuvent être là, dans le magasin.

Tout le monde dans la file d'attente doit attendre la commande de la première personne

Presque la même situation se produit avec NGINX lorsqu’il demande à lire un fichier qui n’est pas mis en cache en mémoire, mais qui doit être lu à partir du disque. Les disques durs sont lents (en particulier ceux qui tournent), et même si les autres requêtes en attente dans la file d'attente n'ont peut-être pas besoin d'accéder au disque, elles sont obligées d'attendre de toute façon. En conséquence, les latences augmentent et les ressources système ne sont pas pleinement utilisées.

Une seule opération de blocage peut retarder toutes les opérations suivantes pendant une durée significative

Certains systèmes d'exploitation fournissent une interface asynchrone pour la lecture et l'envoi de fichiers et NGINX peut utiliser cette interface (voir la directive aio ). Un bon exemple est FreeBSD. Malheureusement, on ne peut pas en dire autant de Linux. Bien que Linux fournisse une sorte d’interface asynchrone pour la lecture de fichiers, il présente quelques inconvénients importants. L’une d’entre elles concerne les exigences d’alignement pour l’accès aux fichiers et aux tampons, mais NGINX gère bien cela. Mais le deuxième problème est pire. L'interface asynchrone nécessite 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. Cela ne le rend certainement pas optimal dans de nombreux cas.

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

Voyons maintenant ce que sont les pools de threads et comment ils fonctionnent.

Pools de threads

Revenons à notre pauvre vendeuse qui livre des marchandises depuis un entrepôt lointain. Mais il est devenu plus intelligent (ou peut-être est-il devenu plus intelligent après avoir été battu par une foule de clients en colère ?) et a engagé un service de livraison. Désormais, lorsque quelqu'un demande quelque chose à un entrepôt éloigné, au lieu de se rendre lui-même à l'entrepôt, il dépose simplement une commande auprès d'un service de livraison qui s'occupera de la commande pendant que notre assistant commercial continuera à servir les autres clients. Ainsi, seuls les clients dont les marchandises ne sont pas en magasin attendent la livraison, tandis que les autres peuvent être servis immédiatement.

La transmission d'une commande au service de livraison débloque la file d'attente

En termes de NGINX, le pool de threads exécute les fonctions du service de livraison. Il se compose d'une file d'attente de tâches et d'un certain nombre de threads qui gèrent la file d'attente. Lorsqu'un processus de travail doit effectuer une opération potentiellement longue, au lieu de traiter l'opération lui-même, il place une tâche dans la file d'attente du pool, à partir de laquelle elle peut être extraite et traitée par n'importe quel thread libre.

Les pools de threads aident à augmenter les performances des applications en attribuant une opération lente à un ensemble distinct de tâches
Le processus de travail décharge les opérations de blocage sur le pool de threads

Il semblerait alors que nous ayons une autre file d'attente. Droite. 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 lecteur à une vitesse supérieure à celle à laquelle le lecteur est capable de produire des données. Au moins, le lecteur ne retarde pas le traitement des autres événements et seules les demandes nécessitant l’accès aux fichiers sont en attente.

L’opération « lecture à partir du disque » est souvent utilisée comme l’exemple le plus courant d’une opération de blocage, mais en réalité, l’implémentation des pools de threads dans NGINX peut être utilisée pour toutes les tâches qui ne sont pas appropriées à traiter dans le cycle de travail principal.

À l'heure actuelle, le déchargement vers des pools de threads n'est implémenté que pour trois opérations essentielles : l'appel système read() sur la plupart des systèmes d'exploitation, sendfile() sur Linux et aio_write() sur Linux qui est utilisé lors de l'écriture de certains fichiers temporaires tels que ceux du cache. Nous continuerons à tester et à évaluer l'implémentation, et nous pourrons peut-être décharger d'autres opérations sur les pools de threads dans les futures versions s'il y a un avantage évident.

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

Analyse comparative

Il est temps de passer de la théorie à la pratique. Pour démontrer l’effet de l’utilisation de pools de threads, nous allons effectuer un test synthétique qui simule le pire mélange d’opérations bloquantes et non bloquantes.

Il faut un ensemble de données qui ne pourra certainement pas tenir dans la mémoire. Sur une machine avec 48 Go de RAM, nous avons généré 256 Go de données aléatoires dans des fichiers de 4 Mo, puis avons configuré NGINX 1.9.0 pour les diffuser.

La configuration est assez simple :

processus_travailleurs 16 ;
événements {
accept_mutex désactivé ;
}

http {
inclure mime.types ;
type_par_défaut application/octet-stream ;

journal_accès désactivé ;
sendfile activé ;
sendfile_max_chunk 512 ko ;

serveur {
écouter 8 000 ;

emplacement / {
racine / stockage ;
}
}
}

Comme vous pouvez le voir, pour obtenir de meilleures performances, certains réglages ont été effectués : la journalisation et accept_mutex ont été désactivés, sendfile a été activé et sendfile_max_chunk a été défini. La dernière directive peut réduire le temps maximum passé à bloquer les appels sendfile() , puisque NGINX n'essaiera pas d'envoyer le fichier entier en une seule fois, mais le fera par blocs de 512 Ko.

La machine dispose de deux processeurs Intel Xeon E5645 (12 cœurs, 24 threads HT au total) et d'une interface réseau 10 Gbit/s. Le sous-système de disque est représenté par quatre disques durs Western Digital WD1003FBYX disposés dans une matrice RAID10. Tout ce matériel est alimenté par 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 avec les mêmes spécifications. Sur l’une de ces machines, wrk crée une charge à l’aide d’un script Lua. Le script demande des fichiers à notre serveur dans un ordre aléatoire en utilisant 200 connexions parallèles, et chaque demande est susceptible d'entraîner un échec de cache et une lecture bloquante à partir du disque. Appelons cette charge la charge aléatoire .

Sur la deuxième machine cliente, nous exécuterons une autre copie de wrk qui demandera le même fichier plusieurs fois en utilisant 50 connexions parallèles. Étant donné que ce fichier sera fréquemment consulté, il restera en mémoire en permanence. Dans des circonstances normales, NGINX répondrait à ces demandes très rapidement, mais les performances diminueront si les processus de travail sont bloqués par d'autres demandes. Appelons cette charge la charge constante .

Les performances seront mesurées en surveillant le débit de la machine serveur à l'aide d'ifstat et en obtenant les résultats wrk du deuxième client.

Maintenant, la première exécution sans pools de threads ne nous donne pas de résultats très intéressants :

% ifstat -bi eth2 eth2 Kbit/s en entrée Kbit/s en sortie 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 voir, avec cette configuration, le serveur est capable de produire environ 1 Gbps de trafic au total. Dans la sortie de top , nous pouvons voir que tous les processus de travail passent la plupart du temps à bloquer les E/S (ils sont dans un état D ) :

top - 10:40:47 en ligne depuis 11 jours, 1:32, 1 utilisateur, charge moyenne : 49,61, 45,77 62,89Tâches :375 total,2 en cours d'exécution,373 dormir,0 arrêté,0 zombie %Cpu(s):0.0 nous,0.3 oui,0.0 ni,67.7 identifiant,31.9 Washington,0.0 Salut,0.0 si,0.0 st KiB Mémoire:49453440 total,49149308 utilisé,304132 gratuit,98780 tampons KiB Swap :10474236 total,20124 utilisé,10454112 gratuit,46903412 Mémoire cache PID UTILISATEUR PR NI VIRT RES SHR S %CPU %MEM TEMPS+ COMMANDE 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 haut 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 disque, tandis que le processeur est inactif la plupart du temps. Les résultats de wrk sont également très faibles :

Exécution d'un test de 1 m à l'adresse http://192.0.2.1:8000/1/1/1 12 threads et 50 connexions
Statistiques des threads Moyenne Écart-type Max +/- Écart-type
Latence 7,42 s 5,31 s 24,41 s 74,73 %
Req/s 0,15 0,36 1,00 84,62 %
488 requêtes en 1,01 m, 2,01 Go en lecture
Requêtes/s :      8,08
Transfert/sec :     34,07 Mo

Et rappelez-vous, ceci concerne le fichier qui doit être servi depuis la mémoire ! Les latences excessivement importantes sont dues au fait que tous les processus de travail sont occupés à lire des fichiers à partir des lecteurs pour répondre à la charge aléatoire créée par 200 connexions du premier client et ne peuvent pas gérer nos demandes à temps.

Il est temps de mettre nos pools de threads en jeu. Pour cela, nous ajoutons simplement la directive aio threads au bloc d'emplacement :

emplacement / {racine/stockage ;
threads aio ;
}

et demandez à NGINX de recharger sa configuration.

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

% ifstat -bi eth2 eth2 Kbit/s en entrée Kbit/s en sortie 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 produit désormais 9,5 Gbps , contre environ 1 Gbps sans pools de threads !

Il pourrait probablement produire encore plus, mais il a déjà atteint la capacité maximale pratique du réseau, donc dans ce test, NGINX est limité par l'interface réseau. Les processus de travail passent la plupart du temps à dormir et à attendre de nouveaux événements (ils sont dans l'état S en haut ) :

top - 10:43:17 en ligne depuis 11 jours, 1:35, 1 utilisateur, charge moyenne : 172,71, 93,84, 77,90Tâches :376 total,1 en cours d'exécution,375 dormir,0 arrêté,0 zombie %Cpu(s):0.2 nous,1.2 oui,0.0 ni,34.8 identifiant,61.5 Washington,0.0 Salut,2.3 si,0.0 st KiB Mémoire:49453440 total,49096836 utilisé,356604 gratuit,97236 tampons KiB Swap :10474236 total,22860 utilisé,10451376 gratuit,46836580 Mémoire cache PID UTILISATEUR PR NI VIRT RES SHR S %CPU %MEM TEMPS+ COMMANDE 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.

Les résultats du travail :

Exécution d'un test de 1 m sur http://192.0.2.1:8000/1/1/1 12 threads et 50 connexions
Statistiques des threads Moyenne Écart-type Max +/- Écart-type
Latence 226,32 ms 392,76 ms 1,72 s 93,48 %
Req/Sec 20,02 10,84 59,00 65,91 %
15 045 requêtes en 1,00 m, 58,86 Go en lecture
Requêtes/sec :    250,57
Transfert/sec :      0,98 Go

Le temps moyen de traitement d'un fichier de 4 Mo a été réduit de 7,42 secondes à 226,32 millisecondes (33 fois moins), et le nombre de requêtes par seconde a été multiplié par 31 (250 contre 8) !

L'explication est que nos requêtes n'attendent plus dans la file d'attente des événements pour être traitées pendant que les processus de travail sont bloqués en lecture, mais sont traitées par des threads libres. Tant que le sous-système de disque fait son travail du mieux qu'il peut en servant notre charge aléatoire à partir de la première machine cliente, NGINX utilise le reste des ressources CPU et de la capacité du réseau pour servir les requêtes du deuxième client à partir de la mémoire.

Toujours pas de solution miracle

Après toutes nos craintes concernant les opérations de blocage et quelques résultats passionnants, la plupart d’entre vous vont probablement déjà configurer des pools de threads sur vos serveurs. Ne te presse pas.

La vérité est que, heureusement, la plupart des opérations de lecture et d’envoi de fichiers ne concernent pas les disques durs lents. Si vous disposez de suffisamment de RAM pour stocker l’ensemble de données, un système d’exploitation sera suffisamment 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 de démontrer d'excellentes performances dans presque tous les cas d'utilisation courants. La lecture à partir du cache de pages est assez rapide et personne ne peut qualifier de telles opérations de « bloquantes ». D'un autre côté, le déchargement vers un pool de threads entraîne une certaine surcharge.

Donc, si vous disposez d’une quantité raisonnable de RAM et que votre ensemble de données de travail n’est pas très volumineux, alors NGINX fonctionne déjà de la manière la plus optimale sans utiliser de pools de threads.

Le déchargement des opérations de lecture vers le pool de threads est une technique applicable à des tâches très spécifiques. Il est particulièrement utile lorsque le volume de contenu fréquemment demandé ne rentre pas dans le cache de la machine virtuelle du système d’exploitation. Cela pourrait être le cas, par exemple, avec un serveur de streaming multimédia basé sur NGINX fortement chargé. C’est la situation que nous avons simulée dans notre benchmark.

Ce serait formidable si nous pouvions améliorer le déchargement des opérations de lecture dans les pools de threads. Tout ce dont nous avons besoin est un moyen efficace de savoir si les données du fichier nécessaires sont en mémoire ou non, et seulement dans ce dernier cas, l'opération de lecture doit être déchargée sur un thread séparé.

Pour revenir à notre analogie commerciale, le vendeur ne peut actuellement pas savoir si l'article demandé est dans le magasin et doit soit toujours transmettre toutes les commandes au service de livraison, soit toujours les gérer lui-même.

Le problème est que les systèmes d’exploitation ne disposent pas de cette fonctionnalité. Les premières tentatives pour l'ajouter à Linux en tant qu'appel système fincore() ont eu lieu en 2010, mais cela n'a pas eu lieu. Plus tard, il y a eu un certain nombre de tentatives pour l'implémenter en tant que nouvel appel système preadv2() avec l'indicateur RWF_NONBLOCK (voir Opérations de lecture de fichiers en mémoire tampon non bloquantes et Opérations de lecture en mémoire tampon asynchrones sur LWN.net pour plus de détails). Le sort de tous ces patchs n’est toujours pas clair. Le point triste ici est qu'il semble que la principale raison pour laquelle ces correctifs n'ont pas encore été acceptés dans le noyau soit le bikeshedding continu.

En revanche, les utilisateurs de FreeBSD n’ont aucune raison de s’inquiéter. FreeBSD dispose déjà d'une interface asynchrone suffisamment bonne pour la lecture de fichiers, que vous devriez utiliser à la place des pools de threads.

Configuration des pools de threads

Donc, si vous êtes sûr de pouvoir tirer profit de l’utilisation de pools de threads dans votre cas d’utilisation, il est temps de plonger dans la configuration.

La configuration est assez simple et flexible. La première chose que vous devriez avoir est la version NGINX 1.7.11 ou ultérieure, compilée avec l'argument --with-threads de la commande configure . Les utilisateurs de NGINX Plus ont besoin de la version 7 ou ultérieure. Dans le cas le plus simple, la configuration semble très simple. Il vous suffit d’inclure la directive aio threads dans le contexte approprié :

# dans les threads contextuels « http », « serveur » ou « emplacement » ;

Il s'agit de la configuration minimale possible des pools de threads. En fait, c'est une version courte de la configuration suivante :

# dans le contexte « main » thread_pool par défaut threads=32 max_queue=65536 ;

# dans le contexte « http », « serveur » ou « emplacement »
aio threads=default ;

Il définit un pool de threads appelé par défaut avec 32 threads de travail et une longueur maximale pour la file d'attente des tâches de 65 536 tâches. Si la file d'attente des tâches est surchargée, NGINX rejette la demande et enregistre cette erreur :

Dépassement de file d'attente du pool de threads « NAME » : N tâches en attente

L’erreur signifie qu’il est possible que les threads ne soient pas en mesure de gérer le travail aussi rapidement qu’il est ajouté à 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 capable de traiter autant de demandes.

Comme vous l'avez déjà remarqué, avec la directive thread_pool, vous pouvez configurer le nombre de threads, la longueur maximale de la file d'attente et le nom d'un pool de threads spécifique. La dernière implique que vous pouvez configurer plusieurs pools de threads indépendants et les utiliser à différents endroits de votre fichier de configuration pour servir à différentes fins :

# dans le contexte 'main'
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 gérer qu'autant de tâches qu'il y a de threads configurés ; aucune tâche n'attendra dans la file d'attente.

Imaginons maintenant que vous disposez d’un serveur avec trois disques durs et que vous souhaitez que ce serveur fonctionne comme un « proxy de mise en cache » qui met en cache toutes les réponses de vos backends. La quantité attendue de données mises en cache dépasse de loin la RAM disponible. Il s’agit en fait d’un nœud de mise en cache pour votre CDN personnel. Bien entendu, dans ce cas, le plus important est d'obtenir des performances maximales des disques.

L’une de vos options est de configurer une matrice RAID. Cette approche a ses avantages et ses inconvénients. Maintenant avec NGINX vous pouvez en prendre un autre :

# Nous supposons que chacun des disques durs est monté sur l'un de ces répertoires :# /mnt/disk1, /mnt/disk2 ou /mnt/disk3

# dans le contexte « main »
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 ;
chemin_cache_proxy /mnt/disk3 niveaux=1:2 zones_clés=cache_3:256m taille_max=1024G 
use_temp_path=off;

split_clients $request_uri $disk {
33.3% 1;
33.3% 2;
* 3;
}

serveur {
# ...
emplacement / {
proxy_pass http://backend;
clé_cache_proxy $request_uri;
cache_cache_proxy_$disk;
threads aio=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, et les directives proxy_cache_path définissent un cache dédié et indépendant sur chaque disque.

Le module split_clients est utilisé pour équilibrer la charge entre les caches (et par conséquent entre les disques), ce qui correspond 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 où se trouvent les données de cache correspondantes. Il est nécessaire d'éviter de copier les données de réponse entre les disques durs lors de la mise à jour de nos caches.

L'ensemble de ces éléments nous permet d'obtenir des performances maximales du sous-système de disque actuel, car NGINX, via des pools de threads séparés, interagit avec les lecteurs en parallèle et indépendamment. Chacun des lecteurs est desservi par 16 threads indépendants avec 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. Assurez-vous que vos disques durs l'apprécient également.

Cet exemple est une bonne démonstration de la flexibilité avec laquelle NGINX peut être réglé spécifiquement pour votre matériel. C’est comme si vous donniez des instructions à NGINX sur la meilleure façon d’interagir avec la machine et votre ensemble de données. Et en ajustant NGINX dans l’espace utilisateur, vous pouvez garantir que votre logiciel, votre système d’exploitation et votre matériel fonctionnent ensemble dans le mode le plus optimal pour utiliser toutes les ressources système aussi efficacement que possible.

Conclusion

En résumé, les pools de threads sont une fonctionnalité formidable qui pousse NGINX vers de nouveaux niveaux de performances en éliminant l’un de ses ennemis bien connus et de longue date : le blocage, en particulier lorsque nous parlons de très gros volumes de contenu.

Et il y a encore plus à venir. Comme mentionné précédemment, cette toute nouvelle interface permet potentiellement de décharger toute opération longue et bloquante sans aucune perte de performances. NGINX ouvre de nouveaux horizons en termes de masse de nouveaux modules et fonctionnalités. De nombreuses bibliothèques populaires ne proposent toujours pas d'interface asynchrone non bloquante, ce qui les rendait auparavant incompatibles avec NGINX. Nous pouvons consacrer beaucoup de temps et de ressources au développement de notre propre prototype non bloquant d'une bibliothèque, mais cela en vaudra-t-il toujours la peine ? Désormais, avec les pools de threads intégrés, il est possible d'utiliser de telles bibliothèques relativement facilement, créant de tels modules sans impact sur les performances.

Restez à l'écoute.

Essayez vous-même les pools de threads dans NGINX Plus – démarrez votre essai gratuit de 30 jours dès aujourd'hui ou contactez-nous pour discuter de vos cas d'utilisation .


« Cet article de blog peut faire référence à des produits qui ne sont plus disponibles et/ou qui ne sont plus pris en charge. Pour obtenir les informations les plus récentes sur les produits et solutions F5 NGINX disponibles, explorez notre famille de produits NGINX . NGINX fait désormais partie de F5. Tous les liens NGINX.com précédents redirigeront vers un contenu NGINX similaire sur F5.com."