Counting socket.io users on horizontal servers

I have several socket.io servers scaled horizontally using redisstore. I have room settings efficiently, and I can successfully translate to rooms on different servers, etc. Now I am trying to create a status page, and I cannot figure out how easy it is to count the number of users connected through all servers.

io.sockets.clients ('room') and io.sockets.sockets will report only the number of connected clients on this server, and not all servers connected to the same RedisStore.

Suggestions?

Thanks.

+7
source share
4 answers

I solved this when each server periodically set a user counter in redis with an expiration that included their own pid:

each do setex userCount:<pid> <interval+10> <count>

then the status server can query for each of these keys, and then get the values ​​for each key:

for each keys userCount* do total + = get <key>

therefore, if the server crashes or shuts down, its number will drop out of redis after an interval of + 10

sorry for the ugly pseudocode. :)

+1
source

When a user connects to the chat, you can atomize the user counter in your RedisStore. When a user logs off, you decrease the value. Thus, Redis supports user counting and is available for all servers.

See INCR and DECR

 SET userCount = "0" 

When a user connects:

 INCR userCount 

When the user disconnects:

 DECR userCount 
+3
source

Here's how I solved it using Redis scripts. This requires version 2.6 or later, so it is likely that you still need to compile your own instance.

Every time the process starts, I generate a new UUID and leave it in the global scope. I could use pid, but this is a little better.

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

When a user connects (socket.io connection event), I then push this user id into the list of users based on this processId. I also set the expiration of this key to 30 seconds.

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

When the user disconnects (disconnect event), I delete him and update the expiration date.

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

I also set up a function that runs on a 30 second interval to essentially “ping” this key so that it stays there. Therefore, if the process accidentally dies, all of these user sessions will substantially disappear.

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

Now for the magic. Redis 2.6 includes LUA scripts, which essentially provide the functionality of a stored procedure. This is very fast and not very intensive for the processor (they compare it with the "almost" running C code).

My stored procedure basically goes through all the process lists and creates the user: user_id key with the total number of current logins. This means that if they are logged in with two browsers, etc., it will still allow me to use the logic to find out if they are completely disconnected or just one of their sessions.

I run this function every 15 seconds in all my processes, and also after the connect / disconnect event. This means that my user’s count will most likely be accurate for the second one and will never be more than 15-30 seconds.

The code for generating this redis function is as follows:

 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 

And then I can use this in my script later:

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

The last thing I do is try to handle some shutdown events to make sure that the process tries not to rely on redis timeouts and is actually gracefully shutting down.

 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') 

While it works fine.

One thing I still want to do is make the redis function return any keys that have changed their values. That way, I could really send an event if the counts changed for a specific user without any active servers (for example, if the process dies). At the moment, I have to rely on a user survey: * Values ​​again to know that it has changed. It works, but it could be better ...

+3
source

You can use hash keys to store values.

When a user connects to server 1, you can set a field called "srv1" to a key called "userCounts". Just redefine the value until the current account uses HSET . No need to increase / decrease. Just set the current value known as socket.io.

 HSET userCounts srv1 "5" 

When another user connects to another server, set a different field.

 HSET userCounts srv2 "10" 

Then any server can get the final value by returning all the fields from "userCounts" and adding them together, using HVALS to return the list value.

 HVALS userCounts 

If the server crashes, you will need to run the script in response to an alarm that removes this server field from userCounts or HSET so that it is "0".

You can watch Forever to automate server restart.

0
source

All Articles