跳到主要内容

请求分发

2024年04月09日
柏拉文
越努力,越幸运

一、请求分发


主进程master, 用来接收浏览器发送的请求,然后fork几个子进程并把请求转发到这几个子进程中。

我们知道一个node主进程,都有几个子线程来配合完成事件循环,这些子线程跑在其他cpu中,也就是说nodejs也用到了其他核的cpu,并不是一个完全单核的情况。这里在fork子进程的时候,其实没有必要每个cpu都要fork一个子进程,因为有些cpu就是为了处理事件循环的,现在你占满了整个cpu,反而让事件循环没有得到及时的处理,所以效果并不好。 同时,每次fork一个子进程,相当于主进程复制了一遍,把内存空间,代码都复制了一份,那么内存就成倍的消耗,但是带来的效果并没有成倍的增长,所以我们一般fork一半的核数

const os = require("os");
const http = require("http");
const cluster = require("cluster");

if(cluster.isMaster){
// 主进程: 开启子进程, 子线程数量为 CPU 核心数的一半即可
const cpuHalfLength = os.cpus().length >>> 1;

for(let i=0; i<cpuHalfLength; i++){
cluster.fork();
}
}else{
// 子进程: 创建服务
http.createServer((req, res) => {
res.writeHead(200);
res.end("hello world");
console.log(`进程 ${process.pid} 处理服务中...`);
}).listen(4000);
}

二、进程守护


主进程通过 cluster.on('exit') 监控子进程, 如果有一个子进程退出, 延时具体时间后重新 fork 一个子进程。在子进程中通过 uncaughtException 来捕获子进程错误, 发生错误后,及时退出进程。

const os = require("os");
const http = require("http");
const cluster = require("cluster");

if(cluster.isMaster){
// 主进程: 开启子进程, 子线程数量为 CPU 核心数的一半即可
const cpuHalfLength = os.cpus().length >>> 1;

for(let i=0; i<cpuHalfLength; i++){
cluster.fork();
}

// 主进程监控子进程
cluster.on("exit", ()=>{
// 如果子进程一挂掉,我们又重新fork一个,当子进程一直挂掉,我们这里就不断的fork,这样会导致一直占用cpu,最后直接挂掉,所以需要延迟下再fork
setTimeout(()=>{
cluster.fork();
}, 5000);
});

}else{
// 子进程: 创建服务
http.createServer((req, res) => {
res.writeHead(200);
res.end("hello world");
console.log(`进程 ${process.pid} 处理服务中...`);
}).listen(4000);

// 子进程: 捕获错误, 并退出进程
process.on("uncaughtException", (err) => {
console.log(err);
process.exit(1);
});
}

三、内存泄漏


通过 process.memoryUsage().rss 每隔一段时间来检测子进程内存, 如果发生内存泄漏就会导致老生代越来越大,从而垃圾回收遍历的时候耗时越来越多,服务器越来越慢,所以对内存使用情况进行监控,一旦大于某个值就认为发生了内存泄漏,然后杀掉。

const os = require("os");
const http = require("http");
const cluster = require("cluster");

if(cluster.isMaster){
// 主进程: 开启子进程, 子线程数量为 CPU 核心数的一半即可
const cpuHalfLength = os.cpus().length >>> 1;

for(let i=0; i<cpuHalfLength; i++){
cluster.fork();
}

// 主进程监控子进程
cluster.on("exit", ()=>{
// 如果子进程一挂掉,我们又重新fork一个,当子进程一直挂掉,我们这里就不断的fork,这样会导致一直占用cpu,最后直接挂掉,所以需要延迟下再fork
setTimeout(()=>{
cluster.fork();
}, 5000);
});

}else{
// 子进程: 创建服务
http.createServer((req, res) => {
res.writeHead(200);
res.end("hello world");
console.log(`进程 ${process.pid} 处理服务中...`);
}).listen(4000);

// 子进程: 捕获错误, 并退出进程
process.on("uncaughtException", (err) => {
console.log(err);
process.exit(1);
});

setInterval(() => {
if (process.memoryUsage().rss > 534003200) {
console.log('oom')
process.exit(1)
}
}, 5000)
}

四、僵尸进程-心跳检测


如果父进程给子进程发送消息,有操作三次子进程没有回复,说明这个子进程是一个僵尸进程。什么情况下会导致僵尸进程呢,比如子进程中有个死循环导致这个进程无法接受任何请求。

const os = require("os");
const http = require("http");
const cluster = require("cluster");

if(cluster.isMaster){
// 主进程: 开启子进程, 子线程数量为 CPU 核心数的一半即可
const cpuHalfLength = os.cpus().length >>> 1;

for(let i=0; i<cpuHalfLength; i++){
const worker = cluster.fork();
// 对子进程进行心跳检测
let missedPing = 0;
let inter = setInterval(()=>{
worker.send('ping');
missedPing++;

// 如果发送了三次消息,子进程没有回复,那么就认为是僵尸进程,杀掉
if(missedPing >= 3){
clearInterval(inter);
process.kill(worker.process.pid)
}
}, 5000);

// 接收子进程的消息
worker.on('message', msg=>{
if(msg === 'pong'){
missedPing--;
}
});
}

// 主进程监控子进程
cluster.on("exit", ()=>{
// 如果子进程一挂掉,我们又重新fork一个,当子进程一直挂掉,我们这里就不断的fork,这样会导致一直占用cpu,最后直接挂掉,所以需要延迟下再fork
setTimeout(()=>{
cluster.fork();
}, 5000);
});

}else{
// 子进程: 创建服务
http.createServer((req, res) => {
res.writeHead(200);
res.end("hello world");
console.log(`进程 ${process.pid} 处理服务中...`);
}).listen(4000);

// 子进程: 捕获错误, 并退出进程
process.on("uncaughtException", (err) => {
console.log(err);
process.exit(1);
});

process.on("message", msg=>{
if(msg === 'ping'){
process.send('pong');
}
});

setInterval(() => {
if (process.memoryUsage().rss > 534003200) {
console.log('oom')
process.exit(1)
}
}, 5000)
}