Node.js 强制只有一个线程执行代码

7
当我启动我的应用程序node app.js时,运行的进程只有1个线程。然而,它运行的时间越长,为该进程创建的线程就越多。问题在于,当我想要执行特定类型的代码时,例如:
var io = require('socket.io')(process.env.PORT);

它失败了,因为信号来自多个线程,因此代码没有成功执行。
简单的测试,如果有人这样做:
var io = require('socket.io')(9001);
var io = require('socket.io')(9002);
var io = require('socket.io')(9003);
var io = require('socket.io')(9004);

它能够正常工作,但是这段代码:

var cPort = 9001;
setInterval(function() {
    var io = require('socket.io')(cPort);
    cPort++;
}, 1000 * 60 * 2); // 1 sec * 60 seconds * 2 = 2 minutes interval

由于在2分钟后,节点将有许多线程并且它们都尝试执行代码,因此不会执行代码-结果您将看到错误:地址正在使用

那么,尽管运行了同一文件的多线程进程,我如何强制节点仅执行此代码一次?

06.11.2017 编辑 ----

澄清问题:

我的意思是,在问题上,我没有资源问题,如果我同时启动所有服务器(例如40个服务器),它们都可以成功启动并无限期地工作。如果我只启动一个服务器,然后运行自动启动更多服务器的代码,则此时总是会出现地址正在使用错误,当然显然在代码执行时地址没有被占用。目前,当人们使用服务较多并且一周中的其他日子里服务器较少时,我必须在周末手动启动更多服务器,我希望创建基于人口的自动化系统,以启动和关闭服务器。

这是服务器启动的代码:

var cp = require('child_process'),
    servers = [],
    per_server = config.per_server,
    check_servers = function(callback) {
        for(var i = 0; i < servers.length; i++) {
            callback(i, servers[i]);
        }
    };

this.add_server = function(port) {
    var server = {
        port: port,
        load: 0,
        process: cp.fork(__dirname + '/../server_instance.js', [], {
            env: {
                port: port
            }
        })
    };

    server.process.on('message', function(message) {
        server.load = message.load;
    });

    servers.push(server);
};

this.find_server = function() {
    var min = Infinity,
        port = false;

    check_servers(function(index, details) {
        if(details.load < min) {
            min = details.load;
            port = details.port;
        }
    });

    return port;
};

现在如果我连续执行 controller.add_server() 40 次,它将正确启动 40 个服务器,但如果我这样做:

var start_port = 3185;
setInterval(function() {
    var min = Infinity;

    check_servers(function(index, details) {
        if(details.load < min) {
            min = details.load;
        }
    });

    if(min > config.per_server) {
        controller.add_server(start_port);
        start_port++;
    }
}, 5000);

我会随机地在第二、第三或第四个服务器创建时遇到地址已被使用的错误。
07.11.2017编辑 ----
如建议所述,我尝试了以下端口扫描/查找库:
- portfinder - portscanner - scan-ports 只有使用第一个库,我才能启动至少两个服务器,这是我使用的代码:
setInterval(function() {
    var min = Infinity;

    check_servers(function(index, details) {
        if(details.load < min) {
            min = details.load;
        }
    });

    if(min > per_server) {
        _self.add_server();
    }
}, 5000);

var portfinder = require('portfinder');
portfinder.basePort = 3185;

this.add_server = function() {
    portfinder.getPortPromise()
        .then((port) => {
            console.log('port found', port);

            var server = {
                port: port,
                load: 0,
                process: cp.fork(__dirname + '/../server_instance.js', [], {
                    env: {
                        port: port
                    }
                })
            };

            server.process.on('message', function(message) {
                server.load = message.load;
            });

            servers.push(server);

        })
        .catch((err) => {
            console.log('error happened');
        });
};

