68 votes

Node.js, Socket.io, Redis pub/sub volume élevé, difficultés de faible latence

Lorsque l'on associe socket.io/node.js et redis pub/sub pour tenter de créer un système de diffusion web en temps réel piloté par les événements du serveur et capable de gérer plusieurs transports, il semble y avoir trois approches possibles :

  1. Créez une connexion redis avec 'createClient' et abonnez-vous au(x) canal(aux). Sur la connexion client socket.io, joindre le client dans une chambre socket.io. Dans l'événement redis.on("message", ...), appelez io.sockets.in(room).emit("event", data) pour distribuer à tous les clients dans la salle concernée. Comme Comment réutiliser une connexion redis dans socket.io ?

  2. createClient' une connexion redis. Sur la connexion client socket.io, joindre le client dans une chambre socket.io et s'abonner au(x) canal(aux) redis approprié(s). Incluez redis.on("message", ...) dans la fermeture de la connexion client et, à la réception du message, appelez client.emit("event", data) pour déclencher l'événement sur le client spécifique. Comme la réponse dans Exemples d'utilisation de RedisStore dans socket.io

  3. Utilisez le RedisStore intégré dans socket.io et diffusez à partir du canal unique "dispatch" de Redis en suivant le protocole socketio-spec.

Le numéro 1 permet de traiter le sous Redis et l'événement associé une fois pour tous les clients. Le numéro 2 offre un accès plus direct à Redis pub/sub. Le numéro 3 est plus simple, mais offre peu de contrôle sur les événements de messagerie.

Cependant, lors de mes tests, tous présentent des performances étonnamment faibles avec plus d'un client connecté. Les événements serveur en question sont 1 000 messages publiés sur un canal Redis le plus rapidement possible, pour être distribués le plus rapidement possible. Les performances sont mesurées par les timings au niveau des clients connectés (basés sur socket.io-client qui enregistrent les timestamps dans une liste Redis pour analyse).

Ce que je suppose, c'est que dans l'option 1, le serveur reçoit le message, puis l'écrit séquentiellement à tous les clients connectés. Dans l'option 2, le serveur reçoit chaque message plusieurs fois (une fois par abonnement client) et l'écrit au client concerné. Dans les deux cas, le serveur n'arrive pas au deuxième événement de message avant qu'il ne soit communiqué à tous les clients connectés. Une situation clairement exacerbée avec l'augmentation de la concurrence.

Cela semble aller à l'encontre de la sagesse perçue des capacités des piles. Je veux y croire, mais j'ai du mal.

Ce scénario (distribution à faible latence d'un grand volume de messages) n'est-il pas (encore) possible avec ces outils, ou est-ce que je rate une astuce ?

30voto

Mark Essel Points 1082

Je pensais que c'était une question raisonnable et j'avais fait de brèves recherches à ce sujet il y a quelque temps. J'ai passé un peu de temps à chercher des exemples dont vous pourriez tirer quelques conseils utiles.

Exemples

J'aime commencer par des exemples directs :

L'exemple léger est une page unique (notez que vous voudrez remplacer redis-node-client par quelque chose comme nœud_redis par Matt Ranney :

/*
 * Mclarens Bar: Redis based Instant Messaging
 * Nikhil Marathe - 22/04/2010

 * A simple example of an IM client implemented using
 * Redis PUB/SUB commands so that all the communication
 * is offloaded to Redis, and the node.js code only
 * handles command interpretation,presentation and subscribing.
 * 
 * Requires redis-node-client and a recent version of Redis
 *    http://code.google.com/p/redis
 *    http://github.com/fictorial/redis-node-client
 *
 * Start the server then telnet to port 8000
 * Register with NICK <nick>, use WHO to see others
 * Use TALKTO <nick> to initiate a chat. Send a message
 * using MSG <nick> <msg>. Note its important to do a
 * TALKTO so that both sides are listening. Use STOP <nick>
 * to stop talking to someone, and QUIT to exit.
 *
 * This code is in the public domain.
 */
var redis = require('./redis-node-client/lib/redis-client');

var sys = require('sys');
var net = require('net');

var server = net.createServer(function(stream) {
    var sub; // redis connection
    var pub;
    var registered = false;
    var nick = "";

    function channel(a,b) {
    return [a,b].sort().join(':');
    }

    function shareTable(other) {
    sys.debug(nick + ": Subscribing to "+channel(nick,other));
    sub.subscribeTo(channel(nick,other), function(channel, message) {
        var str = message.toString();
        var sender = str.slice(0, str.indexOf(':'));
        if( sender != nick )
        stream.write("[" + sender + "] " + str.substr(str.indexOf(':')+1) + "\n");
    });
    }

    function leaveTable(other) {
    sub.unsubscribeFrom(channel(nick,other), function(err) {
        stream.write("Stopped talking to " + other+ "\n");
    });
    }

    stream.addListener("connect", function() {
    sub = redis.createClient();
    pub = redis.createClient();
    });

    stream.addListener("data", function(data) {
    if( !registered ) {
        var msg = data.toString().match(/^NICK (\w*)/);
        if(msg) {
        stream.write("SERVER: Hi " + msg[1] + "\n");
        pub.sadd('mclarens:inside', msg[1], function(err) {
            if(err) {
            stream.end();
            }
            registered = true;
            nick = msg[1];
// server messages
            sub.subscribeTo( nick + ":info", function(nick, message) {
            var m = message.toString().split(' ');
            var cmd = m[0];
            var who = m[1];
            if( cmd == "start" ) {
                stream.write( who + " is now talking to you\n");
                shareTable(who);
            }
            else if( cmd == "stop" ) {
                stream.write( who + " stopped talking to you\n");
                leaveTable(who);
            }
            });
        });
        }
        else {
        stream.write("Please register with NICK <nickname>\n");
        }
        return;
    }

    var fragments = data.toString().replace('\r\n', '').split(' ');
    switch(fragments[0]) {
    case 'TALKTO':
        pub.publish(fragments[1]+":info", "start " + nick, function(a,b) {
        });
        shareTable(fragments[1]);
        break;
    case 'MSG':
        pub.publish(channel(nick, fragments[1]),
            nick + ':' +fragments.slice(2).join(' '),
              function(err, reply) {
              if(err) {
                  stream.write("ERROR!");
              }
              });
        break;
    case 'WHO':
        pub.smembers('mclarens:inside', function(err, users) {
        stream.write("Online:\n" + users.join('\n') + "\n");
        });
        break;
    case 'STOP':
        leaveTable(fragments[1]);
        pub.publish(fragments[1]+":info", "stop " + nick, function() {});
        break;
    case 'QUIT':
        stream.end();
        break;
    }
    });

    stream.addListener("end", function() {
    pub.publish(nick, nick + " is offline");
    pub.srem('mclarens:inside', nick, function(err) {
        if(err) {
        sys.debug("Could not remove client");
        }
    });
    });
});

server.listen(8000, "localhost");

Documents

Il existe une tonne de documentation, et les apis changent rapidement sur ce type de pile. Vous devrez donc évaluer la pertinence temporelle de chaque document.

Questions connexes

Juste quelques questions connexes, c'est un sujet brûlant sur stack :

Conseils notables (ymmv)

Désactivez ou optimisez la mise en commun des sockets, utilisez des liaisons efficaces, surveillez la latence et assurez-vous que vous ne faites pas double emploi (c'est-à-dire qu'il n'est pas nécessaire de publier deux fois à tous les auditeurs).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X