Comment, en général, Node.js gère-t-il 10 000 requêtes simultanées?


395

Je comprends que Node.js utilise un seul thread et une boucle d'événements pour traiter les demandes en ne les traitant qu'une à la fois (ce qui n'est pas bloquant). Mais encore, comment cela fonctionne, disons 10 000 demandes simultanées. La boucle d'événement traitera toutes les demandes? Cela ne prendrait-il pas trop de temps?

Je ne peux pas (encore) comprendre comment il peut être plus rapide qu'un serveur Web multithread. Je comprends que le serveur Web multi-thread sera plus cher en ressources (mémoire, CPU), mais ne serait-il pas encore plus rapide? Je me trompe probablement; veuillez expliquer comment ce thread unique est plus rapide dans de nombreuses demandes et ce qu'il fait généralement (à haut niveau) lors du traitement de nombreuses demandes, comme 10 000.

Et aussi, est-ce que ce thread unique s'adapte bien à cette grande quantité? Veuillez garder à l'esprit que je commence tout juste à apprendre Node.js.


5
Parce que la plupart du travail (déplacement des données) n'implique pas le CPU.
OrangeDog

5
Notez également que le fait qu'un seul thread exécute Javascript ne signifie pas qu'il n'y a pas beaucoup d'autres threads qui fonctionnent.
OrangeDog

Cette question est soit trop large, soit un double de diverses autres questions.
OrangeDog


Parallèlement au thread unique, Node.js fait quelque chose appelé "E / S non bloquantes". C'est ici que toute la magie est opérée
Anand N

Réponses:


764

Si vous devez poser cette question, vous n'êtes probablement pas familier avec ce que font la plupart des applications / services Web. Vous pensez probablement que tous les logiciels font cela:

user do an action
       
       v
 application start processing action
   └──> loop ...
          └──> busy processing
 end loop
   └──> send result to user

Cependant, ce n'est pas ainsi que fonctionnent les applications Web, ou en fait n'importe quelle application avec une base de données comme back-end. Les applications Web font ceci:

user do an action
       
       v
 application start processing action
   └──> make database request
          └──> do nothing until request completes
 request complete
   └──> send result to user

Dans ce scénario, le logiciel passe la majeure partie de son temps d'exécution à utiliser 0% de temps processeur en attendant le retour de la base de données.

Application réseau multithread:

Les applications réseau multithread gèrent la charge de travail ci-dessus comme ceci:

request ──> spawn thread
              └──> wait for database request
                     └──> answer request
request ──> spawn thread
              └──> wait for database request
                     └──> answer request
request ──> spawn thread
              └──> wait for database request
                     └──> answer request

Ainsi, le thread passe la plupart de son temps à utiliser 0% de CPU en attendant que la base de données renvoie des données. Ce faisant, ils ont dû allouer la mémoire requise pour un thread qui comprend une pile de programme complètement distincte pour chaque thread, etc. pas cher.

Boucle d'événement à filetage unique

Puisque nous passons la plupart de notre temps à utiliser 0% de CPU, pourquoi ne pas exécuter du code lorsque nous n'utilisons pas de CPU? De cette façon, chaque demande aura toujours le même temps CPU que les applications multithread mais nous n'avons pas besoin de démarrer un thread. Nous faisons donc ceci:

request ──> make database request
request ──> make database request
request ──> make database request
database request complete ──> send response
database request complete ──> send response
database request complete ──> send response

En pratique, les deux approches retournent des données avec à peu près la même latence car c'est le temps de réponse de la base de données qui domine le traitement.

Le principal avantage ici est que nous n'avons pas besoin de générer un nouveau thread donc nous n'avons pas besoin de faire beaucoup, beaucoup de malloc qui nous ralentiraient.

Filetage magique et invisible

La chose apparemment mystérieuse est de savoir comment les deux approches ci-dessus parviennent à exécuter la charge de travail en "parallèle"? La réponse est que la base de données est filetée. Notre application à un seul thread exploite donc le comportement à plusieurs threads d'un autre processus: la base de données.

Là où l'approche à fil unique échoue

Une application à un seul fil tombe en panne si vous devez faire beaucoup de calculs CPU avant de renvoyer les données. Maintenant, je ne parle pas d'une boucle for traitant le résultat de la base de données. C'est encore principalement O (n). Ce que je veux dire, c'est des choses comme la transformation de Fourier (encodage mp3 par exemple), le lancer de rayons (rendu 3D), etc.

Un autre écueil des applications à filetage unique est qu'elles n'utiliseront qu'un seul cœur de processeur. Donc, si vous avez un serveur quadricœur (ce qui n'est pas rare de nos jours), vous n'utilisez pas les 3 autres cœurs.

