Si vous avez suivi notre guide Node.js, vous savez que Node repose sur trois bases fondamentales : le moteur JavaScript V8 de Google, une API de bas niveau nommée libuv et une boucle d’événement plus communément appelée event loop. Ces trois éléments fonctionnent en complémentarité pour permettre de traiter les entrées et les sorties d’une façon innovante, non-bloquante et asynchrone — ce qui est la caractéristique principale de Node.
Alors que le moteur V8 est dédié à l’interprétation du JavaScript, l’API fournie par la librairie libuv permet aux développeurs Node d’accéder à l’event loop. Cette dernière est par ailleurs distincte de la boucle d’événement interne aux navigateurs (JavaScript event loop) et qui leur permet d’exécuter le JS.
Lorsque l’on exécute un programme sur son ordinateur, on crée une instance liée à ce programme. À celle-ci peut être attachée un ou plusieurs threads (ou fil d’exécution), qui correspondent à une suite d’opérations à faire réaliser par le processeur (CPU). La gestion et l’optimisation des threads est gérée par le système d’exploitation, qui leur alloue les ressources disponibles au bon moment.
Lorsque l’on exécute un programme Node.js, un seul thread est créé. C’est dans celui-ci que l’ensemble du code sera traité. À l’intérieur de ce fil d’exécution est générée la boucle d’événement ou event loop qui va décider quelle opération doit être exécutée et à quel moment. Pour cela, l’event loop communique avec le noyau (ou kernel) du système d’exploitation. Les noyaux modernes étant multi-threaded, ils peuvent exécuter plusieurs opérations en arrière-plan. Quand l’une d’elles est terminée, le kernel renvoie à Node.js le callbackcorrespondant (la fonction à exécuter à la suite de l’opération) pour être exécutée plus tard dans la boucle.
L’event loop accessible via libuv permet ainsi de lire chaque ligne de code, de l’interpréter, et d’en créer un événement à faire exécuter par le thread. Chaque événement est ainsi stocké dans une file d’attente appelée Event queue. En Node, il existe en réalité plusieurs types d’event queues qui correspondent à différents types d’événements. Ces différentes files d’attente sont traitées à des moments distincts, en plusieurs étapes successives qui constituent les phases de l’event loop.
Quand on lance une application Node, l’event loop est initialisée avant que le code soit interprété. Après l’interprétation, l’event loop est démarrée. Chaque itération (appelée tick) de la boucle passe par différentes phases, mais un tick ne peut démarrer que si des opérations sont en attente dans une event queue.
Illustration de l’event loop NodeJS
Une fois une itération commencée et qu’on rentre donc à l’intérieur d’un tick, l’event loop passe successivement par les étapes suivantes.
Un timer spécifie un délai après lequel un callback sera exécuté. Ce délai peut être allongé, par exemple si des tâches réalisées par le système d’exploitation ou l’exécution d’autres callbacks empêchent l’exécution du callback du timer à temps. Lorsque le délai du timer est dépassé, le callback de celui-ci est placé dans une event queue, signifiant à Node qu’il est prêt à être exécuté.
Lorsqu’une itération de l’event loop est lancée, elle débute par cette première phase dans laquelle Node va exécuter les callbacks de timers prêts à être exécutés et placés en attente suite à l’expiration d’un setTimeout() ou d’un setInterval() — deux fonctions pour spécifier un délai en JavaScript.
Si chaque tick de l’event loop commence par cette phase, c’est pour s’assurer qu’un callback de timer soit exécuté le plus rapidement possible après sa mise en attente.
Quand tous ces callbacks de timers en attente sont exécutés (ou s’il n’y en a pas à exécuter), l’event loop passe à sa deuxième phase.
De la même façon que pour la première phase, Node va venir maintenant chercher des callbacks en attente dans une event queue pour les exécuter.
Cette fois, elle va chercher dans la file d’attente réservée aux callbacks d’opérations système comme les fonctions Node filesystem ou les erreurs TCP par exemple.
La boucle d’événement passe ensuite à la phase suivante, toujours à l’intérieur du même tick.
La troisième étape de l’event loop Node est la poll phase ou phase d’interrogation.Cette phase est plus complexe : durant celle-ci, Node va bloquer l’event loop et attendre de nouveaux événements pour les exécuter. Pour cela, Node va d’abord calculer la durée du blocage puis traiter les événements présents dans la poll queue (tous ceux non attribués dans les autres files d’attente).
Si la boucle d’événement entre dans la poll phase alors qu’aucun timer n’est planifié, deux possibilités existent :
Quand on veut exécuter un script de façon asynchrone mais le plus rapidement possible, une option possible en Node est d’utiliser la fonction setImmediate(). N’importe quelle fonction passée en argument de setImmediate() est un callback à exécuter dans la prochaine itération de l’event loop.
C’est dans cette quatrième étape, la phase de vérification ou check phase que Node vient exécuter les événements de cette event queue. Ainsi, si la boucle est bloquée à la phase 3 et que des scripts ont été planifiés avec `setImmediate()`, l’event loop poursuit jusqu’à la phase 4 plutôt qu’elle ne continue d’attendre.
Lorsqu’un événement de type ‘close’ est émis, par exemple lorsqu’un socket est fermé avec socket.destroy(), il est placé dans l’event queue dédiée à cette étape. Lorsque la boucle atteint cette dernière phase, Node vient chercher les événements dans cette file d’attente et les émettre. Cette étape permet de “nettoyer” l’état de notre application avant de débuter une nouvelle itération de l’event loop.
Au-delà de ces cinq phases de l’event loop Node.js, qui correspondent donc à cinq event queues différentes, s’ajoutent deux files d’attente supplémentaires qui ne font pas partie de libuv mais qui sont implémentées par Node. On peut les qualifier de phases intermédiaires :
setImmediate()
ou setInterval()
par exemple. Au contraire, un script appelé par process.nextTick()
est stocké dans la nextTickQueue. Les événements présents dans cette file d’attente sont systématiquement traités juste après l’opération en cours, peu importe la phase à laquelle se trouve l’event loop. Qu’importe le moment où process.nextTick()
est appelé, l’event loop est bloquée le temps de l’exécuter, ce qui peut causer des comportements inattendus telle que l’impossibilité pour la boucle d’atteindre la poll phase.En tant que l’un des trois fondements de Node.js, la connaissance et la maîtrise de l’event loop et des contraintes qui lui sont associées sont indispensables pour réussir une application Node. Une mauvaise utilisation de certaines fonctions peut complètement bloquer l’event loop et causer de nombreux bugs très difficiles à repérer et à résoudre.
En Node, on définit généralement toutes les fonctions faisant appel au système ou au réseau comme étant des fonctions I/O (pour input/output). Toutes les fonctions I/O standards de Node.js sont asynchrones : elles ne bloquent pas l’event loop et acceptent des callbacks qui sont exécutés dans les conditions vues précédemment. Certaines existent également en version synchrone, c’est-à-dire qu’elles bloquent l’event loop pendant leur exécution, l’empêchant de poursuivre vers d’autres phases et d’exécuter le reste du code.
Pour ne pas bloquer l’event loop, il ne faut pas utiliser les fonction I/O synchrones (telles que fs.readFileSync()
ou fs.renameFileSync()
par exemple) dans des blocs de code appelés à répétition telles que des boucles ou des fonctions fréquemment répétées.
La meilleure pratique est bien souvent de réaliser ces opérations pendant l’initialisation de l’app.
Une règle d’or en Node est de n’écrire que des fonctions uniquement synchrones ou uniquement asynchrones.
Si votre fonction est un hybride entre synchrone et asynchrone, elle aura des résultats irréguliers et imprévisibles. Il faut donc, pour chaque fonction que l’on crée, choisir un caractère bloquant ou non-bloquant et s’y tenir.
process.nextTick()
peut être très utile pour s’assurer qu’un script soit immédiatement exécuté. Il est cependant à utiliser avec parcimonie car son utilisation trop fréquente peut forcer l’event loop à se concentrer exclusivement sur le traitement de la nextTickQueue et la bloquer entre deux phases. Si la poll phase n’est jamais atteinte par l’event loop, le reste du code n’est pas exécuté et l’app est bloquée.
Pour les opérations I/O coûteuses en ressources, libuv maintient un thread pool qui contient plusieurs threads prêts à être utilisés en même temps. La taille de ce thread pool est limitée, par défaut à 4 threads. Cela signifie par exemple qu’au maximum 4 opérations de lecture d’un fichier sur le disque peuvent être réalisées en même temps. Les autres seront délayées le temps que les premières soient terminées et que leurs threads soient à nouveau disponibles.
Cette limite peut être augmentée en ajustant la variable d’environnement UV_THREADPOOL_SIZE qui peut être augmentée jusqu’à 128. Ce sont alors théoriquement autant d’opérations I/O qui pourront être réalisées en même temps, augmentant drastiquement les performances de l’application par rapport à la valeur par défaut, dans certains cas. Les performances sont en effet toujours relatives aux limites du système sur lequel l’application est hébergée.
Surveiller l’état de la boucle d’événement est indispensable pour empêcher des dysfonctionnements et peut être utilisé pour créer des alertes, forcer un redémarrage ou aider à scaler l’app.
Le but du monitoring de l’event loop est d’identifier des délais supplémentaires dans l’exécution d’un callback de timer. Si un timer met plus de temps que ce qu’on lui a spécifié pour s’exécuter, c’est que la boucle d’événement a été délayée. Ce délai représente le temps nécessaire pour que l’event loop exécute les autres phases. On pourrait dès lors par exemple imposer un seuil limite à partir duquel notre application serait considérée comme surchargée et appliquer automatiquement les mesures nécessaires.
Elles sont passées où les femmes dans la tech ? Entre le manque de représentation…
Dans cette vidéo, on interview Nicolas Grekas, contributeur clé de Symfony, pour discuter de sa…
Comment trouver son job dans la tech ? Marie a la réponse ! Grâce à…
Adobe, l'empire créatif, et pas des moindres ! Belle ascension de la part de ces…
Est-ce plus simple de créer des morceaux avec les outils de Musique Assistée par Ordinateur…