A lightweight Linux process manager for Bun and Node.js applications, written in MoonBit
Dependencies
| 对比项 | bm2 | pm2 |
|---|---|---|
| 形态 | 原生静态二进制 | Node.js 应用 |
| 额外依赖 | 无 | Node.js 运行时 |
| 体积 | 约 5.5 MB(两个二进制) | 约 23 MB |
| 文件数量 | 2 个 | 3036 个 |
| 空闲守护进程内存 | 约 2.6 MB | 约 50 MB |
| 命令响应 | 约 1 ms | 约 200~400 ms |
| 日志轮转 | 内置,10 MB × 10 代 | 需额外安装 pm2-logrotate 模块 |
| 多实例模式 | fork 连续端口 + cluster 单端口(内核分发) | fork + cluster(master 分发) |
| cluster 分发层 | 内核 SO_REUSEPORT,无中间层、无单点 | cluster master 进程,宕机即整应用中断 |
| 守护进程崩溃 | 应用无感继续运行,新守护进程原地收养 | cluster 应用随之中断,恢复需重建进程树 |
| 配置方式 | 静态 TOML,字段强校验,零代码执行 | ecosystem.config.js,可执行任意代码 |
| 环境变量 | 最小白名单 + 保留变量显式注入 | 继承调用 shell 的完整环境 |
| 崩溃恢复 | pidfd 钉住原进程收养,环境校验防误管 | 守护进程崩溃后按 dump 重建,不收养 |
| 自升级 | bm2 upgrade:比对版本、安装、自动换入新守护进程 | npm 手动升级后 pm2 update 重载 |
curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bashmoon install chensuiyi/bm2/...bm2 upgrade # 比对版本、执行 moon install,并自动换入新守护进程# 项目名:字母开头,后接字母、数字和下划线。
# 同时也是应用名,在所有已注册项目中必须唯一。
name = "api"
# 应用工作目录(绝对路径),默认为 bm2.toml 所在目录。
cwd = "/srv/api"
# 相对 cwd 的脚本路径,禁止 .. 段。
script = "src/index.ts"
# 运行时:bun 或 node。
runtime = "bun"
# 执行模式:cluster(默认,所有实例共享同一个 port,由内核分发
# 连接,应用监听需开启 reusePort)或 fork(端口从 port 起连续分配)。
exec_mode = "cluster"
# 实例数量(1..1024)。
instances = 2
port = 3000
# 单实例内存上限(MiB),超过按异常重启处理,至少为 1。
max_memory_mb = 512
# 连续异常重启预算,0 表示首次异常即 errored。
max_restarts = 10
# 自动重启前的延迟(毫秒),崩溃后固定,spawn 失败后按次数递增。
restart_delay_ms = 1000
# 干净退出早于此时长(毫秒)会计入重启预算。
min_uptime_ms = 10000
# SIGTERM 到 SIGKILL 的宽限期(毫秒),最大 60000。
stop_timeout_ms = 10000[env]
MYSQL_HOST = "127.0.0.1"
MYSQL_PORT = "3306"const PORT = Number(process.env.BM2_APP_PORT ?? 3000);
const isPrimary = process.env.BM2_APP_INSTANCE === "0";
Bun.serve({ port: PORT, fetch: () => new Response("ok") });
if (isPrimary) {
// 只在主实例执行:数据库迁移、定时任务等
}bm2 start # 注册/更新当前目录中的项目并启动它
bm2 kill <name> # 停止一个项目并注销它;bm2d 继续运行
bm2 kill -y # 停止所有项目、注销它们并退出 bm2d(裸 `kill` 会拒绝执行)
bm2 list [name] # 显示所有已注册项目的状态;--json 输出机器可读 JSON
bm2 reload # 换入新的 bm2d;被管理的应用继续运行
bm2 upgrade # 把 bm2 升级到 mooncakes 上的最新版本
bm2 version # 显示 bm2 版本# exec_mode 可省略,默认即 cluster
instances = 4
port = 3000const PORT = Number(process.env.BM2_APP_PORT);
// Bun
Bun.serve({ port: PORT, reusePort: true, fetch: () => new Response("ok") });
// Node.js
const http = require("node:http");
http.createServer((req, res) => res.end("ok")).listen({ port: PORT, reusePort: true });upstream bm2_app {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://bm2_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}example.com {
reverse_proxy 127.0.0.1:3000 127.0.0.1:3001
}daemon.port # 控制通道端口(本机 TCP)
daemon.token # 控制通道共享令牌,0700 目录内
bm2d.pid # 守护进程 PID
bm2.events.jsonl # CLI 连接与重试事件
bm2d.log # 守护进程 stderr / 运行时诊断
bm2d.events.jsonl # 守护进程与监督事件
<name>/project.json # 每个项目的注册信息(配置路径)
<name>/<name>-<id>.json # 持久化的实例状态
<name>/logs/<name>-<id>.out.log # 应用 stdout
<name>/logs/<name>-<id>.error.log # 应用 stderr
<name>/logs/<name>-<id>.crash.log # 异常退出诊断tail -f ~/.bm2/bm2d.events.jsonl
jq -c . ~/.bm2/bm2d.events.jsonlbm2 notify # 在项目目录执行,给该项目所有目标发一条测试消息| 字段 | 含义 | 默认 |
|---|---|---|
| enabled | 总开关 | false |
| lang | 通知文案语言:en / zh(仅聊天消息;CLI/日志/JSON 恒为英文) | en |
| timeout_ms | 单条投递硬超时(100~60000),也是监督循环被占用的上限 | 5000 |
| dedupe_window_s | 事件防重窗口(秒,0~604800):同签名事件 N 秒内只发一条;0 关闭。仅作用于即时上报 | 3600 |
| max_per_minute | 即时上报每分钟上限(0~6000);超限丢弃并计数;0 不限 | 30 |
| queue_size | 即时上报队列上限(1~4096);满时丢最新保最旧 | 128 |
| events | 事件总闸(10 选 N,未知值启动报错),见下方事件表 | [] |
| threshold_rss_percent | RSS 达内存上限百分比 → 阈值告警(0~100) | 0 |
| threshold_offline_min | 实例非 online 持续分钟数 → 阈值告警(0~1440) | 0 |
| threshold_restarts | 连续异常重启次数 → 阈值告警(0~1000) | 0 |
| start_timeout_ms | spawn 后端口无监听毫秒数 → 启动超时告警(0~600000) | 0 |
| threshold_disk_mb | 状态盘剩余 MB 下限 → 磁盘压力告警(0~1048576) | 0 |
| threshold_storm | 10 分钟窗口重启次数 → 重启风暴告警(0~1000) | 0 |
| mail_url | 邮件通道 SMTP 地址(smtp:// / smtps://);email 目标必须先配它 | 无 |
| mail_from | 邮件发件人 | 无 |
| mail_username / mail_password | SMTP 认证(中继免认证可省) | 无 |
| template | 内容 |
|---|---|
| health | 全量体检:逐实例行(状态/端口/内存/峰值/CPU/运行/重启)+ 项目汇总(在线/异常/模式/端口/磁盘/负载) |
| stats | 窗口统计:同体检布局,另附自上一份统计以来的内存峰值(peak_mb)与自动重启次数(restarts) |
[[notify.report]]
name = "健康巡检"
cron = "*/5 * * * *" # 每 5 分钟
template = "health"
[[notify.report]]
name = "统计"
cron = "0 8 * * *" # 每天 8:00
template = "stats"| platform | 必填 | 可选 |
|---|---|---|
| feishu | url | secret(加签)、format |
| dingtalk | url | secret(加签)、format |
| wecom | url | format |
| slack | url | format |
| discord | url | format |
| webhook | url | secret(Bearer)、format |
| to(收件人数组) | format(url/发件人/认证来自 mail_* 通道) |
| 事件 | 级别 | 含义 |
|---|---|---|
| crash | warn | 异常退出或启动失败,未超预算,退避重试中 |
| errored | error | 崩溃预算耗尽,需人工 bm2 start |
| memory_limit | warn | RSS 超上限,按异常重启处理 |
| oom_kill | warn | SIGKILL 且内存逼近上限,判定内核 OOM killer |
| reuseport_missing | error | cluster 未开 reusePort,项目被停 |
| lifecycle | info | 项目启动/停止/注销,干净退出自动重启 |
| daemon | info | 守护进程启动/reload 后本项目被接管 |
| threshold | warn | 内存逼近/离线过久/重启过频/重启风暴 |
| disk_pressure | warn | 状态盘剩余空间不足 |
| start_timeout | warn | spawn 后端口始终未监听 |
| report | — | 已移出事件:定时内容改由 cron + template 表达 |
[notify]
enabled = true
lang = "zh"
dedupe_window_s = 60
max_per_minute = 60
events = ["lifecycle", "daemon", "crash", "errored", "memory_limit",
"reuseport_missing", "threshold", "oom_kill", "disk_pressure",
"start_timeout"]
threshold_rss_percent = 80
start_timeout_ms = 30000
threshold_disk_mb = 1024
mail_url = "smtps://smtp.example.com:465"
mail_from = "bm2@example.com"
mail_username = "bm2@example.com"
mail_password = "${BM2_MAIL_PASSWORD}"
# 即时上报:故障 → 主群 + 值班邮件
[[notify.report]]
name = "故障告警"
events = ["crash", "errored", "memory_limit", "oom_kill", "disk_pressure", "reuseport_missing"]
min_level = "warn"
[[notify.report.target]]
platform = "feishu"
url = "${BM2_MAIN_WEBHOOK_URL}"
[[notify.report.target]]
platform = "email"
to = ["oncall@example.com"]
min_level = "error"
# 周期上报:健康巡检每 5 分钟 → 监控群
[[notify.report]]
name = "健康巡检"
cron = "*/5 * * * *"
template = "health"
[[notify.report.target]]
platform = "feishu"
url = "${BM2_REPORT_WEBHOOK_URL}"
# 周期上报:每日 8 点统计 → 报表群
[[notify.report]]
name = "每日统计"
cron = "0 8 * * *"
template = "stats"
[[notify.report.target]]
platform = "feishu"
url = "${BM2_STATS_WEBHOOK_URL}"Install
Download zipA lightweight Linux process manager for Bun and Node.js applications, written in MoonBit
Dependencies