Scalable event-driven asynchronous non-blocking wonder
nginx (prononcer engine x) est une solution haute performance combinant un serveur web et un load balancer de niveau 7.
Open source et distribué sous licence BSD, nginx a été bâti autour des concepts de forte concurrence et de scalability. Sa très faible empreinte mémoire et son utilisation des entrées/sorties non-bloquantes en font un candidat idéal pour les environnements très sollicités nécessitant une utilisation optimale des ressources.
C'est aujourd'hui le 3e serveur web le plus populaire, derrière Apache et IIS. (source Netcraft)
nginx est un projet lancé par Igor Sysoev en 2002, il est alors administrateur système sénior chez Rambler Media Group, l'équivalent russe de Yahoo.
À cette époque les sites de RMG génèrent plusieurs centaines de milliers de pages vues chaque mois et sont servis par Apache depuis un cluster de machines. Le trafic augmentant chaque année, il apparaît évident d'investir préventivement dans de nouvelles machines pour encaisser la montée en charge.
Le coût en matériel est réduit, mais cette extension horizontale entraine aussi une augmentation de la complexité de la maintenance et donc des coûts cachés. La gestion équitable de la charge, la reprise sur erreur, le backup, la synchronisation, sont autant de problématiques compliquées par des environnement distribués.
Se pose alors la question de l'optimalité dans l'utilisation des ressources à disposition : faisons-nous au mieux avec ce que l'on a ?
En 2002, Igor Sysoev contribue régulièrement au projet Apache par le biai d'extensions pour accélérer le traitement des requêtes. C'est à cette époque qu'il réalise que le modèle preforking adopté par celui-ci encaisse mal les montées en charge à cause de processus trop nombreux qui prennent beaucoup de place en mémoire.
Se basant sur les travaux de recherche de Dan Kegel et son explication du C10K problem, Sysoev entreprend d'écrire son propre serveur web pour sortir des modèles habituels du multithreading et du preforking. Pour ce faire, il va exploiter les entrées/sorties non-bloquantes, les évènements et les communications asynchrones afin de réduire drastiquement le nombre de threads et de processus concurrents.
L'objectif avoué est d'être en mesure de servir plus de 10 000 clients simultanément sur un lien gigabit avec seulement 2 Go de RAM.
La cause principale de ralentissement dans un environnement web est l'empilement des connexions. Cette situation n'est pas provoquée par un manque de ressources côté serveur, mais par deux facteurs incontrôlables : la lenteur des clients et le nombre de connexions concurrentes que ceux-ci ouvrent.
Ces trois éléments ont pour conséquence directe d'accroître considérablement le nombre de connexions ouvertes simultanément sur un serveur : on constate alors que le problème pour un serveur web n'est pas tant de servir rapidement une page, mais de pouvoir le faire au même rythme qu'il y ait 1, 100, ou 10 000 connexions concurrentes.
En Informatique, la performance d'un système mesure en termes de vitesse, de stabilité et d'exhaustivité sa capacité a remplir ses fonctions. La scalability désigne en revanche la capacité de celui-ci à maintenir ses performances lors d'une montée en charge.
C'est sur ce dernier point que nginx se concentre, son créateur a bien compris que l'important est avant tout de maintenir le même niveau de service pour tous, peut importe le taux d'occupation.
Contrairement au reste des serveurs web qui sont basés sur l'utlisation d'un processus ou thread par requête, le parti pris de nginx est de réduire au maximum leur nombre, afin d'économiser la mémoire nécessaire au traitement et au maintient de chaque connexion.
Cela est possible grâce à l'utilisation conjointe de deux notions importantes en programmation réseau, les entrées/sorties non-bloquantes asynchrones et les évènements.
Chaque thread ou processus est ainsi en mesure de gérer plusieurs milliers de connexions à la fois, ce qui a pour effet immédiat de réduire drastiquement la place occupée en mémoire, et d'augmenter le nombre de connexions concurrentes maintenables.
nginx est divisé en deux entités : les workers qui traitent les connexions, et le master qui gère les workers et qui accepte les connexions : il n'y a qu'un seul master mais il y a plusieurs workers.
(courtoisie de Joshua Zhu)
Chaque worker gère un nombre prédéfini de connexions de bout en bout (sauf l'acceptation). Le master a pour mission principale de "lancer" les worker via un fork puis de les monitorer. Il s'occupe également de charger les directives de la configuration et d'ouvrir une socket en écoute sur l'interface définie (bind).
Le master sert aussi à contrôler le daemon, il masque tous les signaux possibles ce qui permet de lui envoyer des directives précises :
Dans n'importe quel système soumit à une forte charge, la performance peut être catastrophique à cause de quatre facteurs.
Mis à part certaines exceptions, tous les appels systèmes effectuent des copies cachées de données même si ce n'est pas leur véritable fonction. Par exemple, lors de l'envoi de données situées sur le disque vers le réseau, un schéma typique est d'employer les deux appels systèmes read et write.
Cette approche provoque non pas moins de 4 copies de données :
Afin d'éliminer les copies inutiles et de se rapprocher du Zero Copy nginx utilise par exemple sendfile qui copie directement un descripteur de fichier vers une socket, sans passer par le user space. Par souci d'optimisation, il faut éviter au maximum la duplication de données.
Un autre facteur de perte de performance est une mauvaise allocation mémoire.
Mais qu'est-ce qui définit une "bonne" allocation mémoire ? Il est nécessaire de faire la part des choses, selon ses objectifs. D'un côté il est possible d'allouer uniquement la mémoire nécessaire, ce qui implique de faire des allocations unitaires très couteuse en temps, ou de pré allouer un espace fixe, ce qui accélère le traitement mais toute la mémoire allouée ne sera pas forcément utilisée.
nginx prend le meilleur des deux mondes en utilisant une pool de mémoire pré allouée. Il exploite son propre garbage collector et les allocations mémoires unitaires se font directement dans la pool. Comme cette mémoire a déjà été allouée par le système, il n'y a pas de surcoût de temps. La quantité de mémoire nécessaire étant prévisible grâce au nombre de processus fixé à l'avance (1 master + n workers), la pool de mémoire est créé à la bonne taille lors du lancement.
Ce qui affecte le plus la performance des serveurs web se basant sur le modèle du multithreading, ce sont les changements de contexte.
Ceux-ci interviennent normalement lorsque le scheduler du système fait son travail, mais ils peuvent également être "forcés" lorsque des threads ou des processus doivent échanger des informations. Les changements de contexte sont extrêmement couteux car le système sauvegarde l'état complet du processus ou du thread pour pouvoir le restaurer.
Les processus worker de nginx gèrent chacun un certain nombre de connexions et les traitent de bout en bout après acceptation du master. Ils n'échangent aucune information entre eux, n'ont même pas connaissance de l'existence d'autres workers. De plus comme leur nombre est généralement réduit (entre 2 et 6), le scheduler va beaucoup plus vite pour les parcourir que s'il en existait un par connexion.
Dans le même ordre d'idées que les changements de contexte, les lock contentions sont également une source non négligeable de ralentissement.
Les locks sont typiquement utilisés dans des environnement multiprocessus ou multithreadés, alors que plusieurs processus ou threads peuvent accéder simultanément à la même ressource.
nginx est un serveur web qui sert des fichiers ou des ressources, il n'a donc pas à gérer l'écriture de fichiers, la seule écriture se fait sur la socket, et celle-ci n'est gérée que par un seul worker. Grâce à l'emploi des entrées/sorties non-bloquantes, les différents workers peuvent accéder aux ressources "sans attendre" et traiter d'autres requêtes en attendant que la ressource soit disponible. En d'autres termes les workers n'attendent jamais qu'une ressource se libère, ils sont prévenus de sa disponibilité pour une action particulière (lecture, écriture) et ne sont donc jamais en "pause" ou en attente active.
Évolution du nombre de requêtes servies par seconde suivant l'augmentation des connexions concurrentes (courtoisie de WebFaction)
Évolution de l'empreinte mémoire suivant l'augmentation des connexions concurrentes (courtoisie de WebFaction)
Ces deux graphiques montrent que plus le nombre de connexions concurrentes augmente, plus les performances en terme de rapidité et d'utilisation mémoire d'Apache se dégradent. Cela est notamment du à l'augmentation du nombre de threads pour gérer les connexions et donc à l'augmentation des changements de contexte et des lock contention. Ajoutons à cela que le preforking a ses limites, et qu'au delà d'une certaine valeur il est nécessaire d'allouer de la mémoire supplémentaire non prévue au départ.
Le principal problème du modèle basé sur les processus et ou les threads vient de la non prédictabilité des ressources qui seront nécessaires pour traiter un nombre n de requêtes.
On constate que nginx répond à un vrai besoin des entreprises qui recherchent non seulement une solution rapide et fiable, mais surtout scalable et capable d'encaisser de très fortes montées en charge. Son approche résolument tournée vers les nouvelles entrées sorties et la programmation évènementielle sont la preuve qu'un autre modèle que celui du multithreading existe, et qu'il possède beaucoup d'avantages.
Toutefois, revers de la médaille, nginx est beaucoup moins versatile qu'Apache. Pas de support de langage dynamique ou de chargement d'extension à la volée, pas de .htaccess non plus.
Cette relative rigidité dans la configuration et l'exploitation en font un candidat plus que déconseillé pour les environnements hétérogènes. Typiquement, les machines mutualisées font fonctionner plusieurs centaines de de sites internet, d'applications web, écrits dans des langages dynamiques différents et avec des besoins spécifiques (URL Rewriting, Authentification, etc.). La configuration "éclatée" d'Apache est vitale pour ce genre d'écosystème.
Il est également bon de garder à l'esprit qu'aucune solution technique n'est applicable systématiquement, et qu'il convient d'étudier au préalable la viabilité de celle-ci dans un certain type d'environnement pour remplir des objectifs précis. Ainsi, nginx a été pensé pour gérer des centaines de milliers de connexions concurrentes, c'est pourquoi il est de plus en plus utilisé en remplacement d'Apache pour les sites à très fort trafic.