cdpc

child process management

Usage no npm install needed!

<script type="module">
  import cdpc from 'https://cdn.skypack.dev/cdpc';
</script>

README

cdpc:坚强的进程管理模块

Node.js环境的进程管理模块。用于针对不同进程的管理工作。可管理任何需要托管的程序。需要注意的是,此扩展目前不提供cluster功能,它利用child_process模块的spawn接口完成子进程的创建工作。

在Web服务中,需要用到cluster模块,有以下解决方案:

  • 自行实现,最简单的示例,使用cluster也就几行代码。

  • 使用titbit框架,此框架内置cluster支持,支持自动负载和监控。

  • 使用其他框架再配合相关扩展。

需要明确的是:

  • 此扩展是为了开发工作而设计,不是提供一个命令去管理程序。

  • 基于此扩展设计进程管理的命令也很容易,并且有一个基于此实现的cdpc命令和服务,具体参考cdpcmd。

  • 同一个命令,同样的参数不能重复。

  • 切忌不要把多个服务监听同一个端口。

示例配置中涉及到的Web服务文件,需要自行编写测试程序,使用任何你熟悉的方式都可以。

开发此扩展的原因

其主要原因是因为我在titbit中已经实现了cluster模式自动管理子进程。而基于worker_threads也可以实现多线程管理的模型。但是还需要一个既能和它们配合使用还可以单独使用的进程管理模块,可以整合多个Web应用,还可以管理脚本、编译的二进制程序等。

另一个原因是,cluster模式由于其内部实现机制比较复杂,考虑到不同用户权限导致的问题,在Linux/Unix上,子进程和父进程必须是相同的uid和gid,如果master进程是root身份,则worker进程也是root身份,所以对worker更改uid和gid会失败。

若要综合实现各种需求,那么这个扩展和web框架再结合cluster是一个利器。

特点

它很简单并强大,而且还很稳定,提供了简单的接口控制子进程的终止和启动,可以在运行时删除和添加子进程服务。

你可以进行嵌套式管理:调用此模块去管理另一个文件,另一个文件中还使用了此模块。

如此反复,可以实现任意复杂的多进程多线程模型。当然我不建议你做的太复杂,尽可能扁平化最好管理。

示例

通过调用runChilds,传递一个配置数组即可,每个元素就是一个要启动的子进程配置说明。


'use strict'

const cdpc = require('cdpc')

//开启strong模式,监听'uncaughtException' 和 'unhandledRejection'事件不退出。
//还可以传递参数设置自定义监听函数,两个监听函数对应的事件顺序就是:
//    'uncaughtException' 'unhandledRejection'

let cm = new cdpc({
  debug: true,
  //收到SIGTERM、SIGABRT、SIGINT信号不退出。
  notExit: true
})

cm.strong()

cm.runChilds([

    {
        name : 'api',
        file : 'app.js',
        args : ['--port', 2021],
        options : {
            stdio: ['ignore', 1, 2]
        }
    },

    {
        name : 'test',
        command : 'date',
        restart : 'count',
        restartLimit: 10,
        restartDelay: 1000,
        options : {
            stdio: ['ignore', 1, 2]
        }
    }
])

子进程配置选项详细说明

配置项 说明 必须 可选值
name 子进程应用的名称 自定义,建议名称必写,方便管理。
command 要运行的命令 若是运行js文件,默认会使用当前node版本。
file 要运行的js文件路径 快捷选项,最终会把此选项指定的文件放在args中作为参数。
args 运行命令要传递的参数 默认为空,具体传递参数自行定义。
options spawn接口的options选项 参考child_process.spawn文档。
callback 创建子进程后的回调函数 回调函数传递的第一个参数是spawn的返回值,就是ChildProcess实例,第二个参数是cdpc实例。
onError error事件的回调函数 方便错误处理提供的选项,所有事件回调都可以在callback中自行定义。
restart 重启模式 默认为always,可选值:always,count,none,fail,fail-count。
user 指定以某个用户身份运行 只针对Linux、类Unix有效,指定的用户必须在/etc/passwd中有记录。
group 指定以某个用户组身份运行 只针对Linux、类Unix有效,指定的用户组必须在/etc/group中有记录。
monitor 是否开启监控,开启后会监控此进程的CPU、内存 true或false。
stopTimeout stop应用之后的定时器毫秒数值 取值范围:5 ~ 600000。包括边界值。
restartDelay 重启延迟 毫秒数,默认为延迟1000毫秒重启。
restartLimit 重启上限 当restart模式为count,则会通过计数和此值比较。
lockReload 设置为true,重加载配置不会停止此应用 有些应用是直接通过runChilds运行的,并无配置文件,若触发reLoadConfig操作则导致这些应用不会再次加载,此时lockReload可以避免它们被停止。

