Comment Node.js est-il intrinsèquement plus rapide lorsqu'il repose toujours sur des threads en interne?

281

Je viens de regarder la vidéo suivante: Introduction à Node.js et je ne comprends toujours pas comment vous obtenez les avantages de la vitesse.

Principalement, à un moment donné, Ryan Dahl (créateur de Node.js) dit que Node.js est basé sur la boucle d'événements plutôt que sur les threads. Les threads sont chers et ne doivent être laissés qu'aux experts de la programmation simultanée pour être utilisés.

Plus tard, il montre ensuite la pile d'architecture de Node.js qui a une implémentation C sous-jacente qui a son propre pool de threads en interne. Donc, évidemment, les développeurs de Node.js ne lanceraient jamais leurs propres threads ou n'utiliseraient pas directement le pool de threads ... ils utilisent des rappels asynchrones. Je comprends cela.

Ce que je ne comprends pas, c'est que Node.js utilise toujours des threads ... il cache simplement l'implémentation alors comment est-ce plus rapide si 50 personnes demandent 50 fichiers (pas actuellement en mémoire) et que 50 threads ne sont pas nécessaires ?

La seule différence étant que, puisqu'il est géré en interne, le développeur Node.js n'a pas à coder les détails des threads, mais en dessous, il utilise toujours les threads pour traiter les demandes de fichiers IO (blocage).

Donc, vous ne prenez pas vraiment un seul problème (threading) et le cachez pendant que ce problème existe toujours: principalement plusieurs threads, changement de contexte, verrous mortels ... etc?

Il doit y avoir quelques détails que je ne comprends toujours pas ici.

Ralph Caraveo
la source
14
Je suis enclin à convenir avec vous que la demande est quelque peu simplifiée. Je crois que l'avantage de performance du nœud se résume à deux choses: 1) les threads réels sont tous contenus à un niveau assez bas, et restent donc limités en taille et en nombre, et la synchronisation des threads est ainsi simplifiée; 2) La «commutation» au niveau du système d'exploitation via select()est plus rapide que les échanges de contexte de thread.
Pointy
Veuillez consulter ce stackoverflow.com/questions/24796334/…
veritas

Réponses:

140

Il y a en fait plusieurs choses différentes qui se confondent ici. Mais cela commence par le mème que les threads sont vraiment très durs. Donc, s'ils sont difficiles, vous êtes plus susceptible, lorsque vous utilisez des threads, 1) de casser en raison de bogues et 2) de ne pas les utiliser aussi efficacement que possible. (2) est celui dont vous parlez.

Pensez à l'un des exemples qu'il donne, où une demande arrive et vous exécutez une requête, puis faites quelque chose avec les résultats. Si vous l'écrivez d'une manière procédurale standard, le code pourrait ressembler à ceci:

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

Si la demande entrant vous a amené à créer un nouveau thread qui a exécuté le code ci-dessus, vous aurez un thread assis là, ne faisant rien du tout pendant l' query()exécution. (Apache, selon Ryan, utilise un seul thread pour satisfaire la demande d'origine alors que nginx le surpasse dans les cas dont il parle parce que ce n'est pas le cas.)

Maintenant, si vous étiez vraiment intelligent, vous exprimeriez le code ci-dessus d'une manière où l'environnement pourrait s'éteindre et faire autre chose pendant que vous exécutez la requête:

query( statement: "select smurfs from some_mushroom", callback: go_do_something_with_result() );

C'est essentiellement ce que fait node.js. Vous décorez fondamentalement - d'une manière qui est pratique en raison du langage et de l'environnement, d'où les points sur les fermetures - votre code de telle manière que l'environnement puisse être intelligent sur ce qui fonctionne et quand. De cette façon, node.js n'est pas nouveau dans le sens où il a inventé les E / S asynchrones (pas que quelqu'un ait réclamé quelque chose comme ça), mais il est nouveau en ce sens que la façon dont il est exprimé est un peu différente.

Remarque: quand je dis que l'environnement peut être intelligent sur ce qui s'exécute et quand, en particulier ce que je veux dire, c'est que le thread utilisé pour démarrer certaines E / S peut maintenant être utilisé pour gérer une autre demande ou un calcul qui peut être effectué en parallèle ou démarrez une autre E / S parallèle. (Je ne suis pas certain que le nœud soit suffisamment sophistiqué pour commencer plus de travail pour la même demande, mais vous avez l'idée.)

