NodeJS Cluster 模块中的网络知识

周末有人问了我一个问题,为什么pm2 本地起三个进程,运行网络服务占用同一个端口但是没有冲突,用我练习两年半的网络知识一想确实应该有这个问题不同的进程如果监听三个一样 ip+port肯定会冲突,既然没冲突三个进程端口都一样,那会不会实在不同的本地 ip 上 类似于这样[0.0.0.0, 127.0.0.1: 192.xxx.xx.xx]。后来一想这样也没法对外提供服务。

于是顺着pm2 这个工具探索了下去。

同端口不同IP的服务

用NodeJS 写一个简单的 HTTP 服务, 代码和运行结果如下,如果我们在同一个电脑上的另一个终端再运行一份可以看到报错端口占用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let http = require("http");
let ip = ''
// 或者 let ip = '0.0.0.0'
let port = 8003;

const app = http.createServer((_, resp)=>{
resp.writeHead(200);
let text = "hello world"
resp.write(text);
resp.end();
})

app.listen(port, ip, ()=>{
console.log("server start ", ip, port)
});

程序正常运行

再运行一次提示端口占用

如果我们改成,不同IP下的相同端口,程序能正常启动但是只能在对应IP上提供服务

  • 使用IP 127.0.0.1
1
2
3
4
5
6
7
8
9
10
11
let http = require("http");
let ip = '127.0.0.1';
let port = 8003;
const app = http.createServer((_, resp)=>{
resp.writeHead(200);
let text = "hello world"
resp.write(text);
resp.end();
}).listen(port,ip, ()=>{
console.log("server start ", ip, port)
});
  • 使用 IP 192.168.140.134
1
2
3
4
5
6
7
8
9
10
11
let http = require("http");
let ip = '192.168.140.134';
let port = 8003;
const app = http.createServer((_, resp)=>{
resp.writeHead(200);
let text = "hello world"
resp.write(text)
resp.end();
}).listen(port,ip, ()=>{
console.log("server start ", ip, port)
});

不同 IP 多端口

使用pm2

如果我们使用 pm2 启动我们默认 IP 下的 服务同时启动三个实例,三个实例都正常启动无端口占用

1
pm2 start ./net_0.js -i 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let http = require("http");
let ip = ''
// 或者 let ip = '0.0.0.0'
let port = 8003;

const app = http.createServer((_, resp)=>{
resp.writeHead(200);
let text = "hello world"
resp.write(text);
resp.end();
})

app.listen(port, ip, ()=>{
console.log("server start ", ip, port)
});

从图片上看 pm2 启动了三个进程 分别是 63579 63580 63593netstat -natp 显示占用用网络8003端口的程序PID 是6400,是一个 pm2 的程序。其他三个进程虽然启动并运行了代码,但是并没有产生端口占用 (listen 没建立 socket 连接)

  • 从 pm2 的日志中看3个进程的listen都被成功执行了

通过查看 进程 6400 和进程 63579 的文件描述符(fd)进一步验证

  • lsof -p 6400

  • lsof -p 63579

确实只有进程6400有对应端口的监听,63579并没有和网络相关的fd信息,因为 pm2 使用的是 NodeJS cluster 模块 实现的这一功能,继续探索 cluster 是怎么做到的。

Cluster 模块分析

同样的我们使用 cluster 启动一个主进程,再 fork 3个worker进程,模拟使用pm2启动程序的场景。

  • server.js 文件内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let http = require("http");
let port = 8003;

console.log("server::process.pid::", process.pid);

const app = http.createServer((_, resp)=>{
resp.writeHead(200);
let text = "hello world"
resp.write(text);
resp.end();
})

app.listen(port, ()=>{
console.log("server start ", port)
});
  • cluster.js 文件内容
1
2
3
4
5
6
7
8
9
10
11
12
const cluster = require("cluster");
const WORKER_COUNT = 3;

// 或者 cluster.isPrimary
if(cluster.isMaster) {
console.log("main::process.pid::", process.pid);
for(let i = 0; i < WORKER_COUNT; i++){
cluster.fork()
}
} else {
require("./server")
}
1
node ./cluster.js

从结果上看和pm2 一致 main process 65523 占用了端口,其他worker process ,被fork出来之后 listen 执行了,但是使用的是同一端口。

cluster 运行 pid

网络端口占用

进一步查阅资料发现在 net 模块中有对 cluster 做相应的处理 (源码).在 cluster 模式中如果 isPrimary = false(child process), 调用的是 cluster._getServer 获取主进程的 server handler 并且监听它。

1
2
3
4
5
6
7
8
9
10
11
12
function listenInCluster() {

if (cluster.isPrimary || exclusive) {
server._listen2(address, port, addressType, backlog, fd, flags);
return;
}

// Get the primary's server handle, and listen on it
cluster._getServer(server, serverQuery, listenOnPrimaryHandle);

}

子进程中的 cluster._getServer 是通过 向 master 发送 IPC 消息 act: 'queryServer' 获取到 main process 的 handle,primary 中对 queryServer 的处理可查看源码

cluster._getServer = function(obj, options, cb) {

  const message = {
    act: 'queryServer',
    index,
    data: null,
    ...options
  };


  send(message, (reply, handle) => {
    if (typeof obj._setServerData === 'function')
      obj._setServerData(reply.data);

    if (handle) {
      // Shared listen socket
      shared(reply, { handle, indexesKey, index }, cb);
    } else {
      // Round-robin.
      rr(reply, { indexesKey, index }, cb);
    }
  });

};

通过上述分析得出结论: cluster 多个进程能共享一个网络端口的原因是因为,child process 在处理 listen的时候,通过 IPC 获取到了 main process 的 handle,因此其实是服用了 main process 的资源,实现了不同进程的端口复用。

处理TCP连接

PM2 默认采用的方式是在主进程接收到 socket 连接后交给 worker 处理 socket 文件描述符,即accept 操作发生在worker进程中,至于交给哪个 worker处理 采用的是 Round-Robin 算法。

pm2 连接

文章作者: I年少有为
文章链接: https://lemonlife.top/2023/02/04/nodejs-cluster-pm2/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 I年少有为