【NJ07】服务托管nodemon&pm2

1,114 阅读6分钟

前言

本文主要内容

  • nodemon
  • pm2
    • pm2常用指令
    • pm2目录结构
    • pm2基本工作原理
  • 生产实践pm2配置
  • 知识拓展(Node Cluster)

一、nodemon [dev]

nodemon是一个用来监视node.js应用程序中的任何更改并自动重启服务,持实时热更新后端服务代码,修改代码后不用重启后端服务,以此提升开发效率,非常适合用在开发环境中。运行在开发环境,不占用进程,关闭了这个服务也就关闭了。

  npm install -g nodemon
  
  # nodemon开启服务
  $ nodemon app.js

二、pm2 [prod]

pm2是一个进程管理工具,可以用它来管理你的node进程,并查看node进程的状态,当然也支持性能监控, 进程守护,负载均衡等功能。运行在服务端后台开发,不占用终端,但是这个启动服务后这个进程仍然存在。

  npm install -g pm2
  
  # nodemon开启服务
  $ pm2 start server.js

启动后,控制台打印

image.png

查看进程详情

image.png

monit监控

image.png

(1)pm2常用指令

1. 启动
    pm2 start app.js
    pm2 start app.js --name my-api   #my-api为PM2进程名称
    pm2 start app.js -i 0           #根据CPU核数启动进程个数
    pm2 start app.js --watch   #实时监控app.js的方式启动,当app.js文件有变动时,pm2会自动reload

2. 查看进程
    pm2 list
    pm2 show 0 或者 # pm2 info 0  #查看进程详细信息,0为PM2进程id

3. 监控
    pm2 monit

4. 停止
    pm2 stop all  #停止PM2列表中所有的进程
    pm2 stop 0    #停止PM2列表中进程为0的进程

5. 重载
    pm2 reload all    #重载PM2列表中所有的进程
    pm2 reload 0     #重载PM2列表中进程为0的进程

6. 重启
    pm2 restart all     #重启PM2列表中所有的进程
    pm2 restart 0      #重启PM2列表中进程为0的进程

7. 删除PM2进程
    pm2 delete 0     #删除PM2列表中进程为0的进程
    pm2 delete all   #删除PM2列表中所有的进程

8. 日志操作
    pm2 logs [--raw]   #Display all processes logs in streaming
    pm2 flush              #Empty all log file
    pm2 reloadLogs    #Reload all logs

9. 升级PM2
    npm install pm2@lastest -g   #安装最新的PM2版本
    pm2 updatePM2                    #升级pm2

10. 更多命令参数请查看帮助
    pm2 --help

(2)pm2目录结构

默认的目录是:当前用于的家目录下的.pm2目录(此目录可以自定义,请参考:五、自定义启动文件),详细信息如下:

$HOME/.pm2                   #will contain all PM2 related files
$HOME/.pm2/logs              #will contain all applications logs
$HOME/.pm2/pids              #will contain all applications pids
$HOME/.pm2/pm2.log           #PM2 logs
$HOME/.pm2/pm2.pid           #PM2 pid
$HOME/.pm2/rpc.sock          #Socket file for remote commands
$HOME/.pm2/pub.sock          #Socket file for publishable events
$HOME/.pm2/conf.js           #PM2 Configuration

(3)pm2基本工作原理

pm2 基于 cluster模块 进行了封装,它能自动监控进程状态、重启进程、停止不稳定进程、日志存储等。利用 pm2 时,可以在不修改代码的情况下实现负载均衡集群。

image.png

从上图可以了解到,PM2的基本工作原理,主要关注pm2 的 Satan进程God Deamon守护进程 以及 两者之间的 进程间远程调用RPC

源码中,
bin文件夹下的pm2文件,负责处理命令行输入;
lib文件夹下的 Satan.js 和 God.js 存放主要逻辑,前者要调用后者的方法。

Satan.js提供了程序的退出、杀死等方法,因此它是魔鬼;God.js 负责维护进程的正常运行,当有异常退出时能保证重启,所以它是上帝。God进程启动后一直运行,相当于 cluster 中的 Master进程,维持 worker 进程的正常运行。我一直在想为啥pm2没有master进程,后来读了源码才知道它的master进程就是Deamon进程,而且进程名字也做了修改。

