Maison >interface Web >js tutoriel >Introduction à quelques méthodes pour exploiter pleinement les performances de Node.js
Un processus Node.JS ne fonctionnera que sur un seul cœur physique. Pour cette raison, une attention particulière doit être portée lors du développement d'un serveur évolutif.
Comme il existe un ensemble stable d'API et le développement d'extensions natives pour gérer les processus, il existe de nombreuses façons différentes de concevoir une application Node.JS qui peut être parallélisée. Dans cet article de blog, nous comparons ces architectures possibles.
Cet article présente également le module Compute-cluster : une petite bibliothèque Node.JS qui peut être utilisée pour gérer facilement les processus et implémenter l'informatique distribuée de deuxième ligne.
Problèmes rencontrés
Dans notre projet Mozilla Persona, nous devons être capables de gérer un grand nombre de requêtes avec des caractéristiques différentes, nous avons donc essayé d'utiliser Node.JS.
Afin de ne pas affecter l'expérience utilisateur, la requête « Interactive » que nous avons conçue ne nécessite qu'une consommation informatique légère, mais offre un temps de réponse plus rapide afin que l'interface utilisateur ne se sente pas bloquée. En comparaison, le traitement d'une opération « Batch » prend environ une demi-seconde, et les délais peuvent être plus longs pour d'autres raisons.
Pour une meilleure conception, nous avons trouvé de nombreuses solutions qui répondent à nos besoins actuels.
Compte tenu de l'évolutivité et du coût, nous énumérons les exigences clés suivantes :
Grâce aux points ci-dessus, nous pouvons filtrer clairement et avec détermination
Option 1 : Traiter directement dans le fil principal.
Lorsque le thread principal traite directement les données, le résultat est très mauvais :
Vous ne pouvez pas profiter pleinement des processeurs multicœurs. En requête/réponse interactive, vous devez attendre que la requête (ou la réponse) en cours soit traitée, ce qui est inélégant.
Le seul avantage de cette solution est qu'elle est assez simple
function myRequestHandler(request, response) [ // Let's bring everything to a grinding halt for half a second. var results = doComputationWorkSync(request.somesuch); }
Dans un programme Node.JS, si vous souhaitez gérer plusieurs requêtes en même temps et que vous souhaitez les traiter de manière synchrone, alors vous allez avoir des problèmes.
Méthode 2 : s'il faut utiliser le traitement asynchrone.
Y aura-t-il une grande amélioration des performances si des méthodes asynchrones sont utilisées en arrière-plan ?
La réponse n'est pas nécessairement. Cela dépend de la pertinence de fonctionner en arrière-plan
Par exemple, dans la situation suivante : si les performances ne sont pas meilleures que le traitement synchrone lors de l'utilisation de JavaScript ou du code local sur le thread principal pour effectuer des calculs, vous n'avez pas nécessairement besoin d'utiliser des méthodes asynchrones en arrière-plan pour traiter
Veuillez lire le code suivant
function doComputationWork(input, callback) { // Because the internal implementation of this asynchronous // function is itself synchronously run on the main thread, // you still starve the entire process. var output = doComputationWorkSync(input); process.nextTick(function() { callback(null, output); }); } function myRequestHandler(request, response) [ // Even though this *looks* better, we're still bringing everything // to a grinding halt. doComputationWork(request.somesuch, function(err, results) { // ... do something with results ... });
}
关键点就在于NodeJS异步API的使用并不依赖于多进程的应用
方案三:用线程库来实现异步处理。
只要实现得当,使用本地代码实现的库,在 NodeJS 调用的时候是可以突破限制从而实现多线程功能的。
有很多这样的例子, Nick Campbell 编写的 bcrypt library 就是其中优秀的一个。
如果你在4核机器上拿这个库来作一个测试,你将看到神奇的一幕:4倍于平时的吞吐量,并且耗尽了几乎所有的资源!但是如果你在24核机器上测试,结果将不会有太大变化:有4个核心的使用率基本达到100%,但其他的核心基本上都处于空闲状态。
问题出在这个库使用了NodeJS内部的线程池,而这个线程池并不适合用来进行此类的计算。另外,这个线程池上限写死了,最多只能运行4个线程。
除了写死了上限,这个问题更深层的原因是:
内建线程机制的组件库在这种情况下并不能有效地利用多核的优势,这降低了程序的响应能力,并且随着负载的加大,程序表现越来越差。
方案四:使用 NodeJS 的 cluster 模块
NodeJS 0.6.x 以上的版本提供了一个cluster模块 ,允许创建“共享同一个socket”的一组进程,用来分担负载压力。
假如你采用了上面的方案,又同时使用 cluster 模块,情况会怎样呢?
这样得出的方案将同样具有同步处理或者内建线程池一样的缺点:响应缓慢,毫无优雅可言。
有时候,仅仅添加新运行实例并不能解决问题。
方案五:引入 compute-cluster 模块
在 Persona 中,我们的解决方案是,维护一组功能单一(但各不相同)的计算进程。
在这个过程中,我们编写了 compute-cluster 库。
这个库会自动按需启动和管理子进程,这样你就可以通过代码的方式来使用一个本地子进程的集群来处理数据。
使用例子:
const computecluster = require('compute-cluster'); // allocate a compute cluster var cc = new computecluster({ module: './worker.js' }); // run work in parallel cc.enqueue({ input: "foo" }, function (error, result) { console.log("foo done", result); }); cc.enqueue({ input: "bar" }, function (error, result) { console.log("bar done", result); });
fileworker.js 中响应了 message 事件,对传入的请求进行处理:
process.on('message', function(m) { var output; // do lots of work here, and we don't care that we're blocking the // main thread because this process is intended to do one thing at a time. var output = doComputationWorkSync(m.input); process.send(output); });
Sans modifier le code appelant, le module de cluster de calcul peut être intégré à l'API asynchrone existante, de sorte qu'un véritable traitement parallèle multicœur puisse être réalisé avec la plus petite quantité de code.
Examinons les performances de cette solution sous quatre aspects.
Capacité parallèle multicœur : le processus enfant utilise tous les cœurs.
Réactivité : étant donné que le processus de gestion de base est uniquement responsable du démarrage des processus enfants et de la transmission des messages, il est inactif la plupart du temps et peut gérer des requêtes plus interactives.
Même si la machine est soumise à une forte pression de charge, nous pouvons toujours utiliser le planificateur du système d'exploitation pour augmenter la priorité du processus de gestion de base.
Simplicité : L'API asynchrone est utilisée pour masquer les détails spécifiques de l'implémentation. Nous pouvons facilement intégrer ce module dans le projet en cours sans même changer le code appelant.
Voyons maintenant si nous pouvons trouver un moyen pour que même si la charge augmente soudainement, l'efficacité du système ne chute pas anormalement.
Bien sûr, le meilleur objectif reste que même si la pression augmente, le système puisse toujours fonctionner efficacement et traiter autant de demandes que possible.
Pour aider à mettre en œuvre de bonnes solutions, le cluster de calcul fait plus que simplement gérer les processus enfants et transmettre des messages ; il gère également d'autres informations ;
Il enregistre le nombre de processus enfants en cours d'exécution et le temps moyen nécessaire à chaque processus enfant pour se terminer.
Grâce à ces enregistrements, nous pouvons prédire combien de temps il faudra avant que le processus enfant ne démarre.
Selon cela, couplé aux paramètres définis par l'utilisateur (max_request_time), nous pouvons directement fermer les demandes qui peuvent expirer sans traitement.
Cette fonctionnalité permet de baser facilement votre code sur l'expérience utilisateur. Par exemple, "Les utilisateurs ne doivent pas attendre plus de 10 secondes pour se connecter." Cela équivaut à définir max_request_time sur 7 secondes (le temps de transmission réseau doit être pris en compte).
Après avoir testé le service Persona, les résultats ont été très satisfaisants.
Dans des conditions de pression extrêmement élevées, nous avons toujours été en mesure de fournir des services aux utilisateurs authentifiés, et avons également bloqué certains utilisateurs non authentifiés et affiché des messages d'erreur pertinents.