前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

pm2 ecosystem部署Node应用与pm2-logrotate日志切割实战

首页2025-11-12 14:40:12Front-End
DockerPM2Node.js部署

服务器磁盘告警,登上去 df -h 一看根分区 100%,服务已经因为写不进文件挂了。一层层 du -sh 找过去,最后定位到应用目录下的 app-error.log,单个文件几个 G。这个文件从部署那天起就没被清理过,一直在往后追加。

这篇把 PM2 的两件事写清楚。一件是用 ecosystem.config.js 管理进程配置,替代那种在命令行敲一长串 pm2 start 参数的做法;另一件是用 pm2-logrotate 做日志切割,让日志按大小和时间自动滚动、自动淘汰旧文件。最后把这一整套塞进 Dockerfile,Next.js standalone 产物加 PM2 cluster 模式,容器起来就是多实例带日志管理的状态。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 为什么要用 ecosystem 配置文件,而不是在命令行传参数
  • ecosystem.config.js 每个字段的作用与取值建议
  • cluster 模式下实例数怎么定,日志文件为什么会带编号
  • 日志撑爆磁盘的真正原因,以及 pm2 flush 为什么只能救急
  • pm2-logrotate 的安装、参数含义与推荐配置
  • 把 PM2 塞进 Docker 镜像的完整 Dockerfile 与注意事项
  • pm2-runtime 和 pm2 start 的区别,为什么容器里必须用前者
  • 部署后怎么验证日志确实在按规则切割

# 一、先说为什么要用 ecosystem 文件

很多人第一次用 PM2 都是这么启动的。

pm2 start server.js --name my-app -i 2 --output ./logs/out.log --error ./logs/err.log
@前端进阶之旅: 代码已经复制到剪贴板

能跑,但有几个问题。参数写在命令行里,没进版本控制,换台机器部署得靠翻历史命令;实例数、日志路径这些东西改一次就要把整条命令重敲一遍;团队里两个人敲的参数不一样,线上跑的配置和你以为的不是一回事。

ecosystem 文件解决的就是这个。它是一份普通的 JS 模块,跟着代码一起进仓库,谁部署都是同一份配置。

// 在nodejs/nextjs项目根目录创建 ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'my-app-name',
      script: './server.js', // 服务启动脚本
      watch: false, // 禁用文件监视,提高性能
      instances: 2, // 指定要启动的实例数量 // 0 和 max 同义
      exec_mode: 'cluster', // 启用集群模式,指定要启动的实例数量
      // 启用文件日志记录
      output: './logs/app-out.log', // 标准输出日志文件
      error: './logs/app-error.log', // 错误日志文件
      // log: './logs/combined.log',        // 合并日志文件(可选)
      // 日志配置
      log_date_format: 'YYYY-MM-DD HH:mm Z',
      merge_logs: false, // 为每个实例单独日志
      time: true, // 在日志中添加时间戳
      env: {}
    }
  ]
}
@前端进阶之旅: 代码已经复制到剪贴板

启动就是一行。

pm2-runtime ecosystem.config.js
@前端进阶之旅: 代码已经复制到剪贴板

apps 是数组,一个文件里可以描述多个应用。做微服务或者一台机器上跑好几个 Node 服务的时候,一份 ecosystem 就能把它们全管起来,pm2 start ecosystem.config.js --only my-app-name 还能单独启其中一个。

# 二、ecosystem 每个字段在做什么

上面那份配置看着简单,几个字段背后的行为差别不小,逐个说一下。

script 指向启动入口。Next.js 用 standalone 输出模式的时候,构建产物里会生成一个 server.js,路径就是它。这一点后面 Dockerfile 那节还会再提,因为 COPY --from=builder /app/.next/standalone ./ 之后 server.js 正好落在工作目录根下,和这里的 ./server.js 对得上。

watch: false 是生产环境必须的。开着 watch,PM2 会监听文件变化自动重启,本地开发方便,线上就是灾难,一次误操作 touch 了某个文件,整个服务重启。而且监听本身在文件多的项目里会持续占 CPU。

instances 和 exec_mode 是一对。exec_mode: 'cluster' 让 PM2 用 Node 的 cluster 模块起多个进程,共享同一个端口,请求由主进程分发。instances: 2 就是起两个。写 0 或者 'max' 表示按 CPU 核心数来,写 -1 表示核心数减一,留一个核给系统。

那实例数到底该写几。我的经验是先看容器分到多少 CPU。如果是 Docker 里跑并且限了 --cpus=1,起再多实例也没用,反而每个进程都要单独占一份内存,Next.js 一个实例常驻内存两三百兆,起四个就快 1G 了。写死 2 而不是 max,很大程度上就是为了防止容器里 os.cpus() 读到的是宿主机核心数,一下起了十几个进程把内存打满。

这个坑我踩过,宿主机 16 核,容器限了 1 核,instances: 'max' 直接起了 16 个进程,OOM 被 kill 掉,PM2 又自动拉起来,反复循环。

output 和 error 分别是标准输出和错误输出的落盘路径。PM2 的配置 schema 里这两个是 out_file 和 error_file 的别名,两种写法都认,看老项目配置的时候别被这两组名字搞混。注释掉的 log 字段是合并日志,开了它 out 和 error 会写到同一个文件里,排查时序问题方便,但 error 会被淹没在正常日志里,我一般不开。