jrtipton
la source
6
D'accord, je peux certainement voir comment cela peut augmenter les performances, car il me semble que vous êtes en mesure de maximiser votre processeur, car il n'y a pas de threads ou de piles d'exécution attendant simplement le retour d'E / S, ce que Ryan a fait est effectivement trouvé un moyen de combler toutes les lacunes.
Ralph Caraveo
34
Oui, la seule chose que je dirais, c'est que ce n'est pas comme s'il avait trouvé un moyen de combler les lacunes: ce n'est pas un nouveau modèle. Ce qui est différent, c'est qu'il utilise Javascript pour permettre au programmeur d'exprimer son programme d'une manière beaucoup plus pratique pour ce type d'asynchronie. Peut-être un détail épineux, mais quand même ...
jrtipton
16
Il convient également de souligner que pour de nombreuses tâches d'E / S, Node utilise les API d'E / S asynchrones au niveau du noyau qui sont disponibles (epoll, kqueue, / dev / poll, peu importe)
Paul
7
Je ne suis toujours pas sûr de bien le comprendre. Si nous considérons qu'à l'intérieur d'une opération Web, les opérations d'E / S sont celles qui prennent le plus de temps pour traiter la demande et si pour chaque opération d'E / S un nouveau thread est créé, alors pour 50 demandes qui arrivent dans une succession très rapide, nous allons ont probablement 50 threads exécutés en parallèle et exécutant leur partie IO. La différence avec les serveurs Web standard est que là-dedans, la requête entière est exécutée sur le thread, tandis que dans node.js juste sa partie IO, mais c'est la partie qui prend la plupart du temps et fait attendre le thread.
Florin Dumitrescu
13
@SystemParadox merci de l'avoir signalé. J'ai fait des recherches sur le sujet récemment et le problème est que les E / S asynchrones, lorsqu'elles sont correctement implémentées au niveau du noyau, n'utilisent pas de threads lors des opérations d'E / S asynchrones. Au lieu de cela, le thread appelant est libéré dès qu'une opération d'E / S est lancée et qu'un rappel est exécuté lorsque l'opération d'E / S est terminée et qu'un thread est disponible pour cela. Ainsi, node.js peut exécuter 50 demandes simultanées avec 50 opérations d'E / S en (presque) parallèle en utilisant un seul thread si la prise en charge asynchrone des opérations d'E / S est correctement implémentée.
Florin Dumitrescu
32

Remarque! Ceci est une vieille réponse. Bien que cela soit toujours vrai dans l'esquisse, certains détails peuvent avoir changé en raison du développement rapide de Node au cours des dernières années.

Il utilise des threads car:

  1. L' option O_NONBLOCK de open () ne fonctionne pas sur les fichiers .
  2. Il existe des bibliothèques tierces qui n'offrent pas d'E / S non bloquantes.

Pour simuler les E / S non bloquantes, les threads sont nécessaires: effectuez le blocage des E / S dans un thread séparé. C'est une solution laide et cause beaucoup de frais généraux.

C'est encore pire au niveau matériel:

  • Avec DMA, le CPU décharge de manière asynchrone les E / S.
  • Les données sont transférées directement entre le périphérique IO et la mémoire.
  • Le noyau encapsule cela dans un appel système synchrone et bloquant.
  • Node.js encapsule l'appel système de blocage dans un thread.

C'est tout simplement stupide et inefficace. Mais ça marche au moins! Nous pouvons profiter de Node.js car il cache les détails laids et encombrants derrière une architecture asynchrone pilotée par les événements.

Peut-être que quelqu'un implémentera O_NONBLOCK pour les fichiers à l'avenir? ...

Edit: j'en ai discuté avec un ami et il m'a dit qu'une alternative aux threads était d'interroger avec select : spécifiez un timeout de 0 et faites des IO sur les descripteurs de fichiers retournés (maintenant qu'ils sont garantis de ne pas bloquer).

nalply
la source
Et Windows?
Pacerier
Désolé, aucune idée. Je sais seulement que libuv est la couche neutre sur la plate-forme pour effectuer un travail asynchrone. Au début de Node, il n'y avait pas de libuv. Ensuite, il a été décidé de séparer libuv et cela a facilité le code spécifique à la plate-forme. En d'autres termes, Windows a sa propre histoire asynchrone qui pourrait être complètement différente de Linux, mais pour nous, cela n'a pas d'importance parce que libuv fait le travail dur pour nous.
1er
28

Je crains que je «fasse la mauvaise chose» ici, si c'est le cas, supprimez-moi et je m'excuse. En particulier, je ne vois pas comment je crée les petites annotations soignées que certains ont créées. Cependant, j'ai beaucoup de préoccupations / observations à faire sur ce fil.

1) L'élément commenté dans le pseudo-code dans l'une des réponses populaires

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

