计算水平服务器上的socket.io用户

时间:2012-08-28 17:52:28

标签: node.js redis socket.io scalability

我有多个使用redisstore水平扩展的socket.io服务器。我已经有效地设置了房间,并且成功地能够通过服务器等广播到房间。现在我正在尝试建立一个状态页面,我正在努力弄清楚如何简单地计算连接的用户数量所有服务器。

io.sockets.clients('room')和io.sockets.sockets只会告诉您该服务器上已连接客户端的数量,而不是所有连接到同一RedisStore的服务器。

建议?

感谢。

4 个答案:

答案 0 :(得分:3)

当用户连接到聊天室时,您可以在RedisStore中以原子方式递增用户计数器。当用户断开连接时,您将减少该值。这样,Redis可以维护用户数,所有服务器都可以访问。

请参阅INCRDECR

SET userCount = "0"

当用户连接时:

INCR userCount

当用户断开连接时:

DECR userCount

答案 1 :(得分:3)

以下是我使用Redis脚本解决它的方法。它需要2.6或更高版本,所以很可能现在仍然需要编译自己的实例。

每次进程启动时,我都会生成一个新的UUID并将其保留在全局范围内。我可以使用pid,但这感觉更安全一点。

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

当用户连接(socket.io连接事件)时,我会根据该processId将该用户的id推送到用户列表中。我还将该密钥的到期时间设置为30秒。

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

当用户断开连接(断开连接事件)时,我将其删除并更新到期日。

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

我还设置了一个以30秒的间隔运行的功能,基本上“ping”该键,使其保持在那里。因此,如果该过程意外死亡,那么所有这些用户会话基本上都会消失。

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

现在为了魔法。 Redis 2.6包括LUA脚本,它本质上提供了存储过程的功能。它非常快,而且处理器密集程度不高(他们将其与“差不多”运行的C代码进行比较)。

我的存储过程基本上遍历所有进程列表,并创建一个user:user_id键及其当前登录的总数。这意味着如果他们使用两个浏览器等登录,它仍然允许我使用逻辑来判断他们是否完全断开连接,或只是他们的一个会话。

我在所有进程上每隔15秒运行一次此函数,并且在连接/断开连接事件之后也是如此。这意味着我的用户计数很可能精确到秒,并且永远不会错误超过15到30秒。

生成该redis函数的代码如下所示:

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

然后我可以在我的脚本中使用它:

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

我做的最后一件事是尝试处理一些关闭事件,以确保进程尝试最好不依赖redis超时并实际上正常关闭。

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

到目前为止,它一直很好用。

我还想做的一件事就是让redis函数返回任何已更改其值的键。这样,如果没有任何服务器主动知道的特定用户的计数已经改变(例如,如果进程死亡),我实际上可以发送事件。现在,我必须依赖轮询用户:*值再次知道它已被更改。它有效,但它可能会更好......

答案 2 :(得分:1)

我解决了这个问题,让每个服务器定期在redis中设置用户数,其中包含自己的pid:

每次setex userCount:<pid> <interval+10> <count>

然后状态服务器可以查询每个密钥,然后获取每个密钥的值:

每个keys userCount*总计+ = get <key>

因此,如果服务器崩溃或关闭,那么在间隔+ 10后,其计数将从redis中退出

抱歉丑陋的伪代码。 :)

答案 3 :(得分:0)

您可以使用哈希键来存储值。

当用户连接到服务器1时,您可以在名为“userCounts”的键上设置名为“srv1”的字段。只需将值覆盖为使用HSET的当前计数。无需递增/递减。只需设置socket.io已知的当前值。

HSET userCounts srv1 "5"

当另一个用户连接到另一个服务器时,设置一个不同的字段。

HSET userCounts srv2 "10"

然后,任何服务器都可以通过返回“userCounts”中的所有字段并使用HVALS将它们添加到一起来获取总数来返回值列表。

HVALS userCounts

当服务器崩溃时,你需要运行一个脚本来响应崩溃,从userCounts中删除该服务器的字段或将其HSET转换为“0”。

您可以查看Forever以自动重启服务器。