Node.js et requêtes gourmandes en CPU

215

J'ai commencé à bricoler avec le serveur HTTP Node.js et j'aime vraiment écrire du 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 quelque peu préoccupé par les cas extrêmes où le code procédural est très consommateur d'UC, comme 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 demandes de pages Web simples telles que l'affichage d'une liste d'utilisateurs ou l'affichage d'un article de blog. Cependant, si je veux écrire du code très gourmand en CPU (dans le back-end administrateur par exemple) qui génère des graphiques ou redimensionne des milliers d'images, la requête sera très lente (quelques secondes). Étant donné que ce code n'est pas asynchrone, toutes les demandes envoyées au serveur pendant ces quelques secondes seront bloquées jusqu'à ce que ma demande lente soit terminée.

Une suggestion était d'utiliser Web Workers pour des tâches gourmandes en CPU. Cependant, je crains que les travailleurs Web ne rendent difficile l'écriture de code propre car cela fonctionne en incluant un fichier JS distinct. Que faire si le code gourmand en CPU se trouve dans la méthode d'un objet? Cela craint d'écrire un fichier JS pour chaque méthode qui consomme beaucoup de CPU.

Une autre suggestion était de générer un processus enfant, mais cela rend le code encore moins maintenable.

Avez-vous des suggestions pour surmonter cet obstacle (perçu)? Comment écrivez-vous du code orienté objet propre avec Node.js tout en vous assurant que les tâches lourdes du processeur sont exécutées en mode asynchrone?

Olivier Lalonde
la source
2
Olivier, vous avez posé la même question que j'avais en tête (nouveau sur node) et plus précisément sur le traitement des images. En Java, je peux utiliser un ExecutorService à thread fixe et lui passer tous les travaux de redimensionnement et attendre qu'il se termine à partir de toute la connexion, dans le nœud, je n'ai pas compris comment shuffle off le travail à un module externe qui limite (disons disons) le nombre maximum d'opérations simultanées à 2 à la fois. Avez-vous trouvé une manière élégante de procéder?
Riyad Kalla

Réponses:

55

Ce dont vous avez besoin, c'est d'une file d'attente de tâches! Déplacer vos tâches de longue durée 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 façon 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. Passez simplement une tâche, faites le travail et écrivez la réponse.

quelque chose comme ça https://github.com/resque/resque

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

Tim
la source
35
Pourquoi établissez-vous un lien vers les bibliothèques Ruby dans une question spécifiquement ancrée dans le monde des nœuds?
Jonathan Dumaine
1
@JonathanDumaine C'est une bonne implémentation d'une file d'attente de tâches. Rad le code ruby ​​et le réécrire en javascript. PROFIT!
Simon Stender Boisen
2
Je suis un grand fan de Gearman pour cela, les travailleurs Gearman ne sondent pas un serveur Gearman pour de nouveaux emplois - de nouveaux emplois sont instantanément transmis aux travailleurs. Très réactif
Casey Flynn
1
En fait, quelqu'un l'a porté dans le monde des nœuds: github.com/technoweenie/coffee-resque
FrontierPsycho
@pacerier, pourquoi dites-vous cela? Que proposez vous?
luis.espinal
289

Il s'agit d'une incompréhension de la définition de serveur Web - il ne doit être utilisé que pour "parler" avec les clients. Les tâches à forte charge doivent être déléguées à des programmes autonomes (qui bien sûr peuvent également être écrits en JS).
Vous diriez probablement qu'il est sale, mais je vous assure qu'un processus de serveur Web bloqué dans le redimensionnement des images est tout simplement pire (même pour, disons Apache, quand il ne bloque pas les autres requêtes). Néanmoins, vous pouvez utiliser une bibliothèque commune pour éviter la redondance de code.

EDIT: J'ai trouvé une analogie; l'application Web doit être un restaurant. Vous avez des serveurs (serveur web) et des cuisiniers (travailleurs). Les serveurs sont en contact avec les clients et effectuent des tâches simples comme fournir un menu ou expliquer si un plat est végétarien. En revanche, ils délèguent des tâches plus difficiles à la cuisine. Parce que les serveurs ne font que des choses simples, ils répondent rapidement et les cuisiniers peuvent se concentrer sur leur travail.

Node.js serait ici un serveur unique mais très talentueux qui peut traiter de nombreuses demandes à la fois, et Apache serait un gang de serveurs stupides qui ne traitent qu'une seule demande chacun. Si ce serveur Node.js commençait à cuisiner, ce serait une catastrophe immédiate. Pourtant, la cuisson pourrait également épuiser même une grande quantité de serveurs Apache, sans parler du chaos dans la cuisine et de la diminution progressive de la réactivité.

