web-dev-qa-db-fra.com

Node.js et requêtes gourmandes en ressources CPU

J'ai commencé à bricoler avec le serveur HTTP Node.js et j'aime vraiment écrire du code Javascript côté serveur, mais quelque chose m'empêche de commencer à utiliser Node.js pour mon application Web.

Je comprends tout le concept d'E/S asynchrone, mais je suis un peu préoccupé par les cas Edge où le code de procédure nécessite beaucoup de ressources de traitement, telles que la manipulation d'images ou le tri de grands ensembles de données.

Si je comprends bien, le serveur sera très rapide pour les requêtes de page Web simples, telles que l'affichage d'une liste d'utilisateurs ou l'affichage d'un article de blog. Cependant, si je veux écrire un code très gourmand en ressources processeur (dans le back-end de l'administrateur par exemple) qui génère des graphiques ou redimensionne des milliers d'images, la requête sera très lente (quelques secondes). Comme ce code n'est pas asynchrone, toutes les requêtes arrivant sur le serveur pendant ces quelques secondes seront bloquées jusqu'à ce que ma requête lente soit terminée.

Une suggestion a été d'utiliser Web Workers pour les tâches gourmandes en ressources CPU. Cependant, j’ai bien peur que les employés Web ne puissent écrire du code propre, car cela fonctionne en incluant un fichier JS séparé. Que se passe-t-il si le code gourmand en ressources CPU se trouve dans la méthode d'un objet? Écrire un fichier JS pour chaque méthode gourmande en ressources processeur est un peu nul.

Une autre suggestion a été de créer un processus enfant, mais cela rend le code encore moins maintenable.

Des suggestions pour surmonter cet obstacle (perçu)? Comment rédigez-vous du code orienté objet propre avec Node.js tout en vous assurant que les tâches lourdes du processeur sont exécutées de manière asynchrone?

202
Olivier Lalonde

Ce dont vous avez besoin, c'est d'une file d'attente de tâches! Déplacer vos longues tâches hors du serveur Web est une bonne chose. Conserver chaque tâche dans un fichier js "séparé" favorise la modularité et la réutilisation du code. Cela vous oblige à réfléchir à la manière de structurer votre programme de manière à faciliter le débogage et la maintenance à long terme. Un autre avantage d'une file d'attente de tâches est que les travailleurs peuvent être écrits dans une langue différente. Il suffit de lancer une tâche, de faire le travail et d’écrire la réponse.

quelque chose comme ceci https://github.com/resque/resque

Voici un article de github expliquant pourquoi ils l'ont construit http://github.com/blog/542-introducing-resque

51
Tim

Ceci est une incompréhension de la définition du serveur Web - elle ne devrait être utilisée que pour "parler" avec les clients. Les tâches lourdes doivent être déléguées à des programmes autonomes (bien entendu, cela peut également être écrit en langage JS).
Vous diriez probablement que c'est sale, mais je vous assure qu'un processus de serveur Web bloqué par le redimensionnement des images est encore pire (même pour Apache, par exemple, lorsqu'il ne bloque pas d'autres requêtes). Néanmoins, vous pouvez utiliser une bibliothèque commune pour éviter la redondance du code.

EDIT: Je suis venu avec une analogie; application web devrait être comme un restaurant. Vous avez des serveurs (serveur Web) et des cuisiniers (ouvriers). Les serveurs sont en contact avec les clients et font des tâches simples, comme fournir un menu ou expliquer si certains plats sont végétariens. D'autre part, ils délèguent des tâches plus difficiles à la cuisine. Parce que les serveurs ne font que des choses simples, ils réagissent rapidement et les cuisiniers peuvent se concentrer sur leur travail.

Node.js serait un serveur unique, mais très talentueux, capable de traiter plusieurs demandes à la fois, et Apache, un groupe de serveurs stupides ne traitant qu'une demande par personne. Si ce serveur Node.js commençait à cuisiner, ce serait une catastrophe immédiate. Néanmoins, la cuisine pourrait également épuiser un nombre important de serveurs Apache, sans parler du chaos dans la cuisine et de la diminution progressive de la réactivité.

280
mbq