est essentiellement faux. Si le thread est informatique, alors ce n'est pas un coup de pouce, il fait le travail nécessaire. Si, d'un autre côté, il attend simplement la fin des E / S, alors il n'utilise pas le temps CPU, tout l'intérêt de l'infrastructure de contrôle des threads dans le noyau est que le CPU trouvera quelque chose d'utile à faire. La seule façon de "tourner les pouces" comme suggéré ici serait de créer une boucle d'interrogation, et personne qui a codé un vrai serveur Web n'est suffisamment incompétent pour le faire.

2) "Les threads sont durs", n'a de sens que dans le contexte du partage de données. Si vous avez des threads essentiellement indépendants, comme c'est le cas lors de la gestion des requêtes Web indépendantes, alors le threading est trivialement simple, il vous suffit de coder le flux linéaire de la façon de gérer un travail et de rester assis sachant qu'il gérera plusieurs demandes, et chacun sera effectivement indépendant. Personnellement, je me risquerais à dire que pour la plupart des programmeurs, l'apprentissage du mécanisme de fermeture / rappel est plus complexe que le simple codage de la version de thread de haut en bas. (Mais oui, si vous devez communiquer entre les threads, la vie devient très dure très vite, mais je ne suis pas convaincu que le mécanisme de fermeture / rappel change vraiment cela, il restreint simplement vos options, car cette approche est toujours réalisable avec les threads Quoi qu'il en soit,

3) Jusqu'à présent, personne n'a présenté de preuve réelle pour expliquer pourquoi un type particulier de changement de contexte prendrait plus ou moins de temps que tout autre type. Mon expérience dans la création de noyaux multitâches (à petite échelle pour les contrôleurs intégrés, rien de si sophistiqué qu'un "vrai" OS) suggère que ce ne serait pas le cas.

4) Toutes les illustrations que j'ai vues à ce jour qui prétendent montrer à quel point Node est plus rapide que les autres serveurs Web sont horriblement imparfaites, cependant, elles sont imparfaites d'une manière qui illustre indirectement un avantage que j'accepterais certainement pour Node (et c'est nullement insignifiant). Le nœud ne semble pas avoir besoin (ni même permis, en fait) de réglage. Si vous avez un modèle fileté, vous devez créer suffisamment de threads pour gérer la charge attendue. Faites-le mal et vous vous retrouverez avec de mauvaises performances. S'il y a trop peu de threads, alors le CPU est inactif, mais incapable d'accepter plus de requêtes, de créer trop de threads, et vous gaspillerez la mémoire du noyau, et dans le cas d'un environnement Java, vous gaspillerez également la mémoire de tas principale . Maintenant, pour Java, le gaspillage de tas est le premier, le meilleur, moyen de bousiller les performances du système, parce que la collecte efficace des ordures (actuellement, cela pourrait changer avec G1, mais il semble que le jury soit encore sur ce point au début de 2013 au moins) dépend du fait d'avoir beaucoup de tas de rechange. Donc, il y a le problème, ajustez-le avec trop peu de threads, vous avez des processeurs inactifs et un débit médiocre, ajustez-le avec trop de threads, et cela s'embourbe d'autres façons.