merge_logs: false 这条决定了日志文件名长什么样。cluster 模式下每个实例都在写日志,如果合并成一个文件,多进程同时追加会出现内容交错。设成 false 之后 PM2 会给每个实例的日志文件加上实例编号,也就是后面会看到的 app-out-1.log 和 app-out-2.log。排查问题时能直接定位是哪个实例出的错,这个设计是真的舒服。

time: true 和 log_date_format 是一对。前者开启时间戳前缀,后者定义格式。'YYYY-MM-DD HH:mm Z' 里的 Z 是时区偏移,服务器和容器时区不一致的时候,这个字段能救你一命,不然你看到的日志时间和用户反馈的时间对不上,白排查半天。

env: {} 是注入给进程的环境变量。空对象表示不额外注入,走容器或者宿主机的环境。真要用的话可以写成 env: { NODE_ENV: 'production', PORT: 3000 },另外 PM2 还支持 env_production 这种带后缀的写法,配合 --env production 切换。

# 三、日志为什么会把磁盘撑爆

回到开头那个磁盘满的问题。

PM2 默认的日志行为是纯追加,写满了不会停,也不会自己删。只要进程一直在跑,日志文件就一直在长。Next.js 这类应用每个请求可能都会打几行日志,一天几十万请求,几个 G 是很正常的量。

更麻烦的是这个增长完全没有反馈。磁盘从 60% 涨到 90% 的过程中没有任何人会注意到,直到写不进去,服务开始报 ENOSPC,才有人上去看。

有人会说手动清一下不就行了。

$ pm2 flush
[PM2] Flushing /root/.pm2/pm2.log
[PM2] Flushing:
[PM2] /root/.pm2/logs/pm2-logrotate-out.log
[PM2] /root/.pm2/logs/pm2-logrotate-error.log
[PM2] Flushing:
[PM2] /app/logs/app-out-1.log
[PM2] /app/logs/app-error-1.log
[PM2] Flushing:
[PM2] /app/logs/app-out-2.log
[PM2] /app/logs/app-error-2.log
[PM2] Logs flushed
@前端进阶之旅: 代码已经复制到剪贴板

pm2 flush 会把所有日志文件清空。它确实能立刻腾出空间,但这是救急手段,不是方案。一是它清得太干净,出问题时你想回看昨天的日志已经没了;二是它得有人记得去执行,靠人记事情就等于没有保障。

真正要做的是让日志自己滚动。

# 四、用 pm2-logrotate 做自动切割

PM2 有个官方模块叫 pm2-logrotate,专门干这件事。它会起一个常驻的工作进程,定时检查日志文件大小,超过阈值就把当前文件改名归档、重新开一个空文件写,同时按保留数量删掉最老的归档。

安装是一行命令。

pm2 install pm2-logrotate
@前端进阶之旅: 代码已经复制到剪贴板

注意这里是 pm2 install 不是 npm install。PM2 的模块系统和 npm 包不是一回事,装完之后模块本体在 ~/.pm2/module_conf.json 有一份配置记录,pm2 list 里也能看到它作为一个进程在跑。

装完先看看默认配置。

$ pm2 conf pm2-logrotate

Module: pm2-logrotate
$ pm2 set pm2-logrotate:max_size 10M
$ pm2 set pm2-logrotate:retain 50
$ pm2 set pm2-logrotate:compress false
$ pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss
$ pm2 set pm2-logrotate:workerInterval 1
$ pm2 set pm2-logrotate:rotateInterval 0 0 * * *
$ pm2 set pm2-logrotate:rotateModule true
Module: module-db-v2
$ pm2 set module-db-v2:pm2-logrotate [object Object]
@前端进阶之旅: 代码已经复制到剪贴板

pm2 conf 的输出很贴心,它直接把每一项打印成可以复制执行的 pm2 set 命令,想改哪项就复制哪行改个值。

各参数的含义整理一下。

配置项 简介
Compress 是否通过gzip压缩日志
max_size 单个日志文件的大小,比如上图中设置为1K(这个其实太小了,实际文件大小并不会严格分为1K)
retain 保留的日志文件个数,比如设置为10,那么在日志文件达到10个后会将最早的日志文件删除掉
dateFormat 日志文件名中的日期格式,默认是YYYY-MM-DD_HH-mm-ss,注意是设置的日志名+这个格式,如设置的日志名为abc.log,那就会生成abc_YYYY-MM-DD_HH-mm-ss.log名字的日志文件
rotateModule 把pm2本身的日志也进行分割
workerInterval 设置启动几个工作进程监控日志尺寸,最小为1
rotateInterval 设置强制分割,默认值是0 0 * * *,意思是每天晚上0点分割,这个足够了个人觉得

有两个参数容易理解错,单独说一下。

fe
  • 一、先说为什么要用 ecosystem 文件
  • 二、ecosystem 每个字段在做什么
  • 三、日志为什么会把磁盘撑爆
  • 四、用 pm2-logrotate 做自动切割
  • 五、把这一套塞进 Dockerfile
  • 六、pm2-runtime 和 pm2 start 的区别
  • 七、部署后怎么验证
  • 总结
  • 参考

← Next.js 15/16 严重RSC漏洞 CVE-2025-66478 影响范围与升级方案Next.js 16 升级指南 从Turbopack默认启用到异步API全面化 →