前面都是 node 直接跑的 Nest 应用,但生产环境不会直接跑 node,而会用 pm2 来跑。为什么要用 pm2 呢?它解决了啥问题?
想一下:
线上的 node 应用不只是跑起来就行了,还要做自动重启、日志、多进程、监控这些事情。而这些事情,都可以用 pm2 来做。
pm2 是 process manager,进程管理。它是第二个大版本,和前一个版本差异很大,所以叫 pm2。pm2 的主要功能就是进程管理、日志管理、负载均衡、性能监控这些。
首先安装 pm2:
npm install -g pm2然后跑一个 node 应用(这里跑一个 Nest 的应用)。直接 node 跑日志打印在控制台,而用 pm2 就可以这样跑:
pm2 start ./dist/main.js它会把这个 node 进程跑起来并管理起来,就有了自动重启、日志管理、性能监控等功能。
执行 pm2 logs,可以看到 pm2 会把所有进程的日志打印出来,通过前面的“进程 id|进程名字”来区分,比如 0|main。而且它会把它写到日志文件里,在 ~/.pm2/logs 下,以“进程名-out.log”和“进程名-error.log”分别保存不同进程的日志。main-out.log 保存 main 进程的正常日志,main-error.log 保存它的报错日志。
再跑一个进程就有两个进程了。也可以这样查看单个进程的日志:
pm2 logs 进程名
pm2 logs 进程id进程管理可以手动启动、重启、停止某个进程,而且崩溃了会自动重启,也可以定时自动重启。只需要 pm2 start 的时候带上几个选项:
pm2 start xxx --max-memory-restart 200M # 超过 200M 内存自动重启
pm2 start xxx --cron-restart "2/3 * * * * *" # 从 2s 开始每 3s 重启一次
pm2 start xxx --watch # 当文件内容改变自动重启
pm2 start xxx --no-autorestart # 不自动重启清理日志用 pm2 flush 或 pm2 flush 进程名|id。查看指定进程的日志用 pm2 logs main --lines 100。前面用到的 pm2 start、pm2 stop、pm2 restart、pm2 delete 等就是进程管理的功能。
node 应用是单进程的,为了充分利用多核 cpu 会使用多进程来提高性能。node 提供的 cluster 模块就是做这个的,pm2 基于这个实现了负载均衡。只要启动进程的时候加上 -i num 就是启动 num 个进程:
pm2 start app.js -i max
pm2 start app.js -i 0这俩是启动 cpu 数量的进程。跑起来之后还可以动态调整进程数,通过 pm2 scale:
pm2 scale main 3 # 调整为 3 个进程
pm2 scale main +3 # 增加 3 个进程可以动态伸缩进程的数量,pm2 会把请求分配到不同进程上去。这就是负载均衡功能。
执行 pm2 monit,可以看到不同进程的 cpu 和内存占用情况。
当进程多了之后,难道都要手动通过命令行来启动么?肯定不会每次都敲一遍。pm2 支持配置文件的方式启动多个应用。执行 pm2 ecosystem 会创建一个配置文件 ecosystem.config.js:apps 部分就是配置应用的,scripts 就是应用的启动路径。它可以指定的配置非常多,基本就是命令行有啥选项,这里就有啥属性。然后 pm2 start ecosystem.config.js 就可以批量跑一批应用,就相当于 pm2 根据配置文件自动执行这些命令,不用手动敲了。
另外还有个 pm2 plus,这个是收费功能,可以在线监控你的应用,一般也不需要用,用免费的本地功能就好了。
docker 容器内的进程同样有日志管理、进程管理和监控的需求。万一 docker 容器内 node 服务崩溃了,是不是需要重启?一般都是在 docker 镜像内安装 pm2 来跑 node。
之前写的 Nest 的 dockerfile 是这样的:
# build stage
FROM node:22 as build-stage
WORKDIR /app
COPY package.json .
RUN npm config set registry https://registry.npmmirror.com/
RUN npm install
COPY . .
RUN npm run build
# production stage
FROM node:22 as production-stage
COPY --from=build-stage /app/dist /app
COPY --from=build-stage /app/package.json /app/package.json
WORKDIR /app
RUN npm install --omit=dev
EXPOSE 3000
CMD ["node", "/app/main.js"]现在要改成多装一个 pm2,然后用 pm2 代替 node 来跑:
# build stage
FROM node:22 as build-stage
WORKDIR /app
COPY package.json .
RUN npm config set registry https://registry.npmmirror.com/
RUN npm install
COPY . .
RUN npm run build
# production stage
FROM node:22 as production-stage
COPY --from=build-stage /app/dist /app
COPY --from=build-stage /app/package.json /app/package.json
WORKDIR /app
RUN npm install --omit=dev
RUN npm install -g pm2
EXPOSE 3000
CMD ["pm2-runtime", "/app/main.js"]就是多装一个 pm2,然后用 pm2-runtime 代替 node 来跑,这样容器内的 node 进程在崩溃时会自动重启。
前面使用 pm2 主要是用它在进程崩溃的时候重启进程的功能,而这个功能 Docker 也是有的。
试一下:写一个 1s 以后抛错误的脚本,把它放到 Docker 容器里跑:
FROM node:22-alpine3.20
WORKDIR /app
COPY ./index.js .
CMD ["node", "/app/index.js"]构建成镜像跑起来,可以看到容器 1s 后就停掉了:当进程退出的时候,容器也会停止。docker run 的时候也可以指定重启策略:
docker run -d --restart=always --name=restart-test-container2 restart-test:first--restart 指定总是重启,在 docker desktop 里可以看到它一直在 restart,打印了很多次错误日志。点击停止就不会再重启了。这就是 docker 的自动重启功能。
Docker 的自动重启功能和 PM2 的自动重启功能是重合的,那还有必要用 PM2 么?
其实 PM2 诞生的时候是没有 Docker 这种容器技术的,那时候都是直接部署在机器上,这时候自然需要一个进程管理工具来做进程的重启、负载均衡等功能,这是 PM2 当年很流行的原因。但后来有了 Docker,里面跑的进程崩溃之后,Docker 容器支持自动重启。所以大多数情况下没必要再用 PM2 了。而且如果你用了 K8S 这种容器编排工具,那更没必要用 PM2 了,直接让 K8S 去调度、重启容器就可以。
但也有另一种说法,Docker 重新跑容器的成本比容器内 PM2 重新跑进程的成本高,所以用 PM2 来管理进程更好一点。综上,有了 Docker 基本没大有必要用 PM2 了。但如果单独跑 Docker 容器,还是可以结合 pm2-runtime 来提高重启速度的。
Docker 重启策略默认是 no,也就是不自动重启。除了 always,还有 on-failure 和 unless-stopped 两种:
on-failure 只有非正常退出才重启,还可以指定最多重启几次,比如 on-failure:2 是容器重启 2 次(一共打印 3 次错误)就不再重启了。unless-stopped 是除非手动停止,否则总是会重启。
always 和 unless-stopped 的区别在于 Docker Daemon 重启的时候:重启策略为 always 的容器在 Docker Daemon 重启的时候容器也会重启,而 unless-stopped 的容器不会重启。
Docker Compose 是用于同时跑多个 Docker 容器的,它自然也支持 restart 的配置。
服务器上的 node 应用需要用 pm2 的日志管理、进程管理、负载均衡、性能监控等功能,分别对应 pm2 logs、pm2 start/restart/stop/delete、pm2 start -i、pm2 monit 等命令。多个应用或者想把启动选项保存下来的时候,可以通过 ecosystem 配置文件批量启动一系列应用。
Docker 和 pm2 可以结合:写 dockerfile 的时候多安装一个 pm2 的依赖,然后把 node 换成 pm2-runtime,在进程崩溃的时候让 pm2 自动重启。Docker 本身也支持自动重启,有 no、always、on-failure、unless-stopped 4 种重启策略。有了 Docker 基本没大有必要用 PM2 了,但进程重启的速度肯定比容器重启的速度快一些,如果只是 Docker 部署,可以结合 pm2-runtime 来做进程的重启。绝大多数情况下,直接用 Docker 跑 node 脚本就行,不需要 PM2。