web-dev-qa-db-fra.com

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

Lorsque vous associez 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 des événements de serveur pouvant gérer plusieurs transports, il semble y avoir trois approches:

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

  2. 'createClient' une connexion redis. Sur la connexion client socket.io, joignez le client dans une salle socket.io et abonnez-vous aux canaux redis appropriés. Inclure redis.on ("message", ...) à l'intérieur de la fermeture de la connexion client et à la réception du message, appelez client.emit ("événement", données) 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 cuit dans socket.io et "broadcast" à partir du canal de "répartition" unique dans Redis en suivant le protocole socketio-spec.

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

Cependant, dans 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 diffusés le plus rapidement possible. Les performances sont mesurées par les timings des clients connectés (basés sur socket.io-client qui enregistrent les horodatages 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 sur 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 sur le client concerné. Dans les deux cas, le serveur n'atteint pas le deuxième événement de message tant qu'il n'a pas été communiqué à tous les clients connectés. Une situation clairement exacerbée par l'augmentation de la concurrence.

Cela semble en contradiction avec la sagesse perçue des capacités des piles. Je veux croire, mais je me bats.

Est-ce que ce scénario (distribution à faible latence d'un volume élevé de messages) n'est tout simplement pas une option avec ces outils (encore?), Ou ai-je raté une astuce?

63
lebear

Je pensais que c'était une question raisonnable et je l'avais recherchée brièvement il y a quelque temps. J'ai passé un peu de temps à chercher des exemples sur lesquels vous pourriez trouver des conseils utiles.

Exemples

J'aime commencer par des exemples simples:

L'échantillon léger est une seule page (notez que vous voudrez remplacer redis-node-client par quelque chose comme node_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");

Les documents

Il existe une tonne de documentation et les API 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 la pile:

Conseils notables (ymmv)

Désactivez ou optimisez le pool de sockets, utilisez des liaisons efficaces, surveillez la latence et assurez-vous de ne pas dupliquer le travail (c'est-à-dire pas besoin de publier deux fois pour tous les écouteurs).

30
Mark Essel