5) Il y a une autre manière d'accepter la logique de l'affirmation selon laquelle l'approche de Node "est plus rapide par conception", et c'est celle-ci. La plupart des modèles de threads utilisent un modèle de changement de contexte à tranches de temps, superposé au modèle préemptif plus approprié (alerte de jugement de valeur :) et plus efficace (pas de jugement de valeur). Cela se produit pour deux raisons, premièrement, la plupart des programmeurs ne semblent pas comprendre la préemption de priorité, et deuxièmement, si vous apprenez le filetage dans un environnement Windows, le découpage du temps est là, que cela vous plaise ou non (bien sûr, cela renforce le premier point ; notamment, les premières versions de Java utilisaient la préemption de priorité sur les implémentations Solaris et le découpage du temps dans Windows. Parce que la plupart des programmeurs ne comprenaient pas et se plaignaient que "le threading ne fonctionne pas dans Solaris" ils ont changé le modèle en tranches de temps partout). Quoi qu'il en soit, l'essentiel est que le découpage temporel crée des changements de contexte supplémentaires (et potentiellement inutiles). Chaque changement de contexte prend du temps CPU, et ce temps est effectivement supprimé du travail qui peut être fait sur le vrai travail à accomplir. Cependant, le temps investi dans le changement de contexte en raison du découpage en temps ne devrait pas dépasser un très faible pourcentage du temps global, à moins que quelque chose d'assez étrange ne se produise, et il n'y a aucune raison pour que je m'attende à ce que ce soit le cas dans un serveur Web simple). Donc, oui, les changements de contexte excessifs impliqués dans le découpage du temps sont inefficaces (et cela ne se produit pas dans et ce temps est effectivement supprimé du travail qui peut être fait sur le vrai travail à accomplir. Cependant, le temps investi dans le changement de contexte en raison du découpage en temps ne devrait pas dépasser un très faible pourcentage du temps global, à moins que quelque chose d'assez étrange ne se produise, et il n'y a aucune raison pour que je m'attende à ce que ce soit le cas dans un serveur Web simple). Donc, oui, les changements de contexte excessifs impliqués dans le découpage du temps sont inefficaces (et cela ne se produit pas dans et ce temps est effectivement supprimé du travail qui peut être fait sur le vrai travail à accomplir. Cependant, le temps investi dans le changement de contexte en raison du découpage en temps ne devrait pas dépasser un très faible pourcentage du temps global, à moins que quelque chose d'assez étrange ne se produise, et il n'y a aucune raison pour que je m'attende à ce que ce soit le cas dans un serveur Web simple). Donc, oui, les changements de contexte excessifs impliqués dans le découpage du temps sont inefficaces (et cela ne se produit pas dansthreads du noyau en règle générale, btw), mais la différence sera de quelques pour cent du débit, et non du type de facteurs de nombre entier qui sont impliqués dans les revendications de performances qui sont souvent impliquées pour Node.

Quoi qu'il en soit, toutes mes excuses pour tout cela étant long et variable, mais je pense vraiment que jusqu'à présent, la discussion n'a rien prouvé, et je serais heureux d'entendre quelqu'un dans l'une ou l'autre de ces situations:

a) une véritable explication de la raison pour laquelle Node devrait être meilleur (au-delà des deux scénarios que j'ai décrits ci-dessus, dont le premier (mauvais réglage) je pense est la vraie explication de tous les tests que j'ai vus jusqu'à présent. ], en fait, plus j'y pense, plus je me demande si la mémoire utilisée par un grand nombre de piles peut être importante ici. Les tailles de pile par défaut pour les threads modernes ont tendance à être assez énormes, mais la mémoire allouée par un système d'événement basé sur la fermeture ne serait que ce qui est nécessaire)

b) une véritable référence qui donne en fait une chance équitable au serveur fileté de choix. Au moins de cette façon, je devrais cesser de croire que les affirmations sont essentiellement fausses;> ([modifier] c'est probablement plus fort que je ne le pensais, mais je pense que les explications données pour les avantages de performance sont au mieux incomplètes, et la les repères indiqués ne sont pas raisonnables).

À la vôtre, Toby

Toby Eggitt
la source
2
Un problème avec les threads: ils ont besoin de RAM. Un serveur très occupé peut exécuter jusqu'à quelques milliers de threads. Node.js évite les threads et est donc plus efficace. L'efficacité n'est pas en exécutant le code plus rapidement. Peu importe si le code est exécuté dans des threads ou dans une boucle d'événements. Pour le CPU, c'est pareil. Mais en supprimant les threads, nous économisons de la RAM: une seule pile au lieu de quelques milliers de piles. Et nous enregistrons également les changements de contexte.
nalply
3
Mais le nœud ne supprime pas les threads. Il les utilise toujours en interne pour les tâches d'E / S, ce dont la plupart des demandes Web ont besoin.
levi
1
Le nœud stocke également les fermetures de rappels dans la RAM, donc je ne peux pas voir où il gagne.
Oleksandr Papchenko
@levi Mais nodejs n'utilise pas le genre de “un thread par requête”. Il utilise un pool de threads IO, probablement pour éviter la complication avec l'utilisation des API IO asynchrones (et peut-être que POSIX open()ne peut pas être rendu non bloquant?). De cette façon, il amortit tout impact sur les performances où le modèle traditionnel fork()/ pthread_create()sur demande devrait créer et détruire des threads. Et, comme mentionné dans le post-scriptum a), cela amortit également le problème d'espace de pile. Vous pouvez probablement servir des milliers de demandes avec, disons, 16 threads IO très bien.
binki
"Les tailles de pile par défaut pour les threads modernes ont tendance à être assez énormes, mais la mémoire allouée par un système d'événements basé sur la fermeture ne serait que ce qui est nécessaire" J'ai l'impression que celles-ci devraient être du même ordre. Les fermetures ne sont pas bon marché, le runtime devra conserver en entier l'arborescence des appels de l'application monothread ("émuler des piles" pour ainsi dire) et pourra nettoyer quand une feuille d'arbre sera libérée comme fermeture associée est "résolu". Cela comprendra de nombreuses références à des éléments sur le tas qui ne peuvent pas être récupérés et qui affecteront les performances au moment du nettoyage.
David Tonhofer
14