Lorsque l'approche multithread échoue

Une application multithread échoue fortement si vous devez allouer beaucoup de RAM par thread. Tout d'abord, l'utilisation de la RAM elle-même signifie que vous ne pouvez pas gérer autant de demandes qu'une application à filetage unique. Pire encore, malloc est lent. L'allocation de lots et de lots d'objets (ce qui est courant pour les frameworks Web modernes) signifie que nous pouvons potentiellement être plus lents que les applications à filetage unique. C'est là que node.js gagne généralement.

Un cas d'utilisation qui finit par empirer le multithread est lorsque vous devez exécuter un autre langage de script dans votre thread. D'abord, vous devez généralement malloc l'intégralité du runtime pour ce langage, puis vous devez malloc les variables utilisées par votre script.

Donc, si vous écrivez des applications réseau en C ou go ou java, la surcharge de thread n'est généralement pas trop mauvaise. Si vous écrivez un serveur Web C pour servir PHP ou Ruby, il est très facile d'écrire un serveur plus rapide en javascript ou Ruby ou Python.

Approche hybride

Certains serveurs Web utilisent une approche hybride. Nginx et Apache2 implémentent par exemple leur code de traitement réseau en tant que pool de threads de boucles d'événements. Chaque thread exécute une boucle d'événements en traitant simultanément les requêtes à thread unique, mais les requêtes sont équilibrées en charge entre plusieurs threads.

Certaines architectures monothread utilisent également une approche hybride. Au lieu de lancer plusieurs threads à partir d'un seul processus, vous pouvez lancer plusieurs applications - par exemple, 4 serveurs node.js sur une machine à quatre cœurs. Vous utilisez ensuite un équilibreur de charge pour répartir la charge de travail entre les processus.

En effet, les deux approches sont des images miroir techniquement identiques l'une de l'autre.


106
C'est de loin la meilleure explication du nœud que j'ai lue jusqu'à présent. Cette "application monothread exploite en fait le comportement multithread d'un autre processus: la base de données." A fait le travail
kenobiwan

que se passe-t-il si le client fait plusieurs requêtes dans le nœud, comme par exemple obtenir un nom et le modifier, et dire que ces opérations poussent vers le serveur pour être traitées très rapidement par de nombreux clients. comment pourrais-je gérer un tel scénario?
Remario

3
@CaspainCaldion Cela dépend de ce que vous entendez par très rapide et beaucoup de clients. En l'état, node.js peut traiter jusqu'à 1 000 requêtes par seconde et une vitesse limitée uniquement à la vitesse de votre carte réseau. Notez qu'il s'agit de 1000 requêtes par seconde et non de clients connectés simultanément. Il peut gérer les 10000 clients simultanés sans problème. Le véritable goulot d'étranglement est la carte réseau.
slebetman

1
@slebetman, la meilleure explication de tous les temps. une chose cependant, si j'ai un algorithme d'apprentissage automatique qui traite certaines informations et fournit des résultats en conséquence, dois-je utiliser une approche multithread ou un thread unique
Ganesh Karewad

5
@GaneshKarewad Les algorithmes utilisent le CPU, les services (base de données, API REST, etc.) utilisent les E / S. Si l'IA est un algorithme écrit en js, vous devez l'exécuter dans un autre thread ou processus. Si l'IA est un service exécuté sur un autre ordinateur (comme les services d'Amazon ou Google ou IBM AI), utilisez une architecture à thread unique.
slebetman

46

Ce que vous semblez penser, c'est que la majeure partie du traitement est gérée dans la boucle d'événements du nœud. Le nœud ferme en fait le travail d'E / S sur les threads. Les opérations d'E / S prennent généralement des ordres de grandeur plus longs que les opérations du processeur, alors pourquoi le processeur attend-il cela? En outre, le système d'exploitation peut déjà très bien gérer les tâches d'E / S. En fait, parce que Node n'attend pas, il permet une utilisation beaucoup plus élevée du processeur.

