2012-08-28 45 views
7

Tengo varios servidores socket.io escalados horizontalmente usando un redistore. Tengo las salas configuradas de manera efectiva y puedo transmitir con éxito a salas de servidores, etc. Ahora intento crear una página de estado y lo que estoy fallando al descubrir es cómo simplemente contar la cantidad de usuarios conectados a través de ella. todos los servidores.Contando usuarios de socket.io en servidores horizontales

io.sockets.clients ('room') y io.sockets.sockets solo le informarán la cantidad de clientes conectados en ese servidor, no todos los servidores conectados a la misma RedisStore.

Sugerencias?

Gracias.

+0

¿Por qué no solo consultar cada uno de los servidores y sumar la cantidad de clientes conectados? – k00k

+0

Yo también estoy buscando una manera de responder a esta pregunta, sin tener que configurar algún tipo de observador para ello. FWIW, sin embargo, parece que la lógica es que cada servidor conoce a todos los clientes conectados a todos los servidores, pero también puede tener clientes obsoletos que se desconectaron de otro servidor. Parece que socket.io no creía que valiera la pena sobrecargar servidores antiguos en otros servidores, sino que algunos servidores simplemente transmitirán a algunos vacíos. – Konklone

Respuesta

1

He resuelto esto teniendo cada servidor establece periódicamente un recuento de usuario en Redis con una caducidad que incluía su propio PID:

cada hacer setex userCount:<pid> <interval+10> <count>

entonces el servidor de estado se puede consultar para cada una de estas teclas, y luego obtener los valores para cada clave:

para cada keys userCount* hacer + total = get <key>

por lo que si un servidor se bloquea o se apaga a continuación, sus recuentos bajarán o ut de redis después del intervalo + 10

perdón por el feo pseudocódigo. :)

+0

¿Cómo obtiene el recuento de los usuarios de cada servidor? El resultado de io.sockets.clients(). Length no siempre es correcto. Por ejemplo: 1. El proceso A se está ejecutando y se conectan 2 clientes. io.sockets.clients(). length devolverá correctamente 2. 2. Inicie un nuevo proceso, B, y conecte 2 clientes a él. B devolverá 2, sin embargo A devolverá 4 porque se ha suscrito a los eventos de conexión de B. Los recuentos parecen ser aún más inexactos cuando intenta reiniciar un servidor y los clientes vuelven a conectarse. – evilcelery

+1

Estoy usando Object.keys (io.sockets.sockets) .length, pero parece crecer y no encogerse con precisión, tal vez por las mismas razones que describe. Así que tuve que conectarme a nuestro sistema de presencia para obtener un conteo preciso. Para eso, guardamos nuestro objeto de usuario en redis usando socket.set y luego actualizamos ese objeto con actividad o inactividad. Entonces, para contar lo que estoy haciendo ahora es colocar los sockets de io.sockets.sockets y si el estado de presencia del usuario es 'activo', entonces los agrego al conteo. – rbrc

3

Cuando un usuario se conecta a la sala de chat, puede incrementar atómicamente un contador de usuario en su RedisStore. Cuando un usuario se desconecta, disminuye el valor. De esta forma, Redis mantiene el recuento de usuarios y todos los servidores pueden acceder a él.

Ver INCR y DECR

SET userCount = "0" 

Cuando un usuario se conecta:

INCR userCount 

Cuando un usuario se desconecta:

DECR userCount 
+2

excepto si un servidor falla, entonces esos recuentos pierden sentido – rbrc

+1

Puede mantener un conteo separado para cada servidor y resumirlos. Si un servidor deja de funcionar, configure el contador de ese servidor a 0. – JamesOR

+0

que requeriría un proceso separado que haga un seguimiento de los servidores y arregle los recuentos para ellos. Realmente esperaba que hubiera un método puramente socket.io para hacer esto. – rbrc

0

usted podría utilizar claves hash para almacenar los valores.

Cuando un usuario se conecta al servidor 1, puede establecer un campo llamado "srv1" en una clave llamada "userCounts". Simplemente anule el valor de lo que sea que use el recuento actual HSET. No es necesario incrementar/disminuir. Simplemente configure el valor actual conocido por socket.io.

HSET userCounts srv1 "5" 

Cuando otro usuario se conecta a un servidor diferente, establezca un campo diferente.

HSET userCounts srv2 "10" 

Entonces, cualquier servidor puede obtener el total mediante la devolución de todos los campos de "userCounts" y sumándolos usando HVALS para devolver una lista de valores.

HVALS userCounts 

Cuando se bloquea un servidor que necesita para ejecutar una secuencia de comandos en respuesta a la caída que elimina el campo de dicho servidor de userCounts o HSEt a "0".

Puede consultar Forever para automatizar el reinicio del servidor.

+0

Utilizo advenedizo para reiniciar los servidores, que funciona mucho mejor que para siempre (que he excavado un poco). Estoy tratando de dar cuenta de una falla completa del servidor, lo que sucede de vez en cuando. Tengo monitoreo para eso (zabbix), pero conseguir zabbix para informar al tablero cuando un servidor falla me parece bastante complicado. – rbrc

