前言
本文主要内容
- 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
启动后,控制台打印
查看进程详情
monit监控
(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
时,可以在不修改代码的情况下实现负载均衡集群。
从上图可以了解到,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-rpc 和 axon 两个库,基本原理是提供服务的server绑定到一个域名和端口下,调用服务的client连接端口实现rpc连接。 后续新版本采用了pm2-axon-rpc 和 pm2-axon两个库,绑定的方法也由端口变成.sock文件,因为采用port可能会和现有进程的端口产生冲突。
以上是 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配置
- 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
- 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"
}
- 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做容器跟原来的不一样导致的。有需要的小伙伴可以关注下。
- 以不同的配置启动服务,服务中可以取到环境变量
process.env.NODE_ENV
四、知识拓展
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模块
,而不是直接使用系统原生的方法?主要的原因是以下两点:
- fork的进程监听同一端口会导致端口占用错误
- fork的进程之间没有负载均衡,容易导致惊群现象
在 cluster模块
中,针对第一个问题,通过判断当前进程是否为 master进程
,若是,则监听端口,若不是则表示为 fork 的 worker进程
,不监听端口。
针对第二个问题,cluster模块
内置了负载均衡功能,
master进程
负责监听端口接收请求,然后通过调度算法(默认为 Round-Robin,可以通过环境变量 NODE_CLUSTER_SCHED_POLICY
修改调度算法)分配给对应的 worker进程
。