Par analogie, imaginez NodeJS comme un serveur qui prend les commandes des clients pendant que les chefs d'E / S les préparent dans la cuisine. D'autres systèmes ont plusieurs chefs qui prennent la commande d'un client, préparent le repas, débarrassent la table et s'occupent ensuite du prochain client.


5
Merci pour l'analogie avec le restaurant! Je trouve les analogies et les exemples du monde réel beaucoup plus faciles à apprendre.
LaVache

13

Je comprends que Node.js utilise un seul thread et une boucle d'événements pour traiter les demandes en ne les traitant qu'une à la fois (ce qui n'est pas bloquant).

Je peux me méprendre sur ce que vous avez dit ici, mais «un à la fois» semble que vous ne compreniez peut-être pas complètement l'architecture basée sur les événements.

Dans une architecture d'application "conventionnelle" (non pilotée par les événements), le processus passe beaucoup de temps à attendre que quelque chose se produise. Dans une architecture basée sur des événements telle que Node.js, le processus n'attend pas simplement, il peut continuer son travail.

Par exemple: vous obtenez une connexion d'un client, vous l'acceptez, vous lisez les en-têtes de requête (dans le cas de http), puis vous commencez à agir sur la requête. Vous pouvez lire le corps de la demande, vous finirez généralement par renvoyer des données au client (il s'agit d'une simplification délibérée de la procédure, juste pour démontrer le point).

À chacune de ces étapes, la plupart du temps est passé à attendre que certaines données arrivent de l'autre extrémité - le temps réel passé à traiter dans le thread JS principal est généralement assez minime.

Lorsque l'état d'un objet d'E / S (comme une connexion réseau) change de telle sorte qu'il doit être traité (par exemple, des données sont reçues sur un socket, un socket devient accessible en écriture, etc.), le thread principal Node.js JS est réveillé avec une liste des éléments devant être traités.

Il trouve la structure de données pertinente et émet un événement sur cette structure qui provoque l'exécution de rappels, qui traitent les données entrantes, ou écrivent plus de données dans un socket, etc. Une fois que tous les objets d'E / S à traiter ont été traité, le thread principal Node.js JS attendra à nouveau jusqu'à ce qu'il soit informé que davantage de données sont disponibles (ou qu'une autre opération est terminée ou a expiré).

La prochaine fois qu'il sera réveillé, cela pourrait bien être dû à un objet d'E / S différent devant être traité - par exemple une connexion réseau différente. Chaque fois, les rappels pertinents sont exécutés, puis il se rendort en attendant que quelque chose d'autre se produise.

Le point important est que le traitement des différentes requêtes est entrelacé, il ne traite pas une requête du début à la fin et passe ensuite à la suivante.

À mon avis, le principal avantage de cela est qu'une demande lente (par exemple, vous essayez d'envoyer 1 Mo de données de réponse à un appareil de téléphonie mobile via une connexion de données 2G, ou vous faites une requête de base de données très lente) a gagné '' t bloquer les plus rapides.

Dans un serveur Web multithread conventionnel, vous aurez généralement un thread pour chaque requête en cours de traitement, et il traitera UNIQUEMENT cette requête jusqu'à ce qu'elle soit terminée. Que se passe-t-il si vous avez beaucoup de demandes lentes? Vous vous retrouvez avec beaucoup de vos discussions traîner autour de ces demandes, et d'autres demandes (qui peuvent être des demandes très simples qui pourraient être traitées très rapidement) sont mises en file d'attente derrière eux.

Il existe de nombreux autres systèmes basés sur les événements en dehors de Node.js, et ils ont tendance à avoir des avantages et des inconvénients similaires par rapport au modèle conventionnel.

Je ne dirais pas que les systèmes basés sur des événements sont plus rapides dans chaque situation ou avec chaque charge de travail - ils ont tendance à bien fonctionner pour les charges de travail liées aux E / S, pas si bien pour celles liées au processeur.


12

Étapes de traitement du modèle de boucle d'événement à thread unique:

  • Les clients envoient une demande au serveur Web.

  • Node JS Web Server gère en interne un pool de threads limité pour fournir des services aux demandes des clients.

  • Node JS Web Server reçoit ces demandes et les place dans une file d'attente. Il est connu sous le nom de «file d'attente d'événements».

  • Node JS Web Server possède en interne un composant, appelé «boucle d'événement». La raison pour laquelle il a obtenu ce nom est qu'il utilise une boucle indéfinie pour recevoir les demandes et les traiter.

  • La boucle d'événement utilise un seul thread. Il s'agit du cœur du modèle de traitement de la plateforme Node JS.

  • La boucle d'événement vérifie que toute demande client est placée dans la file d'attente d'événements. Sinon, attendez indéfiniment les demandes entrantes.

  • Si oui, récupérez une demande client dans la file d'attente d'événements

    1. Démarre le processus que la demande du client
    2. Si cette demande client ne nécessite aucune opération d'E / S de blocage, alors traitez tout, préparez la réponse et renvoyez-la au client.
    3. Si cette demande client nécessite des opérations d'E / S bloquantes comme l'interaction avec la base de données, le système de fichiers et les services externes, elle suivra une approche différente.
  • Vérifie la disponibilité des threads à partir du pool de threads internes
  • Prend un thread et attribue cette demande client à ce thread.
  • Ce thread est responsable de prendre cette demande, de la traiter, d'effectuer des opérations d'E / S de blocage, de préparer la réponse et de la renvoyer à la boucle d'événement

    très bien expliqué par @Rambabu Posa pour plus d'explications allez jeter ce lien


le diagramme donné dans ce billet de blog semble être faux, ce qu'ils ont mentionné dans cet article n'est pas complètement correct.
rranj

11

Ajout à la réponse de slebetman: Lorsque vous dites qu'il Node.JSpeut traiter 10 000 demandes simultanées, il s'agit essentiellement de demandes non bloquantes, c'est-à-dire que ces demandes concernent principalement la requête de base de données.

En interne, event loopof Node.JSgère un thread pool, où chaque thread gère une non-blocking requestboucle d'événement et continue d'écouter davantage de requêtes après avoir délégué le travail à l'un des threads du thread pool. Lorsque l'un des threads termine le travail, il envoie un signal event loopindiquant qu'il a terminé aka callback. Event looppuis traitez ce rappel et renvoyez la réponse.

Comme vous êtes nouveau sur NodeJS, lisez la suite nextTickpour comprendre comment la boucle d'événements fonctionne en interne. Lisez les blogs sur http://javascriptissexy.com , ils m'ont vraiment aidé lorsque j'ai commencé avec JavaScript / NodeJS.


2

Ajout à la réponse de slebetman pour plus de clarté sur ce qui se passe lors de l'exécution du code.

Le pool de threads internes dans nodeJs n'a que 4 threads par défaut. et ce n'est pas comme si la requête entière était attachée à un nouveau thread du pool de threads, toute l'exécution de la requête se passait comme n'importe quelle requête normale (sans aucune tâche de blocage), juste que chaque fois qu'une requête avait une longue exécution ou une opération lourde comme db appel, une opération de fichier ou une requête http, la tâche est mise en file d'attente dans le pool de threads interne fourni par libuv. Et comme nodeJs fournit 4 threads dans le pool de threads internes par défaut, chaque 5e ou prochaine demande simultanée attend qu'un thread soit libre et une fois ces opérations terminées, le rappel est poussé vers la file d'attente de rappel. et est capté par la boucle d'événements et renvoie la réponse.

Maintenant, voici une autre information que ce n'est pas une seule file d'attente de rappel, il y a beaucoup de files d'attente.

  1. File d'attente NextTick
  2. Micro file d'attente de tâches
  3. Timers Queue
  4. File d'attente de rappel d'E / S (demandes, opérations de fichier, opérations de base de données)
  5. File d'attente IO Poll
  6. Vérifier la file d'attente de phase ou SetImmediate
  7. fermer la file d'attente des gestionnaires

Chaque fois qu'une demande arrive, le code s'exécute dans cet ordre de rappels mis en file d'attente.

Ce n'est pas comme quand il y a une demande de blocage, il est attaché à un nouveau thread. Il n'y a que 4 threads par défaut. Il y a donc une autre file d'attente là-bas.

Chaque fois que dans un code un processus de blocage comme la lecture de fichier se produit, appelle ensuite une fonction qui utilise le thread du pool de threads, puis une fois l'opération terminée, le rappel est transmis à la file d'attente respective, puis exécuté dans l'ordre.

Tout est mis en file d'attente en fonction du type de rappel et traité dans l'ordre mentionné ci-dessus.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.