• [^] # Re: Petit joueur

    Posté par (site web personnel) . En réponse au journal Des vieilles bases d'unix à la hype reactive actuelle. Évalué à 4.

    Un des gros inconvénients du modèle de Nodejs est d'avoir un seul fil d'exécution en collaboratif. Cela veut dire que si ce fil d'exécution est bloqué pendant 100ms pour calculer le hash d'un mot de passe avec une fonction moderne de hashage comme scrypt, il n'y a aucune autre requête HTTP qui va être traitée pendant ce temps et la latence va augmenter de 100ms pour toutes les requêtes de ce processus.

    Ce serait effectivement une manière assez plus maladroite de faire! ;) Dans ce paradigme on n'attend que les "threads" collaborent entre eux: de temps en temps il faut donc rendre la main à la boucle. Plutôt qu'un long discours, un petit exemple:

    function fiboLoop(n, k, a1, a2, callback) {
     var collaborate_p = (k % 20 === 0);
     if(n == k) {
     callback(a2);
     } else {
     if(collaborate_p) {
     setTimeout(() => {
     fiboLoop(n, k + 1, a2, combine(a1, a2), callback);
     }, 1);
     } else {
     fiboLoop(n, k + 1, a2, combine(a1, a2), callback);
     }
     }
    }

    (À tout hasard je précise que je suis loin d'être un spécialiste ou un amoureux de JavaScript, moi mon truc c'est plutôt OCaml, Lisp, SH et TeX. Donc mon code est très certainement assez médiocre. ;) )

    Ma fonction combine déduit un hash des valeurs de a1 et a2, et j'utilise un "schéma de Fibonacci" pour faire une boucle, qui toutes les 20 itérations repasse la main à l'ordonnanceur pour collaborer un peu avec ses petits copains: il suffit de se retenir de faire le calcul et de demander à la boucle de continuer quand bon lui semble.

    Voici un code complet qui met ça dans un serveur HTTP

    const http = require('http');
    const process = require('process');
    const crypto = require('crypto');
    const url = require('url');
    const server_port = 8080;
    const server_iterations = 400000;
    const fibo_initialize_a1 = "I don't like secrets.";
    const fibo_initialize_a2 = "God is my co-pilot.";
    function combine(a1, a2) {
     return crypto.createHmac('sha256', a1)
     .update(a2)
     .digest('hex');
    }
    function fiboLoop(n, k, a1, a2, callback) {
     var collaborate_p = (k % 20 === 0);
     if(n == k) {
     callback(a2);
     } else {
     if(collaborate_p) {
     setTimeout(() => {
     fiboLoop(n, k + 1, a2, combine(a1, a2), callback);
     }, 1);
     } else {
     fiboLoop(n, k + 1, a2, combine(a1, a2), callback);
     }
     }
    }
    function fibo(n, callback) {
     fiboLoop(n, 0, fibo_initialize_a1, fibo_initialize_a2, callback);
    }
    function safeParseInt(text, _defaultValue) {
     var parsedInt = parseInt(text);
     var defaultValue = _defaultValue || 0;
     return isNaN(parsedInt) ? defaultValue : parsedInt;
    };
    http.createServer(function (req, res) {
     var parts = url.parse(req.url, true);
     var query = parts.query;
     var iterations =
     ('n' in query)
     ? safeParseInt(query['n'])
     : server_iterations;
     console.log('Receive fibo ' + iterations);
     fibo(iterations, (ax) => {
     res.writeHead(200, {'Content-Type': 'text/plain'});
     res.write('Hello World! ' + ax);
     res.end();
     console.log('Done fibo ' + iterations);
     });
    }).listen(server_port);

    On peut tester avec quelques commandes (while true; do curl 'localhost:8080?n=10'; done) (while true; do curl 'localhost:8080?n=100'; done) et (while true; do curl 'localhost:8080?n=10000'; done) qui tournent dans des terminaux.

    On a également la distinction entre les threads dédiées aux appels système et les autre threads dédiés aux traitements. Le nombre de threads dédiés aux traitements est la variable d'ajustement pour la montée en puissance, il est limité par la variable d'environnement GOMAXPROCS.

    C'est sympa cette distinction. En pratique ça se passe comment? Il faut annoter ses routines ou bien le compilateur s'en sort tout seul? Aussi c'est un peu bizarre de penser qu'une fonction change de catégorie si on la "pepper-printf" pour déboguer.

    alors que pour Nodejs, ça demande souvent encore un peu de boulot de passer d'un seul à plusieurs processus : écrire le petit bout de code pour cluster, faire attention aux variables globales qui ne le sont plus (cache, pools de connexions à la base de données), etc.

    Si on cherche à faire facilement du redimensionnement de capacité, la bonne stratégie dépend du contexte. Si par exemple on travaille avec des ressources de calcul élastiques avec des instances de 2-4 cœurs qu'on ajuste à la capacité demandée on peut se passer complètement de la couche cluster et compter sur un reverse-proxy type haproxy ou bien par exemple le répartiteur de charge de docker ou bien le consul de hashicorp par exemple – ou tout autre composant qui va répartir le flux sur plusieurs processus qui tournent sur des serveurs différents. Et même si on a une machine grassouillette avec 192 cœurs dont on veut tirer parti, ce n'est pas forcément aberrant de procéder de la sorte aussi.