群集模块丢失server.close()上的共享net.Server句柄

时间:2016-05-04 19:23:32

标签: node.js

我运行的服务器/客户端应用程序使用核心nettls模块打开侦听器。每个客户端与服务器的连接都会导致服务器分支新工作人员与客户端进行交互。这可能导致多个工作人员在同一端口上侦听。通常这不是问题,因为连接在两者之间分配,并且它们都在内部执行相同的任务。如果worker 1在端口15000上创建了一个监听器,则创建worker 2并在端口15000上启动它自己的监听器,没有任何问题。

但是,如果worker 1在端口15000上创建了一个侦听器,将其关闭,然后再次打开它(相当典型的用例),然后创建worker 2并尝试在端口15000上创建一个侦听器,然后worker 2收到EADDRINUSE错误。

这似乎是由整个集群失去了这个服务器句柄应该在所有工作者之间共享以及新创建的监听器仅由创建它的工作进程所拥有的想法引起的,因此该端口不可用其他工人。 (也许这不是实际的端口共享如何跨集群工作者工作,这只是我的一个误解。)

我写过这个小问题,展示了这个问题。它每隔10秒就会派一名新工人,每个工人将在16000端口创建一个监听器,15秒后将其拆除,然后在15秒后再在端口16000上创建另一个监听器:

var cluster = require('cluster');
var net     = require('net');

if (cluster.isMaster) {
    setInterval(function() {
        cluster.fork();
    }, 10000)
} else {
    var workerID = cluster.worker.id;
    var server;
    var setup = function() {
        console.log('Worker ' + workerID + ' setting up listener');
        server = net.createServer(function(stream) {});
        server.on('error', function(err) {
            console.log('Error on worker ' + workerID, err);
            console.log('Worker ' + workerID + ' exiting')
            process.exit();
        });
        server.listen(16000);
        setTimeout(teardown,15000);
    }
    var teardown = function() {
        console.log('Worker ' + workerID + ' closing listener');
        server.close();
        setTimeout(setup, 15000);
    }
    setup();
}

当worker 1尝试重新创建监听器时(当循环继续时后续的工作者),会看到该行为:

Worker 1 setting up listener
Worker 2 setting up listener
Worker 1 closing listener
Worker 3 setting up listener
Worker 2 closing listener
Worker 1 setting up listener
Error on worker 1 { [Error: bind EADDRINUSE null:16000]
  code: 'EADDRINUSE',
  errno: 'EADDRINUSE',
  syscall: 'bind',
  address: null,
  port: 16000 }
Worker 1 exiting
Worker 4 setting up listener
Worker 3 closing listener
Worker 2 setting up listener
Error on worker 2 { [Error: bind EADDRINUSE null:16000]
  code: 'EADDRINUSE',
  errno: 'EADDRINUSE',
  syscall: 'bind',
  address: null,
  port: 16000 }
Worker 2 exiting
...

知道为什么会发生这种情况或是否有解决方法?

编辑以添加其他测试用例

有些反馈提到,此处的初始测试用例可能会产生污染事件循环堆栈的问题。我已经将测试更新到以下内容,删除了大部分超时,只留下了证明手头问题的必要条件。

这个新的测试用例将每100毫秒产生一个新的工作者。每个工作人员将在端口16000上建立一个监听器。工作者10在成功建立监听器之后,将设置超时以在1s之后拆除其监听器。在工人10调用拆卸功能时,应该有18个其他工作人员在端口16000上进行监听。工人10将不会再次成功建立监听器。

在这种特殊情况下,如果成功关闭和尝试收听之间没有延迟,该工作人员将使主人不再能够分叉新工人;然而,即使向工作人员10的重试尝试添加500毫秒的超时(这将允许主人继续分叉工作人员),工人10仍然不会成功建立监听器。通过超时,新分叉的工作人员都能够成功建立监听器。

var cluster = require('cluster');
var net     = require('net');

if (cluster.isMaster) {
    cluster.fork();
    setInterval(function(){cluster.fork()},100);
} else {
    var workerID = cluster.worker.id;
    var server;
    var setup = function() {
        console.log('Worker ' + workerID + ' setting up listener');
        server = net.createServer(function(stream) {});
        server.on('error', function(err) {
            console.log('Error on worker ' + workerID, err);
            teardown();
        });
        if (workerID == 10) {
            server.listen(16000, function() {
                setTimeout(teardown, 1000);
            });
        } else {
            server.listen(16000);
        }
    }
    var teardown = function() {
        console.log('Worker ' + workerID + ' closing listener');
        server.close(setup);
    }
    setup();
}

此测试产生以下输出:

Worker 1 setting up listener
Worker 2 setting up listener
Worker 3 setting up listener
Worker 4 setting up listener
Worker 5 setting up listener
Worker 6 setting up listener
Worker 7 setting up listener
Worker 8 setting up listener
Worker 9 setting up listener
Worker 10 setting up listener
Worker 11 setting up listener
Worker 12 setting up listener
Worker 13 setting up listener
Worker 14 setting up listener
Worker 15 setting up listener
Worker 16 setting up listener
Worker 17 setting up listener
Worker 18 setting up listener
Worker 19 setting up listener
Worker 10 closing listener
Worker 10 setting up listener
Error on worker 10 { [Error: bind EADDRINUSE null:16000]
  code: 'EADDRINUSE',
  errno: 'EADDRINUSE',
  syscall: 'bind',
  address: null,
  port: 16000 }
Worker 10 closing listener
Worker 10 setting up listener
Error on worker 10 { [Error: bind EADDRINUSE null:16000]
  code: 'EADDRINUSE',
  errno: 'EADDRINUSE',
  syscall: 'bind',
  address: null,
  port: 16000 }
Worker 10 closing listener

1 个答案:

答案 0 :(得分:1)

与您调试后,我提交了这个答案。

没有为worker指定地址会导致地址在发送到listner函数时为null。因此,提供唯一的地址允许模块识别正确的工作人员。但是当它为null时,它无法识别在调用server.close()时终止的正确工作程序