使用redis + cluster

时间:2015-11-11 17:20:13

标签: node.js nginx amazon-ec2 socket.io load-balancing

目前,我面临的任务是必须使用Amazon EC2扩展Node.js应用。根据我的理解,这样做的方法是让每个子服务器使用集群使用所有可用的进程,并具有粘性连接以确保连接到服务器的每个用户都被记住"关于他们以前的会议目前是什么样的工作人员。

在这样做之后,我所知道的下一个最佳举措是根据需要部署尽可能多的服务器,并使用nginx在所有服务器之间进行负载平衡,再次使用粘性连接来了解哪个" child"每个用户数据都在的服务器。

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

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

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

下面是我在一台计算机上使用所有CPU进行单独Node.js过程的代码:

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 个答案:

答案 0 :(得分:26)

我相信你的一般理解是正确的,虽然我想发表一些意见:

负载均衡

你是正确的,一种做负载平衡的方法是在不同实例之间使用nginx负载平衡,并且每个实例内部在它创建的工作进程之间具有集群平衡。然而,这只是一种方式,并不一定总是最好的方式。

实例之间

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

ELB的一个特点是,它与你的问题特别相关,它支持粘性会话out of the box - 只需标记一个复选框。

但是,我必须添加一个主要警告,即ELB可以在bizarre ways中破坏socket.io。如果你只是使用长轮询你应该没问题(假设启用了粘性会话),但实际的websockets工作是介于非常令人沮丧和不可能之间。

进程之间

虽然使用群集有许多替代方案,within节点和without,我倾向于认同群集本身通常都很好。

但是,工作的一种情况是,当您想要在负载均衡器后面进行粘性会话时,就像您在此处所做的那样。

首先,应该明确指出,首先你甚至需要粘性会话的唯一原因是因为socket.io依赖存储在内存中的会话数据来处理请求之间(在websockets握手期间,或者基本上整个长期投票)。一般来说,出于各种原因,应尽可能避免依赖以这种方式存储的数据,但使用socket.io,您实际上无法做出选择。

现在,这似乎并不太糟糕,因为集群可以支持粘性会话,使用socket.io的sticky-session中提到的documentation模块或{{ 3}}你似乎在使用。

问题是,由于这些粘性会话基于客户端的IP,因此它们不会在负载均衡器后面工作,无论是nginx,ELB还是其他任何东西,因为那时在实例中可见的所有内容都是< em>负载均衡器的 IP。您的代码尝试哈希的remoteAddress实际上并不是客户端的地址。

也就是说,当您的节点代码尝试充当进程之间的负载均衡器时,它尝试使用的IP将始终是另一个负载均衡器的IP,它在实例。因此,所有请求都将在同一进程中结束,从而破坏集群的整体目的。

您可以在此snippet中查看此问题的详细信息以及解决此问题的几种方法(其中没有一个特别漂亮)。

Redis的重要性

正如我之前提到的,一旦您有多个实例/进程接收来自用户的请求,会话数据的内存存储就不再足够了。粘性会话是一种可行的方法,尽管存在其他可以说是更好的解决方案,其中包括Redis可以提供的中央会话存储。请参阅此question以获得有关该主题的全面评论。

看到你的问题是关于socket.io,但我认为你可能意味着Redis对websockets的特殊重要性,所以:

当您有多个socket.io服务器(实例/进程)时,给定用户将在任何给定时间仅连接到一个此类服务器。但是,任何服务器都可以随时向给定用户发送消息,甚至向所有用户发送广播,无论他们当前使用哪个服务器。

为此,socket.io支持“Adapters”,其中Redis是其中一个,允许不同的socket.io服务器相互通信。当一台服务器发出一条消息时,它会进入Redis,然后所有服务器都会看到它(Pub / Sub)并将其发送给用户,确保消息能够到达目标。

这再次在socket.io的post中解释了多个节点,在此Stack Overflow documentation中甚至可能更好。