Owen Garret, responsable produit chez Nginx, Inc., a décrit sur leur blog quels choix de conception permettent à NGINX de proposer les meilleurs résultats en matière de performance et de scalabilité.
L’architecture globale de NGINX est caractérisée par un ensemble de processus coopératifs :
- Le processus maître, responsable de réaliser les opérations soumises à privilège, telles que la lecture des fichiers de configuration, contrôler les sockets ou créer et envoyer des signaux aux processus enfants.
- Les processus de travail (workers), responsables de recevoir les connexions et d’y répondre, lire et écrire sur le disque et communiquer avec les serveurs en amont. Ce sont les seuls processus occupés lorsque NGINX est actif.
- Le processus de chargement de cache, responsable de charger le cache disque en mémoire. Ce processus est exécuté au lancement, puis s’arrête.
- Le gestionnaire de cache, responsable d’éliminer les entrées des caches disques, pour que ceux-ci ne dépassent pas leurs limites. Celui-ci est exécuté de façon périodique.
La clé de la performance et la scalabilité de NGNIX réside en deux points de design fondamentaux :
- Le nombre de workers est contraint, pour minimiser le context switching. La configuration par défaut et recommandée est d’utiliser un processus de travail par core CPU pour une utilisation efficace des ressources matérielles.
- Les workers sont à thread unique et prennent en charge toutes les connexions de façon non-bloquante.
Chaque processus de travail prend en charge de multiples connexions, via une machine à état implémentée de façon non-bloquante :
- Un worker a un certain nombre de sockets à prendre en charge, ce sont soit des sockets d’écoute, soit des sockets de connexion.
- Quand une nouvelle requête entre sur un socket d’écoute, un nouveau socket de connexion est ouvert pour prendre en charge la communication avec le client à l’origine de la requête.
- Quand un nouvel événement arrive sur un socket de connexion, le worker y répond le plus rapidement possible et passe au traitement de l’événement suivant arrivant sur n’importe quel socket.
Pour Garrett, les décisions de conception prises pour NGINX en font une solution radicalement différente des autres serveurs Web, qui optent généralement pour un modèle où à chaque connexion est assigné un thread séparé. Avec ce modèle, il est très facile de prendre en charge de multiples connexions, puisque chacune peut être vue comme une séquence linéaire d’étapes. Cependant, cela se paie en context switching. En effet, les workers passent le plus clair de leur temps bloqués à attendre le client ou un autre serveur en amont. Le prix du context switching devient non négligeable lorsque le nombre de connexions simultanées voulant par exemple exécuter des opérations d’I/O passe un certain seuil, ou lorsque l’on arrive à la limite de la mémoire disponible.
Le modèle NGINX, lui, permet que les workers ne soient jamais bloqués, à moins qu’il n’y ait pas de travail à effectuer. De plus, chaque nouvelle connexion consomme très peu de ressources, uniquement un descripteur de fichier et une petite part de mémoire dans le processus du worker.
Globalement, ceci permet à NGINX de prendre en charge des centaines de milliers de connexions HTTP concurrentes par worker, avec le paramétrage système approprié.