mbq
la source
6
Eh bien, dans un environnement où les serveurs Web sont multi-threads ou multi-processus et peuvent gérer plus d'une demande simultanée, il est très courant de passer quelques secondes sur une seule demande. Les gens en sont venus à s'y attendre. Je dirais que le malentendu est que node.js est un serveur web "normal". En utilisant node.js, vous devez ajuster un peu votre modèle de programmation, ce qui inclut de pousser le travail "de longue durée" vers un travailleur asynchrone.
Thilo
13
Ne générez pas de processus enfant pour chaque demande (ce qui va à l'encontre de l'objectif de node.js). Générez des travailleurs de l'intérieur de vos demandes lourdes uniquement. Ou acheminez votre travail d'arrière-plan lourd vers autre chose que node.js.
Thilo
47
Bonne analogie, mbq!
Lance Fisher
6
Ha, j'aime vraiment ça. "Node.js: faire mal fonctionner les mauvaises pratiques"
ethan
7
@mbq J'aime l'analogie mais cela pourrait utiliser du travail. Le modèle traditionnel multithread serait une personne à la fois serveur et cuisinier. Une fois la commande prise, cette personne doit retourner faire cuire le repas avant de pouvoir gérer une autre commande. Le modèle node.js a les nœuds comme serveurs et les webworkers comme cuisiniers. Les serveurs gèrent la récupération / résolution des demandes tandis que les travailleurs gèrent les tâches les plus longues. Si vous avez besoin d'une plus grande échelle, vous devez simplement faire du serveur principal un cluster de nœuds et inverser le proxy des tâches gourmandes en CPU vers d'autres serveurs conçus pour le traitement milti-thread.
Evan Plaice
16

Vous ne voulez pas que votre code gourmand en CPU exécute async, vous voulez qu'il s'exécute en parallèle . Vous devez extraire le travail de traitement du thread qui sert les requêtes HTTP. C'est le seul moyen de résoudre ce problème. Avec NodeJS, la réponse est le module de cluster, pour engendrer des processus enfants pour faire le gros du travail. (AFAIK Node n'a pas de concept de threads / mémoire partagée; c'est des processus ou rien). Vous disposez de deux options pour structurer votre application. Vous pouvez obtenir la solution 80/20 en générant 8 serveurs HTTP et en gérant de manière synchrone les tâches à forte intensité de calcul sur les processus enfants. Faire cela est assez simple. Vous pourriez prendre une heure pour en savoir plus sur 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 est de configurer une file d'attente de travaux et d'envoyer de grandes tâches de calcul sur la file d'attente. Notez qu'il y a beaucoup de surcharge associée à l'IPC pour une file d'attente de travaux, donc cela n'est 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.

Contexte: Le code asynchrone est un code qui se suspend jusqu'à ce que quelque chose se produise ailleurs , auquel cas le code se réveille et continue son exécution. Un E / S est un cas très courant où quelque chose de lent doit se produire ailleurs.

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

Maintenant, il peut sembler que le code asynchrone est une niche, mais en fait, 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 modèle qui se produit toujours dans les serveurs Web, par exemple. Chaque client qui se connecte à votre serveur obtient un socket. La plupart du temps, les prises sont vides. Vous ne voulez rien faire jusqu'à ce qu'un socket reçoive des données, moment auquel vous souhaitez gérer la demande. Sous le capot, un serveur HTTP comme Node utilise une bibliothèque d'événements (libev) pour garder une trace des milliers de sockets ouverts. Le système d'exploitation notifie libev, puis libev avertit NodeJS lorsque l'un des sockets obtient des données, puis NodeJS place un événement dans la file d'attente d'événements, et votre code http entre en action à ce stade et gère les événements les uns après les autres. Les événements ne sont pas mis dans la file d'attente jusqu'à ce que le socket ait des données, donc les événements n'attendent jamais de données - il est déjà là pour eux.

Les serveurs Web basés sur des événements à thread unique ont du sens comme paradigme lorsque le goulot d'étranglement attend sur un tas de connexions de socket principalement vides et que vous ne voulez pas un thread ou un processus entier pour chaque connexion inactive et que vous ne voulez pas interroger votre 250k sockets pour trouver le prochain qui contient des données.

maçon
la source
devrait être la bonne réponse .... quant à la solution où vous générez 8 clusters, vous auriez besoin de 8 cœurs à droite? Ou l'équilibreur de charge avec plusieurs serveurs.
Muhammad Umer
également ce qui est un bon moyen d'en savoir plus sur la 2e solution, la mise en place d'une file d'attente. Le concept de file d'attente est assez simple, mais sa partie de messagerie entre les processus et la file d'attente est étrangère.
Muhammad Umer
C'est vrai. Vous devez mettre le travail sur un autre noyau, en quelque sorte. Pour cela, vous avez besoin d'un autre noyau.
Masonk
Re: files d'attente. La réponse pratique consiste à utiliser une file d'attente de travaux. Il y en a pour le nœud. Je n'en ai jamais utilisé, je ne peux donc pas faire de recommandation. La réponse curieuse est que les processus de travail et les processus de file d'attente vont finalement communiquer via des sockets.
Masonk
7

Quelques approches que vous pouvez utiliser.

Comme le note @Tim, vous pouvez créer une tâche asynchrone qui se trouve à l'extérieur ou parallèlement à votre logique de desserte principale. Cela dépend de vos besoins exacts, mais même cron peut agir comme un mécanisme de mise en file d'attente.

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

Vous obtenez toujours que vous pouvez toujours réutiliser les modules et le code via le mécanisme standard "requiert". Il vous suffit de vous assurer que l'envoi initial au travailleur transmet toutes les informations nécessaires au traitement des résultats.

Toby Hede
la source
0

L'utilisation child_processest une solution. Mais chaque processus enfant généré peut consommer beaucoup de mémoire par rapport à Gogoroutines

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

néo
la source