Ce que je ne comprends pas, c'est que Node.js utilise toujours des threads.

Ryan utilise des threads pour les parties qui bloquent (La plupart de node.js utilise des E / S non bloquantes) parce que certaines parties sont follement difficiles à écrire non bloquantes. Mais je crois que Ryan souhaite que tout soit non bloquant. Sur la diapositive 63 (conception interne), vous voyez que Ryan utilise libev (bibliothèque qui résume les notifications d'événements asynchrones) pour la boucle d' événements non bloquante . En raison de la boucle d'événements, node.js a besoin de moins de threads, ce qui réduit le changement de contexte, la consommation de mémoire, etc.

Alfred
la source
11

Les threads sont utilisés uniquement pour traiter des fonctions n'ayant pas de fonctionnalité asynchrone, comme stat().

La stat()fonction est toujours bloquante, donc node.js doit utiliser un thread pour effectuer l'appel réel sans bloquer le thread principal (boucle d'événement). Potentiellement, aucun thread du pool de threads ne sera jamais utilisé si vous n'avez pas besoin d'appeler ce type de fonctions.

gawi
la source
7

Je ne sais rien du fonctionnement interne de node.js, mais je peux voir comment l'utilisation d'une boucle d'événement peut surpasser la gestion des E / S filetées. Imaginez une demande de disque, donnez-moi staticFile.x, faites-lui 100 demandes pour ce fichier. Chaque demande prend normalement un thread récupérant ce fichier, c'est 100 threads.

Imaginez maintenant que la première demande crée un thread qui devient un objet éditeur, les 99 autres requêtes recherchent d'abord s'il existe un objet éditeur pour staticFile.x, si c'est le cas, écoutez-le pendant qu'il fonctionne, sinon démarrez un nouveau thread et donc un nouvel objet éditeur.

Une fois le thread unique terminé, il transmet staticFile.x aux 100 écouteurs et se détruit, de sorte que la requête suivante crée un nouveau thread et objet éditeur.

Donc, c'est 100 threads contre 1 thread dans l'exemple ci-dessus, mais aussi 1 recherche de disque au lieu de 100 recherches de disque, le gain peut être assez phénoménal. Ryan est un gars intelligent!

Une autre façon de voir les choses est l'un de ses exemples au début du film. Au lieu de:

pseudo code:
result = query('select * from ...');

Encore une fois, 100 requêtes distinctes vers une base de données contre ...:

pseudo code:
query('select * from ...', function(result){
    // do stuff with result
});

Si une requête était déjà en cours, d'autres requêtes égales sauteraient simplement dans le train en marche, vous pouvez donc avoir 100 requêtes en un seul aller-retour de base de données.

BGerrissen
la source
3
La question de la base de données est plus une question de ne pas attendre la réponse tout en retardant d'autres demandes (qui peuvent ou non utiliser la base de données), mais plutôt de demander quelque chose et de le laisser vous appeler quand il reviendra. Je ne pense pas que cela les relie, car il serait assez difficile de garder une trace de la réponse. De plus, je ne pense pas qu'il existe une interface MySQL qui vous permet de conserver plusieurs réponses sans tampon sur une seule connexion (??)
Tor Valamo
C'est juste un exemple abstrait pour expliquer comment les boucles d'événements peuvent offrir plus d'efficacité, nodejs ne fait rien avec les
bases de
1
Oui, mon commentaire portait davantage sur les 100 requêtes en un seul aller-retour de base de données. : p
Tor Valamo
2
Salut BGerrissen: bon post. Ainsi, lorsqu'une requête est en cours d'exécution, d'autres requêtes similaires "écoutent" comme l'exemple staticFile.X ci-dessus? par exemple, 100 utilisateurs récupèrent la même requête, une seule requête sera exécutée et les 99 autres écouteront la première? Merci !
CHAPa
1
Vous donnez l'impression que nodejs mémorise automatiquement les appels de fonction ou quelque chose. Maintenant, parce que vous n'avez pas à vous soucier de la synchronisation de la mémoire partagée dans le modèle de boucle d'événements de JavaScript, il est plus facile de mettre en cache les choses en mémoire en toute sécurité. Mais cela ne signifie pas que nodejs le fait magiquement pour vous ou que c'est le type d'amélioration des performances qui est demandé.
binki