经过多次测试,看起来我可以启动2个服务器,然后随机地,在第三或第四次尝试时崩溃。很明显问题比端口查找更深入,这个库只是告诉我我已经知道的东西,我知道哪些端口是开放的,并在脚本尝试使用手动netstat -anp | grep PORT命令启动服务器之前进行了双重检查。
因此,很明显问题不在于查找打开的端口,从结果来看,似乎node正在尝试从单个命令中多次启动服务器。
跟进编辑 ----
添加server_instance.js代码:
var io = require('socket.io')(process.env.port),
    connections_current = 0,
    connections_made = 0,
    connections_dropped = 0;

io.on('connection', function(socket) {

    connections_current++;
    connections_made++;

    // ... service logic here, not relevant (like query db, send data to users etc)

    socket.on('disconnect', function() {
        connections_current--;
        connections_dropped++;
    });

});

setInterval(function() {
    process.send({
        load: connections_current
    });
}, 5000);

2017年08月11日编辑 ----

我测试了许多解决方案来解决这个问题,我观察到了这种情况:

  • 在Mac OS X上的本地测试中,我可以生成最多3000个连接到服务器。错误从未发生过,节点有1个进程6个线程用于路由文件。使用3000个连接,我甚至可以生成200个服务器而没有任何问题。

  • 在Linux Debian上的服务器测试中,我生成了2百万个连接到服务器。当我连接所有人时,错误总是发生在第3或第4个服务器实例上,节点有6个进程每个进程的10个线程用于路由文件。

这显然是问题的根源,我拥有的容量越大,节点产生的进程就越多,尝试启动新服务器时它会更快地重叠。


你的意图非常不明确。你的 setInterval() 代码会无限运行,启动越来越多的服务器,直到耗尽服务器端资源。 - jfriend00
@jfriend00 可能会为 https://stackoverflow.com/questions/47071894/socket-io-dynamically-start-servers-to-listen 提供一些启示 - Alex Blex
@AlexBlex - 这是一个被删除的问题,也不太描述代码的最终目标。而且,这个问题需要清楚地描述出它自己在这里的问题。 - jfriend00
@Mevia,间隔的停止位在哪里?你的问题是这是一个未经检查的无限循环,它注定会耗尽所有可用端口。 - Tarun Lalwani
我有些困惑。首先,nodejs是单线程的。我按照您的示例进行了测试,使用1秒的时间间隔,它可以正常工作。也许在您的完整程序中,您没有正确关闭旧的服务器套接字,或者在尝试重用它们之前等待其完全关闭。 - Joshua Kifer
显示剩余5条评论
2个回答

1
最好的解决方案是在主进程中生成端口号,然后将它们传递给工作进程,以避免它们重叠。

此外,您可以使用npm模块(如test-port-provider)检查端口是否正在使用并获取空闲端口。


0
您可以使用portfinder包在系统中发现可用的网络端口(从端口8000开始发现)。使用方法很简单:
const http = require('http');
const portfinder = require('portfinder');
const pid = process.pid;


portfinder.getPort((err, port) => {
    if (err)
        throw err;

    http.createServer((req, res) => {         
        res.end(`Response from server ${pid}.\n`);
    }).listen(port, () => {
        console.log(`Server ${pid} running on port ${port}...`);
    });    
});



** 编辑 **

看起来portfinder返回了多个相同的端口,因此会抛出EADDRINUSE错误。我怀疑的是当portfinder尝试查找新端口时,该端口还没有在监听(因此返回相同的端口),但是通过简单的for循环启动多个服务器似乎可以正常工作,这似乎与我的怀疑相矛盾:

for (let i = 0; i < max_number_of_servers; ++i) {
    this.add_server();
}


你的代码一个简单的修复方法是在每次调用add_server时增加portfinder的基地址:

portfinder.basePort = 8000;