Vous ne voulez pas que votre code gourmand en ressources CPU s'exécute de manière asynchrone, vous voulez qu'il s'exécute en parallèle . Vous devez extraire le travail de traitement du thread qui sert les demandes HTTP. C'est la seule façon de résoudre ce problème. Avec NodeJS, la réponse est le module de cluster , pour que les processus enfants géniteurs fassent le gros du travail. (AFAIK Node n'a pas de concept de threads/mémoire partagée; c'est processus ou rien). Vous avez deux options pour structurer votre application. Vous pouvez obtenir la solution 80/20 en créant 8 serveurs HTTP et en gérant de manière synchrone des tâches gourmandes en calculs sur les processus enfants. Faire cela est assez simple. Vous pourriez prendre une heure pour lire à ce sujet à ce lien. En fait, si vous extrayez simplement l'exemple de code en haut de ce lien, vous obtiendrez 95% du chemin.

L'autre façon de structurer cela consiste à configurer une file d'attente de travaux et à envoyer de grandes tâches de calcul sur la file d'attente. Notez qu'il y a beaucoup de surcharge associée à IPC pour une file d'attente de travaux. Cela n'est donc utile que lorsque les tâches sont sensiblement plus grandes que la surcharge.

Je suis surpris qu'aucune de ces autres réponses ne mentionne même le cluster .

Arrière-plan: le code asynchrone est un code suspendu jusqu'à ce que quelque chose se produise ailleurs , moment auquel le code se réveille et poursuit son exécution. Un cas très courant où quelque chose de lent doit se produire ailleurs est I/O.

Le code asynchrone n'est pas utile si votre processeur est responsable de l'exécution du travail. C'est précisément le cas des tâches "intensives en calcul".

Maintenant, il peut sembler que le code asynchrone soit une niche, mais en réalité, il est très courant. Il se trouve que cela n’est pas utile pour les tâches de calcul intensives.

L'attente sur les E/S est un motif qui se produit toujours dans les serveurs Web, par exemple. Chaque client qui se connecte à votre serveur reçoit un socket. La plupart du temps, les sockets sont vides. Vous ne voulez rien faire jusqu'à ce qu'un socket reçoive des données, à quel point vous voulez gérer la demande. Sous le capot, un serveur HTTP comme Node utilise une bibliothèque d'événements (libev) pour garder trace des milliers de sockets ouverts. Le système d'exploitation notifie libev, puis libev notifie NodeJS lorsque l'un des sockets obtient des données, puis NodeJS place un événement dans la file d'attente des événements. Votre code http entre alors en jeu et gère les événements l'un après l'autre. Les événements ne sont pas mis en file d'attente tant que le socket ne contient pas de données. Par conséquent, ils ne sont jamais en attente de données. Ils sont déjà là.

Les serveurs Web basés sur des événements à thread unique sont utiles en tant que paradigme lorsque le goulot d'étranglement attend sur un ensemble de connexions de socket essentiellement vides et que vous ne voulez pas d'un processus ou d'un processus complet pour chaque connexion inactive et que vous ne souhaitez pas interroger votre 250k sockets pour trouver le prochain qui contient des données.

13
masonk

Quelques approches que vous pouvez utiliser.

Comme @Tim le note, vous pouvez créer une tâche asynchrone située en dehors ou en parallèle de votre logique serveur principale. Dépend de vos exigences exactes, mais même cron peut agir comme un mécanisme de mise en file d'attente.

WebWorkers peut fonctionner pour vos processus asynchrones, mais ils ne sont actuellement pas pris en charge par node.js. Deux extensions fournissent un support, par exemple: http://github.com/cramforce/node-worker

Vous obtenez toujours que vous pouvez toujours réutiliser des modules et du code via le mécanisme standard "Requiert". Vous devez simplement vous assurer que la dépêche initiale envoyée au travailleur transmet toutes les informations nécessaires au traitement des résultats.

7
Toby Hede

Utilisation child_process est une solution. Mais chaque processus enfant généré peut consommer beaucoup de mémoire comparé à Go goroutines

Vous pouvez également utiliser une solution basée sur la file d'attente telle que kue

0
neo