Node进程与线程

17 2月

进程是操作系统运行程序单元,拥有独立的资源(如内存)。线程是进程内运算的单元,所有线程共享进程内的资源,多核CPU可以同时进行多个线程操作。

Node的主线程负责运行Chrome V8引擎执行JS,其他子线程通过事件循环(Event Loop)被调度。

  • child_process
  • worker_threads
  • cluster
  • 进程守护

子进程

因为是主线程执行JS,单线程只能利用一个CPU,如果主线程内JS执行非常耗时,会造成资源浪费。此时可以用子进程child_process创建多个主线程环境,充分利用多核CPU。

主进程创建子进程后,可以将各种参数send给子进程去处理,让子进程分担一些执行JS任务的工作。

// master.js
const cp = require('child_process')

const child_process = cp.fork(__dirname + '/child.js')   // 创建子进程
child_process.send('i am master')
child_process.on('message', str => {  // 子进程处理完会通知主进程,这里监听下
    console.log(str)
})


// child.js
process.on('message', str => {
    // 处理JS任务
    console.log(str)
    process.send('i am child')  // 处理完通知主进程
})

工作线程

Node v10版后提供了工作线程worker_threads的能力,因为Node内置的异步IO操作,加上子进程能力已经能让开发者充分利用多核CPU了,所以拖的比较晚才推出工作线程。

cluster

子进程可以充分利用多核CPU。例如4核CPU,主进程创建3个子进程,子进程内提供http服务,主进程收到http request后,分发给子进程,子进程处理完后将http response的结果通知给主进程,再由主进程返给客户端。即利用多核CPU的能力,每个核里都运行着一个http服务。

因为Node常用来提供http服务,所以上述例子封装成了内置cluster模块。

const cluster = require('cluster')
const os = require('os')

if (cluster.isMaster) { // 主进程走分发逻辑
    // 因为Node本来就要占据几个CPU去处理内置事件循环中的子线程,所以不要将CPU全占满,除2差不多。具体留多少CPU余量,要根据压测数据找个最佳比
    for (let i = 0; i < os.cpus().length / 2; i++) {  
        cluster.fork()
    }
} else {    // 子进程走http服务逻辑
    require('./app')
}

进程守护

JS是一门运行时语言,很多错误会在执行时爆出来,而不是编译时,等上线后服务端出问题就晚了。尤其前后端同构项目,例如在服务端访问window对象,又没有try-catch会导致服务崩溃,所以需要对进程守护。

  • 子进程出现“未捕获异常”,要及时退出子进程
  • 子进程出现“内存泄露”,要及时退出子进程
  • 子进程出现“心跳无反应”,要及时退出子进程
  • 主进程里监听到子进程退出消息后,再创建新子进程(tips:可以延迟几秒再创建新子进程)
const cluster = require('cluster')
const os = require('os')

if (cluster.isMaster) { // 主进程走分发逻辑
    // 因为Node本来就要占据几个CPU去处理内置事件循环中的子线程,所以不要将CPU全占满,除2差不多。具体留多少CPU余量,要根据压测数据找个最佳比
    for (let i = 0; i < os.cpus().length / 2; i++) {  
        createWorker()
    }

    cluster.on('exit', function () {    // 当子进程崩溃退出后,主进程可以再创建个新的子进程
        // 如果子进程代码很容易崩溃,一崩溃马上创建新的子进程会不停崩溃不停新建,消耗大量CPU直至服务彻底崩溃,所以可以设置个5s超时
        setTimeout(() => {
            createWorker()
        }, 5000)
    })

    function createWorker() {
        // 创建子进程并进行心跳监控
        let worker = cluster.fork()

        let missed = 0      // 子进程没有回应的ping次数
        
        let timer = setInterval(function () {   // 每10s主进程向子进程发送一下心跳
            if (missed === 3) {
                clearInterval(timer)
                console.log(worker.process.pid + ' 没有心跳,疑似进入僵尸状态')
                process.kill(worker.process.pid)
                return
            }
            missed++
            worker.send('ping#' + worker.process.pid)
        }, 1000)

        worker.on('message', function (msg) {   // 主进程监听子进程的心跳回应
            if (msg === 'pong#' + worker.process.pid) {
                missed--
            }
        })

        worker.on('exit', function () {
            clearInterval(timer)
        })
    }
} else {    // 子进程走http服务逻辑
    process.on('uncaughtException', function (err) {    // 当子进程出现会崩溃的错误
        console.log(err)
        process.exit(1)
    })

    process.on('message', function (msg) {          // 子进程回应心跳信息
        if (msg === 'ping#' + process.pid) {
            process.send('pong#' + process.pid);
        }
    });

    if (process.memoryUsage().rss > 734003200) {    // 子进程里内存使用超过700m,认为发生了内存泄露,就退出子进程
        console.log('内存占用过多,疑似内存泄露')
        process.exit(1)
    }
    require('./app')
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注