使用集群将 Socket.IO 扩展到多个 Node.js 进程
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/18310635/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Scaling Socket.IO to multiple Node.js processes using cluster
提问by Lee Benson
Tearing my hair out with this one... has anyone managed to scale Socket.IOto multiple "worker" processes spawned by Node.js's clustermodule?
用这个撕掉我的头发......有没有人设法将Socket.IO 扩展到由 Node.js 的集群模块产生的多个“工作”进程?
Lets say I have the following on fourworker processes (pseudo):
假设我有以下四个工作进程(伪):
// on the server
var express = require('express');
var server = express();
var socket = require('socket.io');
var io = socket.listen(server);
// socket.io
io.set('store', new socket.RedisStore);
// set-up connections...
io.sockets.on('connection', function(socket) {
socket.on('join', function(rooms) {
rooms.forEach(function(room) {
socket.join(room);
});
});
socket.on('leave', function(rooms) {
rooms.forEach(function(room) {
socket.leave(room);
});
});
});
// Emit a message every second
function send() {
io.sockets.in('room').emit('data', 'howdy');
}
setInterval(send, 1000);
And on the browser...
然后在浏览器...
// on the client
socket = io.connect();
socket.emit('join', ['room']);
socket.on('data', function(data){
console.log(data);
});
The problem:Every second, I'm receiving fourmessages, due to four separate worker processes sending the messages.
问题:由于四个单独的工作进程发送消息,我每秒都会收到四条消息。
How do I ensure the message is only sent once?
如何确保消息只发送一次?
回答by hexacyanide
Edit:In Socket.IO 1.0+, rather than setting a store with multiple Redis clients, a simpler Redis adapter module can now be used.
编辑:在 Socket.IO 1.0+ 中,现在可以使用更简单的 Redis 适配器模块,而不是设置具有多个 Redis 客户端的存储。
var io = require('socket.io')(3000);
var redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379 }));
The example shown below would look more like this:
下面显示的示例看起来更像这样:
var cluster = require('cluster');
var os = require('os');
if (cluster.isMaster) {
// we create a HTTP server, but we do not use listen
// that way, we have a socket.io server that doesn't accept connections
var server = require('http').createServer();
var io = require('socket.io').listen(server);
var redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379 }));
setInterval(function() {
// all workers will receive this in Redis, and emit
io.emit('data', 'payload');
}, 1000);
for (var i = 0; i < os.cpus().length; i++) {
cluster.fork();
}
cluster.on('exit', function(worker, code, signal) {
console.log('worker ' + worker.process.pid + ' died');
});
}
if (cluster.isWorker) {
var express = require('express');
var app = express();
var http = require('http');
var server = http.createServer(app);
var io = require('socket.io').listen(server);
var redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379 }));
io.on('connection', function(socket) {
socket.emit('data', 'connected to worker: ' + cluster.worker.id);
});
app.listen(80);
}
If you have a master node that needs to publish to other Socket.IO processes, but doesn't accept socket connections itself, use socket.io-emitterinstead of socket.io-redis.
如果您有一个主节点需要发布到其他 Socket.IO 进程,但本身不接受套接字连接,请使用socket.io-emitter而不是socket.io-redis。
If you are having trouble scaling, run your Node applications with DEBUG=*. Socket.IO now implements debugwhich will also print out Redis adapter debug messages. Example output:
如果您在扩展时遇到问题,请使用DEBUG=*. Socket.IO 现在实现了debug,它也会打印出 Redis 适配器调试消息。示例输出:
socket.io:server initializing namespace / +0ms
socket.io:server creating engine.io instance with opts {"path":"/socket.io"} +2ms
socket.io:server attaching client serving req handler +2ms
socket.io-parser encoding packet {"type":2,"data":["event","payload"],"nsp":"/"} +0ms
socket.io-parser encoded {"type":2,"data":["event","payload"],"nsp":"/"} as 2["event","payload"] +1ms
socket.io-redis ignore same uid +0ms
If both your master and child processes both display the same parser messages, then your application is properly scaling.
如果您的主进程和子进程都显示相同的解析器消息,那么您的应用程序正在正确扩展。
There shouldn't be a problem with your setup if you are emitting from a single worker. What you're doing is emitting from all four workers, and due to Redis publish/subscribe, the messages aren't duplicated, but written four times, as you asked the application to do. Here's a simple diagram of what Redis does:
如果您是从单个工作人员发出的,那么您的设置应该没有问题。您正在做的是从所有四个工作人员发出,并且由于 Redis 发布/订阅,消息不会重复,而是按照您要求应用程序的方式写入四次。这是Redis所做的简单图表:
Client <-- Worker 1 emit --> Redis
Client <-- Worker 2 <----------|
Client <-- Worker 3 <----------|
Client <-- Worker 4 <----------|
As you can see, when you emit from a worker, it will publish the emit to Redis, and it will be mirrored from other workers, which have subscribed to the Redis database. This also means you can use multiple socket servers connected the the same instance, and an emit on one server will be fired on all connected servers.
如您所见,当您从一个 worker 发出时,它会将发出的内容发布到 Redis,并且会从其他订阅了 Redis 数据库的 worker 镜像。这也意味着您可以使用连接到同一实例的多个套接字服务器,并且在所有连接的服务器上都会触发一台服务器上的发射。
With cluster, when a client connects, it will connect to one of your four workers, not all four. That also means anything you emit from that worker will only be shown once to the client. So yes, the application is scaling, but the way you're doing it, you're emitting from all four workers, and the Redis database is making it as if you were calling it four times on a single worker. If a client actually connected to all four of your socket instances, they'd be receiving sixteen messages a second, not four.
使用集群,当客户端连接时,它将连接到您的四个工作人员之一,而不是全部四个。这也意味着您从该工作人员发出的任何内容都只会向客户端显示一次。所以是的,应用程序正在扩展,但是你这样做的方式,你从所有四个工作人员发出,Redis 数据库使它好像你在一个工作人员上调用它四次。如果客户端实际上连接到所有四个套接字实例,他们将每秒接收 16 条消息,而不是 4 条。
The type of socket handling depends on the type of application you're going to have. If you're going to handle clients individually, then you should have no problem, because the connection event will only fire for one worker per one client. If you need a global "heartbeat", then you could have a socket handler in your master process. Since workers die when the master process dies, you should offset the connection load off of the master process, and let the children handle connections. Here's an example:
套接字处理的类型取决于您将拥有的应用程序类型。如果您要单独处理客户端,那么您应该没有问题,因为连接事件只会为每个客户端的一个工作人员触发。如果您需要全局“心跳”,那么您可以在主进程中有一个套接字处理程序。由于工人在主进程死亡时死亡,因此您应该抵消主进程的连接负载,并让子进程处理连接。下面是一个例子:
var cluster = require('cluster');
var os = require('os');
if (cluster.isMaster) {
// we create a HTTP server, but we do not use listen
// that way, we have a socket.io server that doesn't accept connections
var server = require('http').createServer();
var io = require('socket.io').listen(server);
var RedisStore = require('socket.io/lib/stores/redis');
var redis = require('socket.io/node_modules/redis');
io.set('store', new RedisStore({
redisPub: redis.createClient(),
redisSub: redis.createClient(),
redisClient: redis.createClient()
}));
setInterval(function() {
// all workers will receive this in Redis, and emit
io.sockets.emit('data', 'payload');
}, 1000);
for (var i = 0; i < os.cpus().length; i++) {
cluster.fork();
}
cluster.on('exit', function(worker, code, signal) {
console.log('worker ' + worker.process.pid + ' died');
});
}
if (cluster.isWorker) {
var express = require('express');
var app = express();
var http = require('http');
var server = http.createServer(app);
var io = require('socket.io').listen(server);
var RedisStore = require('socket.io/lib/stores/redis');
var redis = require('socket.io/node_modules/redis');
io.set('store', new RedisStore({
redisPub: redis.createClient(),
redisSub: redis.createClient(),
redisClient: redis.createClient()
}));
io.sockets.on('connection', function(socket) {
socket.emit('data', 'connected to worker: ' + cluster.worker.id);
});
app.listen(80);
}
In the example, there are five Socket.IO instances, one being the master, and four being the children. The master server never calls listen()so there is no connection overhead on that process. However, if you call an emit on the master process, it will be published to Redis, and the four worker processes will perform the emit on their clients. This offsets connection load to workers, and if a worker were to die, your main application logic would be untouched in the master.
在示例中,有五个 Socket.IO 实例,一个是主实例,四个是子实例。主服务器从不调用,listen()因此该进程没有连接开销。但是,如果您在主进程上调用一个发射,它将被发布到 Redis,四个工作进程将在它们的客户端上执行发射。这抵消了工作人员的连接负载,如果工作人员死亡,您的主要应用程序逻辑将在主服务器中保持不变。
Note that with Redis, all emits, even in a namespace or room will be processed by other worker processes as if you triggered the emit from that process. In other words, if you have two Socket.IO instances with one Redis instance, calling emit()on a socket in the first worker will send the data to its clients, while worker two will do the same as if you called the emit from that worker.
请注意,使用 Redis,所有发出,即使在命名空间或房间中,也将由其他工作进程处理,就好像您从该进程触发了发出一样。换句话说,如果您有两个 Socket.IO 实例和一个 Redis 实例,则调用emit()第一个工作程序中的套接字会将数据发送到其客户端,而第二个工作程序将执行相同的操作,就像您从该工作程序调用发射一样。
回答by Taner Topal
Let the master handle your heartbeat (example below) or start multiple processes on different ports internally and load balance them with nginx (which supports also websockets from V1.3 upwards).
让 master 处理您的心跳(下面的示例)或在内部启动不同端口上的多个进程,并使用 nginx(它还支持 V1.3 以上的 websockets)对它们进行负载平衡。
Cluster with Master
集群与主
// on the server
var express = require('express');
var server = express();
var socket = require('socket.io');
var io = socket.listen(server);
var cluster = require('cluster');
var numCPUs = require('os').cpus().length;
// socket.io
io.set('store', new socket.RedisStore);
// set-up connections...
io.sockets.on('connection', function(socket) {
socket.on('join', function(rooms) {
rooms.forEach(function(room) {
socket.join(room);
});
});
socket.on('leave', function(rooms) {
rooms.forEach(function(room) {
socket.leave(room);
});
});
});
if (cluster.isMaster) {
// Fork workers.
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
// Emit a message every second
function send() {
console.log('howdy');
io.sockets.in('room').emit('data', 'howdy');
}
setInterval(send, 1000);
cluster.on('exit', function(worker, code, signal) {
console.log('worker ' + worker.process.pid + ' died');
});
}
回答by Aaron Dufour
This actually looks like Socket.IO succeeding at scaling. You would expect a message from one server to go to all sockets in that room, regardless of which server they happen to be connected to.
这实际上看起来像 Socket.IO 在扩展方面取得了成功。您希望来自一台服务器的消息发送到该房间中的所有套接字,而不管它们碰巧连接到哪台服务器。
Your best bet is to have one master process that sends a message each second. You can do this by only running it if cluster.isMaster, for example.
最好的办法是让一个主进程每秒发送一条消息。例如,您可以通过仅运行它来做到这一点cluster.isMaster。
回答by gdorbes
Inter-process communication is not enough to make socket.io 1.4.5 working with cluster. Forcing websocket mode is also a must. See WebSocket handshake in Node.JS, Socket.IO and Clusters not working
进程间通信不足以使 socket.io 1.4.5 与集群一起工作。强制使用 websocket 模式也是必须的。请参阅Node.JS 中的 WebSocket 握手,Socket.IO 和集群不起作用