RPC(Remote Procedure Call Protocol)是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。同一机器不同进程间的方法调用也属于rpc的作用范畴。 代码中采用了axon-rpcaxon 两个库,基本原理是提供服务的server绑定到一个域名和端口下,调用服务的client连接端口实现rpc连接。 后续新版本采用了pm2-axon-rpcpm2-axon两个库,绑定的方法也由端口变成.sock文件,因为采用port可能会和现有进程的端口产生冲突。

image.png

以上是 pm2 的执行流程图,

  • 每次命令行的输入都会执行一次satan程序。如果God进程不在运行,首先需要启动God进程。然后根据指令,satan通过rpc调用God中对应的方法执行相应的逻辑。
  • 以 pm2 start app.js -i 4为例,God在初次执行时会配置cluster,同时监听cluster中的事件
  • 在God启动后, 会建立Satan和God的rpc链接,然后调用prepare方法。prepare方法会调用cluster.fork,完成集群的启动

(4)其他问题

  • restart 与 reload 的区别
restart 与 reload 字面意思翻译过来为’重启‘ 和 ’重载‘,
区别在于 
restart 为’冷启动‘,需要进程先停止再启动;
reload 为’热启动‘,保持进程在运行状态下完全’刷新‘ 自身数据,reload 的好处是可以减少’重启‘等待时间。
restart会杀死所有进程;
reload实现了0s时间重新加载;通过重新加载,`pm2`一步一步地重新启动所有进程,始终保持至少一个进程在运行

三、实践pm2&nodemon配置

  1. control.sh
#!/bin/bash
MODULE="demo"
workspace="/home/deyi/$MODULE"
LOG_DIR="$workspace/logs"

pre="us01-pre-v"
prod="us01-v"

function start() {
    export NODE_PATH=/home/deyi/node-v8.9.1-linux-x64:/home/deyi/node-v8.9.1-linux-x64/lib/node_modules
    export PATH=/home/deyi/node-v8.9.1-linux-x64/bin:$PATH
    export PM2_HOME=/home/deyi/.pm2

    date +'%Y-%m-%d %H:%M:%S'
    echo -e "user: `whoami` node: `node -v`, npm: `npm -v`"

    [ ! -d $LOG_DIR ] && mkdir -p $LOG_DIR

    cd $workspace

    local clusterfile="$workspace/.deploy/service.cluster.txt"


    if [[ -f "$clusterfile" ]]; then
        local cluster=`cat $clusterfile`
        # SRE加到动态容器中的,文件中就是环境变量
        if [ $cluster == $pre ]; then
            npm run pre
        elif [ $cluster == $prod ]; then
            npm run prod
        else
           npm run prod
        fi
    else
       npm run prod
    fi

    echo "app start"
}


function stop() {
    echo "app stop"
}


action=$1
case $action in
    "start" )
        # 启动服务
        start
        ;;
    "stop" )
        # 停止服务
        stop
        ;;
    * )
        echo "unknown command"
        exit 1
        ;;
esac

  1. package.json
"scripts": {
    "dev": "NODE_ENV=development nodemon --watch 'src/**/*.ts' --exec 'ts-node' ./src/app.ts",
    "prod": "pm2 start ./pm2.json --env production --update-env",
    "pre": "pm2 start ./pm2.json --env pre --update-env"
}
  1. pm2.json
{
  "apps": [{
    "name": "demo",
    "script": "./dist/app.js",
    "args": [],
    "node_args": "--harmony",
    "merge_logs": true,
    "error_file": "/home/deyi/ibddp-node/logs/pm2.error.log",
    "out_file": "/home/deyi/ibddp-node/logs/pm2.out.log",
    "instances": 3,
    "exec_mode": "cluster",
    // "instances": 1,
    // "exec_mode": "fork_mode",
    "cwd": ".",
    "max_memory_restart": "500M",
    "ignore_watch": ["log"],
    "watch_options": {
      "followSymlinks": false
    },
    "env": {
      "NODE_ENV": "production"
    },
    "env_development": {
      "NODE_ENV": "development"
    },
    "env_pre": {
      "NODE_ENV": "pre"
    }
  }]
}

Tips:
instances:2 代表工作线程数量为2。如果给定的数字为0,PM2则会根据你CPU核心的数量来生成对应的工作线程。

