# 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或run，传递一个配置数组即可，每个元素就是一个要启动的子进程配置说明。

```javascript

'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中有记录。 |
| cgroup    | 指定Linux cgroups控制组，默认为空表示不做资源控制。 |
| monitor   | 是否开启监控，开启后会监控此进程的CPU、内存 | 否 | true或false。  |
| stopTimeout  | stop应用之后的定时器毫秒数值 | 否 | 取值范围：5 ～ 600000。包括边界值。 |
| restartDelay | 重启延迟 | 否 | 毫秒数，默认为延迟1000毫秒重启。 |
| restartLimit | 重启上限 | 否 | 当restart模式为count，则会通过计数和此值比较。 |
| autoRemove | 自动移除 | 否 | 只有在restart为count、fail、fail-count时有效，表示当应用运行完成后，自动移除。 |
| onceMode | 作为命令执行一次 | 否 | true或false。相当于设定了autoRemove为true、restart为count、restartLimit为0。 |
| after | 声明关系依赖 | 否 | 用于声明此应用要在哪些服务运行之后再启动。可以传递一个字符串，或字符串数组，参数是所依赖服务的名字。 |
| monitorNetData | 是否监控网络数据 | 否 | 默认是false，当开启后，会在loadinfo.net上看到网络收发数据以及一段时间内的速率。 |
| env | 环境变量，object类型 | 否 | 此配置表示扩展添加的环境，如果是options.env配置是直接覆盖。 |
| only | 是否唯一，默认为false， | 否 | 表示此进程是否需要唯一运行，比如某个服务监听某个端口必须是唯一的。 |
| onlyArgs | only为true的时候，哪些参数作为唯一标识，默认是空数组 | 否 | 默认是空数组，表示命令本身就是唯一标识。 |

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

### CDPC.prototype.run

run是runChilds的别名。

### 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。

### 对用户和组进行缓存

指定了用户和组，会读取文件进行解析，但是这是一个同步处理的过程。所以提供了一个缓存机制。

首次解析用户会把解析后的结果加入到this.linuxUsers和this.linuxGroups进行缓存，格式如下：

```javascript

//this.linuxUsers
{
  www: {uid: 123, gid: 126}
}

//this.linuxGroups
{
  www: {gid: 126}
}

```
如果想避免首次执行进程在指定用户和组的情况下去读取文件进行解析，可以自定义处理函数，预先解析好数据，并按照对应的格式，设置两个缓存变量的值。


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

- /etc/passwd

- /etc/group

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

```javascript

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

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

```

### 自定义事件

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

```javascript
'use strict'

const CDPC = require('cdpc')

let cm = new CDPC({
  debug: true
})

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

```

## 指定用户和用户组

```javascript
'use strict'

const CDPC = require('cdpc')

let cm = new CDPC({
  debug: true
})

cm.runChilds([
    {
        name: 'web-service',
        file: '/home/xx/api/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
```

user、group选项可以使用数组传递多个用户，这种方式是为了防止对应的用户身份不存在，比如web服务系统经常会运行在www或www-data用户身份，但是有的系统没有www-data，有的没有www，为了避免程序经常的更改，可以这样指定用户，用来兼容多个不同的系统。

#### 指定多个用户和用户组

这种方式，会在查到用户之后，直接返回，不会继续查找用户身份。

```javascript
'use strict'

const CDPC = require('cdpc')

let cm = new CDPC({
  debug: true
})

cm.runChilds([
    {
        name: 'web-service',
        file: '/home/xx/api/app.js',
        user: ['www-data', 'www', 'nobody'],
        group: ['www-data', 'www', 'nobody'],
        options: {
            stdio: ['ignore', 1, 2]
        }
    }
])
```

## name选项和应用管理

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


## 暂停和恢复、停止和启动

```javascript
'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用于在运行时动态添加应用。**

示例：

```javascript
'use strict'

const CDPC = require('cdpc')

let cm = new 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)

```

## 使用cgroup进行资源控制

在Linux上可以使用cgroups进行资源控制，cdpc在检测到是Linux平台会自动初始化cgroup功能模块，使用cgroup属性即可访问。

```javascript

const CDPC = require('cdpc')

let cm = new CDPC()

//创建一个名为cf-test的控制组，设定模式为domain，这是默认值，type可以不传。
cm.cgroup.create('cf-test', {
  type: 'domain',
  //在10000时间片上，占有5000，就是50%占有率
  cpu: [5000, 10000],
  //内存占用～50M
  memory: 50000000
})

cm.runChilds([
  {
    name: 'xxx',
    file: './a.js',
    cgroup: 'cf-test'
  }
])

```

## cdpc初始化选项

| 配置项 | 说明 | 可选值 |
|----|----|----|
| debug | 是否启用调试模式。 | true或false |
| signalHandle | 信号处理函数，不设置采用默认处理，参考process.on的信号事件。 | 函数，接收参数signal |
| onExit | process的exit事件回调函数，不设置则采用默认处理。 | 函数 |
| errorHandle | 统一的错误处理函数，接收参数第一个是error，第二个是错误描述的辅助标记名称。 | 函数，示例：(err, errname) => {} |
| eventDir | fs.watch事件目录，默认/tmp/cdpc_watch | 对重新加载配置、重启、暂停、恢复应用等操作的文件事件目录。 |
| notExit | 不退出应用，默认为false，设置为true则会监听信号不退出。 | 若自定义signalHandle，则需要自行处理。 |
| notExitButSpread | 不退出应用，但是收到信号会扩散到子进程，默认为false。 | 若自定义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设置的事件目录不再起作用。 |
| childDetached | 子进程是否分离，默认为false。 | true或false。 |
| beforeStartCallback | 运行子进程之前的回调函数，接收参数为一个配置对象，是格式化后的配置对象。 | 函数。 |

## 以配置文件的方式加载。

```javascript

const CDPC = require('cdpc')

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

cm.loadConfig()

```

## 开启IPC

若要使用IPC通信，需要使用options.stdio选项：

```javascript

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文件的代码是：

```javascript

'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)

```

## 启用监控

负载监控的定时器采用了时间片和步进式的策略，并且支持波动策略。

```javascript

let cm = new CDPC()

//...
/*
  定时器每10毫秒执行一次，每执行一次，步进计数器加1。
  步进从50到105开始波动，采取的方案是：计数到50开始读取监控信息，然后是计数到101开始读取监控信息，
  直到步进计数达到105，然后开始计数到104进行负载信息的获取，直到步进达到50的状态。
  如此反复。
  dynamicStep = 5 设定动态步进为5，默认是1。这表示每次增长是5，就是50、55、60...
*/
cm.dynamicStep = 5
cm.setStepSlice(10)
cm.setMaxStep(50, 105)

cm.monitorStart()

```

**cdpc.prototype.setStepSlice**

设定定时器时间片

**cdpc.prototype.setMaxStep**

设定步进最大计数

步进数和定时器时间片相乘就是获取负载信息的时间间隔。

## Net部分

若要获取进程的网络数据统计，请传递选项：monitorNetData，设置为true。

**重置网络数据统计**

CDPC.prototype.resetNetData(name)

**注意：** 使用resetNetData重置网络统计数据，会把统计项的几个属性设置为0，但是loadinfo.net.devData直接一个新的空对象替换。