如果配置项monitor设置为true,需要调用monitorStart开启监控。

file和所在路径

当你指定file的时候,会自动在创建子进程的时候让子进程的工作目录在file文件所在目录。

restart模式

  • always 表示总是重启。
  • count 是表示重启次数有上限。
  • none 表示不重启。
  • fail 表示失败后重启,通过检测退出状态码(code)是不是为0。
  • fail-count 表示失败重启计数,只有在检测退出状态码不是0,并且计数不超过限制的情况下才会重启。

关于user和group

如果直接设定options的uid和gid会比较麻烦,需要去查看文件,而不同发行版或者同一发行版的不同版本其用户的uid和gid也可能不同。

Linux上默认就有很多为服务提供的系统用户,比如在各个不同发行版中基本都会有nobody、www、www-data等经常用于Web服务的系统用户。所以通过名称来指定用户和用户组是更好的选择。

当指定了user和group,则会自动去对应的配置文件解析查找出对应的uid和gid,如果不指定group,则使用user默认的uid和gid。

Linux发行版默认的user和group文件路径:

  • /etc/passwd

  • /etc/group

如果你使用的Linux发行版对目录结构做了调整,可以通过配置来指定:


//假设你使用的系统把默认的配置文件放在了/usr/etc。

let cm = new cdpc({
  userFile: '/usr/etc/passwd',
  groupFile: '/usr/etc/group'
})

自定义事件

若要针对创建的子进程做事件处理,则可以在callback中完成,示例:

'use strict'

const cdpc = require('cdpc')

let cm = cdpc({
  debug: true
})

cm.runChilds([

    {
        name : 'testapp',
        command : 'date',
        restart: 'count',
        restartLimit: 10,
        restartDelay: 1000,
        callback: (ch) => {
            ch.stdout.on('data', data => {
                console.log(data.toString())
            })
        }
    }

])

指定用户和用户组

'use strict'

const cdpc = require('cdpc')

let cm = cdpc({
  debug: true
})

cm.runChilds([

    {
        name : 'web-service',
        file : 'app.js'
        user : 'www-data',
        group: 'www-data',
        options : {
            stdio : ['ignore', 1, 2]
        }
    }
])

其中的app.js是你使用web框架编写的服务程序。无论是直接通过options指定uid和gid还是使用user和group指定用户名,只有root用户有权限这样做,所以这个程序必须以root身份运行才可以成功,你需要用sudo。

假设以上代码的文件名是chld.js:

sudo node chld.js

name选项和应用管理

以下演示的pause、resume、stop、start、remove、restart都是基于name的,也就是说你要给应用命名。

暂停和恢复、停止和启动

'use strict'

const cdpc = require('cdpc')


let cm = cdpc({
  debug: true
})

cm.runChilds([

  {
      name : 'tofile',
      file : 'tofile.js',
      user : 'www',
      options : {
        stdio: ['ignore', 1, 2]
      }
  },

])

//5秒之后暂停tofile应用,此时应用程序不销毁,还在内存里。
setTimeout(() => {
  cm.pause('tofile')
}, 5000)

//15秒后恢复tofile应用,resume用于恢复pause暂停的应用。
setTimeout(() => {
  cm.resume('tofile')
}, 15000)

//25秒后停止tofile应用,此时应用销毁,子进程停止。
setTimeout(() => {
  cm.stop('tofile')
}, 25000)

//35秒后启动tofile应用,start用于启动stop停止的应用。
setTimeout(() => {
  cm.start('tofile')
}, 35000)

//45秒后重启tofile应用。
setTimeout(() => {
  cm.restart('tofile')
}, 45000)

stop和清理工作

stop接口会向指定的应用发送SIGTERM信号。5秒后检测是否还在运行,仍然运行则发送SIGKILL信号。

一个细节问题是,如果我使用了stop停止服务,但是如果服务子进程有一些资源需要清理,或者还需要向它自己的子进程发送通知,该如何处理?

子进程监听SIGTERM信号,当收到此信号,表示要退出,可用于后续任务安排后再选择退出。

stop支持第二个参数用于指定多少毫秒后检测是否运行并发送SIGKILL信号,默认为5000毫秒。若需要灵活的配置,可以在子进程配置项中通过stopTimeout指定。

移除和添加应用