exec_mode: "fork_mode"
> fork模式,单实例多进程,常用于多语言混编,比如php、python等,不支持端口复用,需要自己做应用的端口分配和负载均衡的子进程业务代码.缺点就是单服务器实例容易由于异常会导致服务器实例崩溃。
> cluster模式,多实例多进程,但是只支持node,端口可以复用,不需要额外的端口配置,0代码实现负载均衡。优点就是由于多实例机制,可以保证服务器的容错性,就算出现异常也不会使多个服务器实例同时崩溃。

我这边在部署aws亚马逊云的时候,发现cluster模式有问题,可能是SRE做容器跟原来的不一样导致的。有需要的小伙伴可以关注下。

  1. 以不同的配置启动服务,服务中可以取到环境变量
process.env.NODE_ENV

四、知识拓展

image.png

Node Cluster

熟悉 js 的朋友都知道,js 是单线程的,在 Node 中,采用的是 多进程单线程 的模型。由于单线程的限制,在多核服务器上,我们往往需要启动多个进程才能最大化服务器性能。

Node 在 V0.8 版本之后引入了 cluster模块,通过一个主进程 (master) 管理多个子进程 (worker) 的方式实现集群

官网上的一个简单示例

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`Master ${process.pid} is running`);

  // Fork workers.
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`worker ${worker.process.pid} died`);
  });
} else {
  // Workers can share any TCP connection
  // In this case it is an HTTP server
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('hello world\n');
  }).listen(8000);

  console.log(`Worker ${process.pid} started`);
}

进程通信

Node中主进程和子进程之间通过进程间通信 (IPC) 实现进程间的通信,进程间通过 .send()(a.send表示向a发送)方法发送消息,监听 message 事件收取信息,这是 cluster模块 通过集成 EventEmitter 实现的。还是一个简单的官网的进程间通信例子

# cluster.isMaster
# cluster.fork()
# cluster.workers
# cluster.workers[id].on('message', messageHandler);
# cluster.workers[id].send();
# process.on('message', messageHandler); 
# process.send();


const cluster = require('cluster');
const http = require('http');

# 主进程
if (cluster.isMaster) {

  // Keep track of http requests
  let numReqs = 0;
  setInterval(() => {
    console.log(`numReqs = ${numReqs}`);
  }, 1000);
  
  // Count requests
  function messageHandler(msg) {
    if (msg.cmd && msg.cmd === 'notifyRequest') {
      numReqs += 1;
    }
  }

  // Start workers and listen for messages containing notifyRequest
  // ********** 开启多进程(cpu核心数)
  const numCPUs = require('os').cpus().length;
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

// ********** cluster worker 子进程 与主进程通信
  for (const id in cluster.workers) {
    cluster.workers[id].on('message', messageHandler); // ***监听来自子线程的事件
    cluster.workers[id].send({                         // ***向子进程发送
        type: 'masterToWorker',
        from: 'master',
        data: {
            number: Math.floor(Math.random() * 50)
        }
    });
  }

} else {

  # 子进程
  // Worker processes have a http server.
  http.Server((req, res) => {
    res.writeHead(200);
    res.end('hello world\n');

    //****** !!!!Notify master about the request !!!!!!*******

    process.send({ cmd: 'notifyRequest' });   // ***向process发送
    process.on('message', function(message) { // ***监听从process来的
        xxxxxxx
    }
  }).listen(8000);
}

进程负载均衡

了解 cluster 的话会知道,子进程是通过 cluster.fork() 创建的。在 linux 中,系统原生提供了 fork 方法,那么为什么 Node 选择自己实现 cluster模块 ,而不是直接使用系统原生的方法?主要的原因是以下两点:

  1. fork的进程监听同一端口会导致端口占用错误
  2. fork的进程之间没有负载均衡,容易导致惊群现象

在 cluster模块 中,针对第一个问题,通过判断当前进程是否为 master进程,若是,则监听端口,若不是则表示为 fork 的 worker进程,不监听端口。

针对第二个问题,cluster模块 内置了负载均衡功能, master进程 负责监听端口接收请求,然后通过调度算法(默认为 Round-Robin,可以通过环境变量 NODE_CLUSTER_SCHED_POLICY 修改调度算法)分配给对应的 worker进程