this.add_server = function() {
        portfinder.getPortPromise()
        .then((port) => {   

            portfinder.basePort += 1;

            var server = {
                port: port,
                load: 0,
                process: cp.fork('server_instance.js', [], {
                    env: {
                        port: port
                    }
                })
            };

            server.process.on('message', function(message) {
                server.load = message.load;
                console.log("message");
            });

            servers.push(server);

        })
        .catch((err) => {
            console.log(err);  
        });   
};

这段代码在我的机器上似乎运行良好。


无论如何,我建议您考虑不同的实现方式。如果您发现在最高流量情况下需要 N 台服务器才能正确处理所有请求,那么没有必要创建较少数量的服务器,然后根据当前流量动态更改它,原因如下:

  • 启动一个新进程是一项昂贵的操作,可能需要一些时间才能正常运行。
  • 在高流量情况下,所有服务器都已准备好为请求提供服务,无需额外延迟。
  • 在低/中等流量情况下,您的服务器将不会过载,但您可以获得更好的弹性和可用性(如果服务器进程崩溃,由于有许多其他服务器可以提供请求,因此您可以启动一个新的服务器进程,这需要一些时间)。


你可以使用本地的集群模块,轻松构建一个具有自动负载平衡和容错功能的分布式服务器应用程序。默认情况下,clusteer模块执行循环算法来分配工作进程中的传入请求,因此您可以免费获得负载平衡!
一个可能简单的实现(仅供测试,我使用了不同的端口查找包):

// main.js

const cluster = require('cluster');
const getPort = require('get-port');
const max_servers = 40;

// master process
if (cluster.isMaster) {
    for (let i = 0; i < max_servers; ++i) {
        getPort().then(port => {
            cluster.fork({port: port});
        })          
    }
    // detect exit event on workers
    cluster.on("exit", (worker, errCode) => {
        console.log(worker);
        // start new worker in case of crashes
        if (errCode != 0 && !worker.suicide) {
            console.log("Worker-server crashed. Starting new worker...");
            getPort().then(port => {
                cluster.fork({port: port});
            })
        }
    });
}
// worker process --> start server
else {
    require('./server_instance.js'); // [2]
}

// server_instance.js

const http = require("http");
const pid = process.pid;
let port = process.env.port;

console.log(`Starting server on process ${pid} running on port ${port}...`);

let io = require('socket.io')(process.env.port),
    connections_current = 0,
    connections_made = 0,
    connections_dropped = 0;

io.on('connection', function(socket) {
    console.log(`Socket.io on process ${pid} running on port ${port}...`);
    connections_current++;
    connections_made++;

    // ... service logic here, not relevant (like query db, send data to users etc)

    socket.on('disconnect', function() {
        connections_current--;
        connections_dropped++;
    });

}); 

我对这个解决方案抱有很高的期望,但不幸的是它跳过了端口,仍然导致“地址已在使用”的错误。例如,它在端口31853186上打开服务器,然后在打开3190之后尝试打开3200时崩溃了。每次测试都返回不同的端口并且会崩溃,在测试期间所有端口都会手动检查,使用命令netstat -anp | grep [PORT] - Mevia
如果您在portfinder设置中将默认基本端口设置为8000,是否会出现任何错误?我在您的代码中看到您将基本端口设置为3185:portfinder.basePort = 3185 - revy
我尝试了许多设置:默认、3185、6000、21500,但没有任何变化。 - Mevia
我尝试了那个解决方案,主要问题是与我的解决方案相同,它们都适用于少量用户,如果你有1000个用户的流量,那么就不会出错。但是当你连接实际容量,大约1-2百万用户时,你可以在htop中观察到节点创建越来越多的进程和线程,然后错误发生。因此,这个解决方案对于低容量流量与我提出的解决方案一样有效,高容量则需要其他方法。请查看我的问题更新以获取更多有关问题的详细信息。 - Mevia
无论如何,您使用 get-port 包来发现可用端口的实现对我很有效,我能够在 1000 个不同的<进程、端口>上启动 1000 个服务器而没有任何问题(尽管这没有任何意义)。 - revy
显示剩余6条评论

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