写了十来年前端,近几年后端服务全部跑在 bun 上,部署一直用 pm2。
pm2 没什么大毛病,但有几件事一直硌手。
去年接触了 MoonBit,能编译出原生二进制,就想明白了一件事:进程管理器这件事,本质上就是几组系统调用——fork、wait、信号、读 /proc,为什么需要一个运行时来干?
于是有了 bm2。
这篇文章讲清楚它是什么、和 pm2 的区别在哪、什么场景该用哪个。
所有数据来自我在 WSL2(Debian 13,20 核,16GB)里的真实压测,不是跑分软件的数字。
先看结论
| 指标 | bm2 0.5.0 | pm2 7.0.4 |
|---|---|---|
| 安装体积 | 5.5 MB(2 个二进制) | 23 MB + 3036 个文件 |
| 运行时依赖 | 无 | Node.js ≥ 16 |
| 空闲守护进程内存 | ~2.6 MB | ~50 MB |
| 命令响应延迟 | ~1 ms | ~200-400 ms |
| 吞吐(4 实例,20k 请求) | 110,551 QPS | 110,116 QPS(fork)/ 不可用(cluster) |
| bun + ESM 顶层 await | 原生支持 | fork 需 CJS wrapper,cluster 不可用 |
| daemon 崩溃后 | 原地收养存活实例,应用无感 | 按 dump 重启应用(会有中断) |
| 内置通知(10 事件 × 7 平台) | ✅ | ❌ 需第三方模块(已停止维护) |
吞吐三者持平(进程管理器不在数据面上),这符合架构预期。
真正的差异在别处。
bm2 是什么
一个用 MoonBit 写的 Linux 进程管理器,编译后就是两个原生二进制:bm2(CLI)和 bm2d(守护进程),加起来 5.5 MB,没有任何运行时依赖。
它管理 bun 和 Node.js 应用:
- fork 模式:每个实例独立进程、独立端口(适合 Nginx upstream)
- cluster 模式:所有实例 SO_REUSEPORT 绑同一端口,内核分发(不需要 Nginx 也能负载均衡)
- 崩溃自动重启(带预算,防止无限重启)
- 内存超限自动重启
- 掉电安全的状态持久化(原子写 + fsync)
- daemon 崩溃后原地收养存活实例(应用零感知)
- 内置通知系统:10 类事件 × 7 平台(飞书/钉钉/企业微信/Slack/Discord/webhook/邮件),即时 + cron 周期两种上报
配置是一个 TOML 文件,强校验,不支持执行任何代码:
name = "api"
cwd = "/wwwroot/api"
script = "index.js"
runtime = "bun" # 或 "node"
instances = 4 # cluster 模式,SO_REUSEPORT 单端口
port = 3020
max_memory_mb = 512 # 内存超限自动重启
max_restarts = 5 # 连崩 5 次停止自愈,标记 errored
为什么不用 pm2
1. 管理器不应该是一个运行时
pm2 是 Node.js 写的,这意味着:管理 Node 应用的前提是先装好并维护一个 Node 运行时。
守护进程 God 本身占 50MB 内存,每次命令交互是一次 RPC 往返(200-400ms)。
你的服务器上可能跑着十几个服务,为了管它们,得先养一个 50MB 的 Node 进程,还要定期 npm update 防漏洞。
bm2 编译后是两个静态二进制,守护进程 2.6MB,命令响应 1ms。
升级是一条命令(bm2 upgrade),自动换入新守护进程,应用不停。
2. cluster 分发层:pm2 是单点,bm2 是内核
pm2 的 cluster 模式:master 进程 accept 连接再分发给 worker。
master 挂了,所有 worker 丢失服务 handle,整组中断。
bm2 的 cluster 模式用 Linux SO_REUSEPORT:所有实例独立 bind 同一端口,内核按连接四元组哈希分发。
没有中间层,任何实例(包括守护进程)崩溃都不影响其余实例接收连接。
守护进程崩溃后新实例通过 pidfd(内核进程句柄)验证归属,原地收养存活进程——应用零感知。
pm2 还有一个 bm2 没有(也不需要)的坑:pm2 对 bun cluster 模式的支持靠一个 500ms 定时器猜测应用是否 online(源码注释原话:“temporary workaround”)——因为 bun 不触发 Node cluster 的 online 事件。
3. bun + ESM:pm2 需要 wrapper,bm2 原生
befly(我的后端框架)入口是 ESM + 顶层 await:
const app = await createBefly(appConfig, menuConfig);
const server = await app.start();
pm2 fork 容器用 require() 加载入口,直接报错:require() async module is unsupported。
必须额外写一个 CJS wrapper 做动态 import(),还得手工注入端口、实例号、NODE_ENV 这些变量——漏一个就启动失败。
bm2 直接 bun index.js,ESM 顶层 await、JSON import、Bun.env 全部原生工作。
4. daemon 崩溃:pm2 重建,bm2 收养
pm2 daemon 崩溃后,重启按 dump 文件重新 spawn 所有应用(先杀掉残留再起新的)——应用会经历一次完整重启。
bm2 daemon 崩溃后,新守护进程逐实例验证 pidfd 归属:活着的进程原地收养(不中断),死去的按持久化状态标记。
状态文件每次变更原子写入(fsync + rename),掉电最坏情况是回退到上一次变更前。
这个能力是我在真实部署里反复验证过的:kill -9 daemon,4 个实例的 uptime 连续不断。
bm2 还做了什么 pm2 没做的
reusePort 探测
cluster 模式要求应用监听时开启 SO_REUSEPORT。
忘开了会怎样。
第一个实例正常 bind,后续实例静默失败,实际上只有一个实例在服务——流量容量直接打了 1/N,没有任何报错。
bm2 在启动后主动探测:向端口发连接,检查是否以 REUSEPORT 方式绑定。
未开启则整组停止,标记 reuseport_missing 错误,通知你修复。
防患于未然。
内存超限重启
max_memory_mb 配置后,bm2 每 5 秒采样 RSS,超限按异常重启处理(计入重启预算,防止内存泄漏应用无限循环重启)。
pm2 的 max_memory_restart 也可以,但轮询粒度和重启策略的可控性不如 bm2。
内置通知
bm2 内置了通知系统,不需要装任何第三方模块:
- 10 类事件:启动/停止/崩溃/预算耗尽/内存超限/OOM 判定/端口探测/磁盘压力/启动超时/阈值告警
- 7 个平台:飞书/钉钉/企业微信/Slack/Discord/webhook/邮件
- 两种周期:cron 健康巡检(全量体检)+ 每日定点统计(窗口峰值/重启次数)
- 双语:配置
lang = "zh",聊天群里直接看中文
比如让 bm2 每 5 分钟给飞书机器人发一条健康巡检,每天 8 点发一份昨日统计:
[[notify.report]]
name = "健康巡检"
cron = "*/5 * * * *"
template = "health"
[[notify.report]]
name = "每日统计"
cron = "0 8 * * *"
template = "stats"
事件日志
所有管理事件(启动/退出/重启/通知失败)写 JSONL 结构化日志,与应用 stdout/stderr 严格分离,方便 jq 查询和日志采集器采集。
诚实说:pm2 也有优势
- 生态和社区:pm2 十年沉淀,文档、教程、Stack Overflow 答案一搜一大把。bm2 是新项目,遇到问题只能看源码或找我。
- 跨平台:pm2 能跑 macOS 和 Windows(开发环境友好)。bm2 只支持 Linux(内核 ≥ 5.3),这是设计取舍——pidfd 和 SO_REUSEPORT 都是 Linux 内核特性。
- pm2.io 监控面板:Web 界面看指标,功能丰富。bm2 只有 CLI 输出和 JSON(适合接自己的监控系统)。
- 模块系统:pm2-logrotate 等模块生态。bm2 功能内置,不可扩展。
什么场景用 bm2
- Linux 服务器上部署 bun 或 Node.js 后端服务
- 想要轻量、快速、可靠的进程管理,不需要 Web 面板
- 应用使用 ESM + 顶层 await(bun 现代写法)
- 需要 daemon 崩溃后应用不中断
- 需要内置通知(飞书/钉钉/邮件),不想装第三方
- 愿意用一条命令升级,接受 TOML 配置
什么场景继续用 pm2
- 需要 Windows/macOS 支持(开发环境)
- 依赖 pm2 生态模块(pm2-logrotate、pm2-io)
- 团队已有 pm2 运维经验和脚本
- 需要 Web 监控面板(pm2 plus)
安装
# 安装 MoonBit 工具链(一次性)
curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash
# 安装 bm2(装到 ~/.moon/bin,无需配 PATH)
moon install chensuiyi/bm2/...
# 验证
bm2 version
在你的项目目录创建 bm2.toml,然后:
bm2 start # 注册并启动
bm2 list # 查看状态
bm2 kill api # 停止并注销
项目地址:github.com/chenbimo/moonbit-x(bm2 是 moonbit-x 仓库中的 bm2 成员)。
有问题欢迎提 issue,我会认真看每一条。