+0

Aunque quizás establecer la caducidad en estos valores redis podría hacerlo ... – rbrc

+0

Desafortunadamente, la caducidad solo está disponible en las teclas y no en los campos individuales. Pero tal vez puedas resolver algo con una combinación de teclas y campos. – JamesOR

3

Así es como lo resolví usando Redis scripting. Requiere la versión 2.6 o posterior, por lo que probablemente aún requiera compilar su propia instancia por el momento.

Cada vez que se inicia un proceso, genero un nuevo UUID y lo dejo en el alcance global. Podría usar el pid, pero esto se siente un poco más seguro.

# Pardon my coffeescript 
processId = require('node-uuid').v4() 

Cuando un usuario se conecta (el evento de conexión socket.io), que luego empuje Identificación de ese usuario en una lista de usuarios en función de que processId. También establecí el vencimiento de esa clave en 30 segundos.

RedisClient.lpush "process:#{processId}", user._id 
RedisClient.expire "process:#{processId}", 30 

Cuando un usuario se desconecta (el evento de desconexión), lo elimino y actualizo la caducidad.

RedisClient.lrem "process:#{processId}", 1, user._id 
RedisClient.expire "process:#{processId}", 30 

También configuro una función que se ejecuta en un intervalo de 30 segundos para esencialmente "hacer ping" esa clave para que permanezca allí. Entonces, si el proceso muere accidentalmente, todas esas sesiones de usuario desaparecerán esencialmente.

setInterval -> 
    RedisClient.expire "process:#{processId}", 30 
, 30 * 1000 

Ahora para la magia. Redis 2.6 incluye secuencias de comandos LUA, que básicamente proporciona una especie de funcionalidad de procedimiento almacenado. Es realmente rápido y no requiere mucho del procesador (lo comparan con "casi" ejecutar el código C).

Mi procedimiento almacenado recorre básicamente todas las listas de procesos y crea una clave user_id de usuario con su recuento total de inicios de sesión actuales. Esto significa que si están conectados con dos navegadores, etc., todavía me permitirá usar la lógica para decir si se han desconectado por completo, o solo una de sus sesiones.

Ejecuto esta función cada 15 segundos en todos mis procesos, y también después de un evento de conexión/desconexión. Esto significa que mis recuentos de usuarios probablemente serán precisos al segundo y nunca incorrectos durante más de 15 a 30 segundos.

El código para generar esa función Redis parece:

def = require("promised-io/promise").Deferred 

reconcileSha = -> 
    reconcileFunction = " 
    local keys_to_remove = redis.call('KEYS', 'user:*') 
    for i=1, #keys_to_remove do 
     redis.call('DEL', keys_to_remove[i]) 
    end 

    local processes = redis.call('KEYS', 'process:*') 
    for i=1, #processes do 
     local users_in_process = redis.call('LRANGE', processes[i], 0, -1) 
     for j=1, #users_in_process do 
     redis.call('INCR', 'user:' .. users_in_process[j]) 
     end 
    end 
    " 

    dfd = new def() 
    RedisClient.script 'load', reconcileFunction, (err, res) -> 
    dfd.resolve(res) 
    dfd.promise 

y luego puedo utilizar eso en mi guión más tarde con:

reconcileSha().then (sha) -> 
    RedisClient.evalsha sha, 0, (err, res) -> 
    # do stuff 

La última cosa que hago es tratar de manejar algunos eventos de apagado para asegurarse de que el proceso intente lo mejor es no confiar en los tiempos de espera de redis y en realidad se cierra con gracia.

gracefulShutdown = (callback) -> 
    console.log "shutdown" 
    reconcileSha().then (sha) -> 
    RedisClient.del("process:#{processId}") 
    RedisClient.evalsha sha, 0, (err, res) -> 
     callback() if callback? 

# For ctrl-c 
process.once 'SIGINT', -> 
    gracefulShutdown -> 
    process.kill(process.pid, 'SIGINT') 

# For nodemon 
process.once 'SIGUSR2', -> 
    gracefulShutdown -> 
    process.kill(process.pid, 'SIGUSR2') 

Hasta ahora ha estado funcionando genial.

Una cosa que todavía quiero hacer es hacer que la función redis devuelva cualquier clave que haya cambiado sus valores. De esta forma, podría enviar un evento si los conteos han cambiado para un usuario en particular sin que ninguno de los servidores lo sepa activamente (como si un proceso muere). Por ahora, tengo que confiar en sondear al usuario: * valores nuevamente para saber que ha cambiado. Funciona, pero podría ser mejor ...

+0

Esa es una implementación interesante. ¿Te preocupa el costo del ping de 30 segundos si tienes 10k + clientes conectados? – rbrc

+0

No realmente. Todavía no se ha probado en esa medida. Redis se está convirtiendo en el componente secundario más importante de mi aplicación, por lo que el servidor obtendrá los recursos necesarios para mantenerlo en funcionamiento. Si puedo ver que las instancias de la aplicación no están fallando mucho, podría tomar otro enfoque que no sea tan costoso. –