remove通过name指定的名字来移除应用,移除应用是一个暴力操作,如果要安全移除,可以使用safeRemove,safeRemove会先进行stop,然后默认在5秒后进行remove操作。

safeRemove仍然支持第二个参数作为定时器超时检测的毫秒数值,safeRemove内部调用了stop,子进程配置项的stopTimout仍然会对此起作用。

add用于在运行时动态添加应用。

示例:

'use strict'

const cdpc = require('cdpc')


let cm = cdpc({
  debug: true
})

cm.runChilds([

  {
      name : 'tofile',
      file : 'tofile.js',
      user : 'wy',
      options : {
        stdio: ['ignore', 1, 2]
      }
  },

  {
      name : 'tofile2',
      file : 'tofile.js',
      user : 'wy',
      args : ['--port', 1235, '--https', '--session'],
      options : {
        stdio: ['ignore', 1, 2]
      }
  },

])

//25秒后安全移除tofile2应用,并添加subchld应用。
//subchld应用同样是一个使用cdpc模块管理子进程的应用。
//其内部应用也是几个Web服务程序。
setTimeout(() => {
  cm.safeRemove('tofile2')

  cm.add({
      name : 'subchld',
      file : 'mchld.js',
      user: 'www-data',
      group: 'www-data',
      options : {
        stdio: ['ignore', 1, 2]
      }
  })
}, 25000)

cdpc初始化选项

配置项 说明 可选值
debug 是否启用调试模式。 true或false
signalHandle 信号处理函数,不设置采用默认处理,参考process.on的信号事件。 函数,接收参数signal
onExit process的exit事件回调函数,不设置则采用默认处理。 函数
signals 要处理的信号有哪些,默认为SIGINT、SIGABRT、SIGTERM 数组,示例:['SIGABRT', 'SIGTERM']
errorHandle 统一的错误处理函数,接收参数第一个是error,第二个是错误描述的辅助标记名称。 函数,示例:(err, errname) => {}
eventDir fs.watch事件目录,默认/tmp/cdpc_watch 对重新加载配置、重启、暂停、恢复应用等操作的文件事件目录。
notExit 不退出应用,默认为false,设置为true则会监听信号不退出。 若自定义signalHandle,则需要自行处理。
config 配置文件路径,配置格式和runChilds接收参数一致。 json或js类型,若是js则必须用module.exports导出模块。
loadInfoType 负载信息的格式,json格式主要用于程序解析。 text或json。
loadInfoFile 负载信息的写入文件路径。 若是不设置则输出到终端。
showColor 在终端输出是否显示颜色。 true或false。
userFile Linux用户信息文件路径,默认为/etc/passwd。 若非特殊发行版或更改了配置路径不要修改此值。
groupFile Linux用户组信息文件路径,默认为/etc/group。 若非特殊发行版或更改了配置路径不要修改此值。
notWatch 不监听文件事件。 true或false。若为true则eventDir设置的事件目录不再起作用。

以配置文件的方式加载。


const cdpc = require('cdpc')

const cm = new cdpc({
  debug: true,
  config: './config.js'
})

cm.loadConfig()

开启IPC

若要使用IPC通信,需要使用options.stdio选项:


const cdpc = require('cdpc')

const cm = new cdpc({
  debug: true,
})

cm.runChilds([
  {
    name: 'app',
    file: 'query-load.js',
    options: {
      //如果不需要输出信息,也可以是 ['ignore', 'ignore', 'ignore', 'ipc']
      stdio: ['ignore', 1, 2, 'ipc']
    },
    //cdp是就是cdpc实例,若是在配置文件中,独立出去的模块,这个参数可以让你操作cdpc实例上的接口。
    callback: (child, cdp) => {
      child.on('message', msg => {
        if (msg.type === 'query-load') {
          //把json格式的负载监控信息发送给子进程。
          child.send(cdp.fmtLoadInfo('json'))
        }
      })
    }
  }
])

对应的query-load.js文件的代码是:


'use strict'

const fs = require('fs')

process.on('message', msg => {
  fs.writeFile('/tmp/query-load.json', JSON.stringify(msg), err => {
    err && console.error(err)
  })
})

setInterval(() => {
  process.send && process.send({
    type: 'query-load'
  })
}, 1000)

启用监控


let cm = new cdpc()

//...

//默认以1000毫秒的间隔获取负载信息。
cm.monitorStart()

monitorStart接受一个整数作为定时器的毫秒数值,默认为1000。

monitorStart允许的取值范围是100~2000。不合法则会使用默认值。