使用Redis和集群扩展Node.js和Socket.IO

12

目前,我面临的任务是使用Amazon EC2扩展Node.js应用程序。据我所知,做法是每个子服务器都使用集群(cluster)来使用所有可用进程,并使用粘性连接(sticky connections)确保连接到服务器的每个用户在以前的会话中“记住”他们当前所在的工作进程(worker)。

在这样做之后,从我所知道的情况来看,下一步最好的方法是部署所需的所有服务器,并使用nginx在它们之间进行负载平衡,再次使用粘性连接(sticky connections)来知道每个用户数据所在的“子”服务器。

那么当用户连接到服务器时,会发生什么呢?

客户端连接 -> 查找/选择服务器 -> 查找/选择进程 -> Socket.IO握手/连接等。

如果不是这样,请让我更好地理解这个负载平衡任务。我也不明白在这种情况下redis的重要性。

以下是我正在使用的代码,用于将一个单独的Node.js进程上的所有CPU用于另一个进程:

var express = require('express');
cluster = require('cluster'),
net = require('net'),
sio = require('socket.io'),
sio_redis = require('socket.io-redis');

var port = 3502,
num_processes = require('os').cpus().length;

if (cluster.isMaster) {
// This stores our workers. We need to keep them to be able to reference
// them based on source IP address. It's also useful for auto-restart,
// for example.
var workers = [];

// Helper function for spawning worker at index 'i'.
var spawn = function(i) {
    workers[i] = cluster.fork();

    // Optional: Restart worker on exit
    workers[i].on('exit', function(worker, code, signal) {
        console.log('respawning worker', i);
        spawn(i);
    });
};

// Spawn workers.
for (var i = 0; i < num_processes; i++) {
    spawn(i);
}

// Helper function for getting a worker index based on IP address.
// This is a hot path so it should be really fast. The way it works
// is by converting the IP address to a number by removing the dots,
// then compressing it to the number of slots we have.
//
// Compared against "real" hashing (from the sticky-session code) and
// "real" IP number conversion, this function is on par in terms of
// worker index distribution only much faster.
var worker_index = function(ip, len) {
    var s = '';
    for (var i = 0, _len = ip.length; i < _len; i++) {
        if (ip[i] !== '.') {
            s += ip[i];
        }
    }

    return Number(s) % len;
};

// Create the outside facing server listening on our port.
var server = net.createServer({ pauseOnConnect: true }, function(connection) {
    // We received a connection and need to pass it to the appropriate
    // worker. Get the worker for this connection's source IP and pass
    // it the connection.
    var worker = workers[worker_index(connection.remoteAddress, num_processes)];
    worker.send('sticky-session:connection', connection);
}).listen(port);
} else {
// Note we don't use a port here because the master listens on it for us.
var app = new express();

// Here you might use middleware, attach routes, etc.

// Don't expose our internal server to the outside.
var server = app.listen(0, 'localhost'),
    io = sio(server);

// Tell Socket.IO to use the redis adapter. By default, the redis
// server is assumed to be on localhost:6379. You don't have to
// specify them explicitly unless you want to change them.
io.adapter(sio_redis({ host: 'localhost', port: 6379 }));

// Here you might use Socket.IO middleware for authorization etc.

console.log("Listening");
// Listen to messages sent from the master. Ignore everything else.
process.on('message', function(message, connection) {
    if (message !== 'sticky-session:connection') {
        return;
    }

    // Emulate a connection event on the server by emitting the
    // event with the connection the master sent us.
    server.emit('connection', connection);

    connection.resume();
});
}
1个回答

34
我相信您的一般理解是正确的,尽管我想做一些评论:

负载均衡

您正确地指出了一种负载均衡的方法,即使用nginx在不同的实例之间进行负载均衡,在每个实例内部使用集群在创建的工作进程之间进行负载均衡。然而,这只是一种方式,并不一定总是最好的。

实例之间

首先,如果您已经在使用AWS,您可能需要考虑使用ELB。它专门为负载均衡EC2实例而设计,使得在实例之间配置负载均衡问题变得微不足道。它还提供了许多有用的功能,并且(通过Auto Scaling)可以使缩放非常动态,而无需您付出任何努力。

ELB具有一个特别与您的问题相关的功能,即它支持开箱即用的粘性会话——只需要标记一个复选框

然而,我必须添加一个重要的警告,即 ELB 可以以奇怪的方式破坏 socket.io。如果您只使用长轮询,那么应该没问题(假设启用了粘性会话),但是使实际的 WebSockets 正常工作则介于极其令人沮丧和不可能之间。
在进程之间
虽然有很多替代方案可以使用集群,在 Node 内部和外部都有,但我倾向于认为集群本身通常完全没有问题。
然而,在负载均衡器后面使用粘性会话时,它不起作用,就像您在这里所做的那样。
首先,必须明确的是,你需要使用粘性会话的唯一原因是因为 socket.io 依赖于存储在请求之间内存中的会话数据才能工作(在网页握手期间,或者基本上在整个长轮询期间)。总的来说,尽可能避免依赖以这种方式存储的数据,有许多理由,但是对于 socket.io,你没有选择。

现在,这似乎并不太糟糕,因为 cluster 可以支持粘性会话,使用 socket.io 文档中提到的 sticky-session 模块,或者你似乎正在使用的 代码片段

问题在于,由于这些粘性会话是基于客户端的 IP,所以它们在负载均衡器后面无法工作,无论是 nginx、ELB 还是其他任何东西,因为此时实例内部可见的只是负载平衡器的 IP。你的代码尝试散列的 remoteAddress 实际上根本不是客户端的地址。
即,当您的Node代码尝试在进程之间充当负载均衡器时,它尝试使用的IP将始终是负载均衡器的另一个IP,该负载均衡器在实例之间平衡。因此,所有请求最终都会到达同一个进程,这违背了集群的整个目的。
您可以在此问题中查看此问题的详细信息以及几种潜在的解决方法(其中没有一种特别美观)。

Redis的重要性

正如我之前提到的,一旦您有多个接收用户请求的实例/进程,内存中存储的会话数据就不再足够。粘性会话是一种选择,尽管存在其他更好的解决方案,其中包括中央会话存储,Redis可以提供此功能。请参见此文章,了解该主题的相当全面的评论。
鉴于您的问题是关于socket.io的,因此我假设您可能指的是Redis在WebSockets方面的特定重要性,因此:
当您拥有多个socket.io服务器(实例/进程)时,给定用户在任何给定时间只会连接到其中一个服务器。但是,任何服务器都可以随时希望向给定用户发出消息,甚至是向所有用户广播,而不管它们当前在哪个服务器下。
为此,socket.io支持“适配器”,其中Redis是其中之一,允许不同的socket.io服务器彼此通信。当一个服务器发出消息时,它进入Redis,然后所有服务器都可以看到它(发布/订阅),并将其发送给他们的用户,确保消息将达到目标。
这在socket.io关于多个节点的文档中再次解释,也许在这个Stack Overflow 答案中解释得更好。

2
我从这个答案中学到了很多,以至于我不得不表达我的感激之情,规则束缚不了我。 - Zmart
有史以来最好的答案之一 - Ahmed

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接