pm2 替代品,nodejs&bun 线上部署必备工具

bm2

bm2 是一个用 MoonBit 编写的轻量级 Linux 进程管理器,用于管理 Bun 与 Node.js 应用。

bm2 由命令行工具和后台守护进程组成,命令行通过 Unix socket 下达指令,守护进程持续托管应用进程。

每个用户一个独立守护进程,可同时管理多个项目。

托管能力包括多实例、崩溃自动重启、内存限制、优雅停止与状态持久化。

反向代理与负载均衡交给 Nginx、Caddy 之类的网关负责,bm2 只专注进程托管。

英文文档:README.en.md

bm2与pm2对比

对比项 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 模块

功能特性

  • 一个 bm2.toml 配置一个项目(单个应用,一个或多个独立实例)。
  • 崩溃重启预算、内存限制、优雅停止超时、状态持久化、Unix socket 控制。
  • 命令:startkilllistreloadupgradeversion
  • start 总是对其项目执行一次完整重启。

它不管理 Nginx、域名、证书、热重载、开机自启或远程管理。

环境要求

  • 仅支持 Linux,非 Linux 构建会拒绝运行并给出明确提示。
  • 需要 Linux 内核 >= 5.3(pidfd 进程跟踪)。
  • MoonBit(仅安装/升级 bm2 时需要)
  • Bun
  • Node.js(仅 runtime = "node" 的项目需要)

安装与升级

先安装 MoonBit 工具链:

bash 复制代码
curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash

再安装 bm2:

bash 复制代码
moon install chensuiyi/bm2/...

bm2 默认安装到 moon 工具链所在的 ~/.moon/bin,无需任何 PATH 配置。

bm2bm2d 两个二进制一起安装。

两者都必须留在 PATH 中,因为 bm2 通过名字启动 bm2d

升级到 mooncakes 上的最新版本,装完自动换入新守护进程:

bash 复制代码
bm2 upgrade          # 比对版本、执行 moon install,并自动换入新守护进程

配置参数

在你运行 bm2 的目录创建 bm2.toml

以下为完整模板,包含全部字段与默认值,直接复制修改即可:

toml 复制代码
# 项目名:字母开头,后接字母、数字和下划线。
# 同时也是应用名,在所有已注册项目中必须唯一。
name = "api"

# 应用工作目录(绝对路径),默认为 bm2.toml 所在目录。
cwd = "/srv/api"

# 相对 cwd 的脚本路径,禁止 .. 段。
script = "src/index.ts"

# 运行时:bun 或 node。
runtime = "bun"

# 实例数量(1..1024),端口从 port 起连续分配。
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

所有已注册项目的端口范围不得重叠,冲突时 bm2 拒绝启动。

环境变量

bm2 只把自己的 PATHHOMETMPDIR 传给被管理进程,外加这些保留变量:

  • BM2_APP_NAME(项目名)
  • BM2_INSTANCE_ID(实例编号,第一个实例为 "0"
  • BM2_APP_INSTANCE(同实例编号,命名对齐 PM2 的 NODE_APP_INSTANCE 习惯)
  • BM2_APP_PORT(分配给该实例的端口,等于 bm2.tomlport 加实例编号)
  • NODE_ENV(恒为 "production"

端口与实例编号一一对应:BM2_APP_PORT = port + BM2_APP_INSTANCE,例如 port = 3000instances = 3 时,三个实例分别监听 300030013002

应用内典型用法:

  • BM2_APP_PORT 绑定监听端口,多实例各占一个端口。
  • BM2_APP_INSTANCE === "0" 判断主实例,只在主实例执行数据库迁移、定时任务等一次性逻辑。
  • NODE_ENV === "production" 走生产分支。
js 复制代码
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 不解析 .env,也不参与加载。

bm2 已注入的变量不会被运行时的 .env 加载覆盖。

命令使用

bash 复制代码
bm2 start             # 注册/更新当前目录中的项目并启动它
bm2 kill <name>       # 停止一个项目并注销它;bm2d 继续运行
bm2 kill -y           # 停止所有项目、注销它们并退出 bm2d(裸 `kill` 会拒绝执行)
bm2 list [name]       # 显示所有已注册项目的状态
bm2 reload            # 换入新的 bm2d;被管理的应用继续运行
bm2 upgrade           # 把 bm2 升级到 mooncakes 上的最新版本
bm2 version           # 显示 bm2 版本

startkill <name> 是异步的:守护进程立即应答,CLI 轮询直到操作完成,因此守护进程永远不会因停止超时而阻塞。

bm2 listbm2 killbm2 reloadbm2 version 可以在任意目录执行。

只有 bm2 start 必须在包含 bm2.toml 的目录中运行,因为它要从该配置注册项目。

bm2 kill(不带 -y)时 bm2 拒绝执行并打印提示。

在项目中(或在另一个使用相同 name 的目录中)重新运行 bm2 start 会更新配置并执行完整重启,因此修改任何字段------包括实例数量、端口或脚本------都会在下一次 start 时生效。

被 kill 的项目(bm2 kill <name>)会完全注销:它从 bm2 list 中消失,且不会因守护进程重启而复现。

reload 在不停止被管理应用的情况下换入新的 bm2d:旧守护进程分离,新守护进程以不变的 PID 收养仍在运行的实例。

手动替换二进制后使用,bm2 upgrade 会自动执行这一步。

list 为每个活跃或异常实例打印一行,包括 PID、端口、运行状态、内存、运行时长,以及最后一列 CWD 中的完整项目工作目录。

被有意停止的实例会被省略,restartingerrored 实例保持可见,便于运维诊断。

如果守护进程意外崩溃并留下过期的 Unix socket,下一个 CLI 请求会短暂等待响应、删除过期 socket、启动一个新的守护进程,并重试一次请求。

负载均衡

bm2 只专注进程托管,反向代理与负载均衡交给 Nginx、Caddy 之类的网关。

网关把域名指向实例的连续端口即可,实例增减后同步更新 upstream 列表并 reload 网关。

Nginx 示例(假设 instances = 2port = 3000):

nginx 复制代码
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;
    }
}

Caddy 示例:

caddyfile 复制代码
example.com {
    reverse_proxy 127.0.0.1:3000 127.0.0.1:3001
}

状态与日志

bm2 总是把 socket、PID、状态和管理日志存放在当前 Linux 用户的 ~/.bm2 下。

每个用户一个守护进程,管理所有已注册项目:

text 复制代码
bm2.sock                         # Unix socket,权限 0600
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  # 异常退出诊断

日志按大小轮转:每个文件达到 10 MB 时轮转,保留十代(.1 .. .10,每个文件最多约 100 MB)。

应用日志与 bm2 自身的管理日志严格分离。

两个 *.events.jsonl 文件每行一个 JSON 对象。

它们只记录管理元数据:时间戳、事件名、适用时的应用/实例/PID、运维原因。

它们包含环境变量值、协议载荷或应用输出。

常用命令:

bash 复制代码
tail -f ~/.bm2/bm2d.events.jsonl
jq -c . ~/.bm2/bm2d.events.jsonl
相关推荐
单线程_0144 分钟前
从案例分析 Vue3 Tokenizer 源码一
前端·javascript·vue.js
BigTopOne1 小时前
【WebRtc】-ICE Candidate 与 STUN/TURN 原理详解
前端
l1258651 小时前
# LangGraph Memory机制深度解析:短期记忆与长期记忆的工程实践
前端·人工智能·python·langchain·bootstrap
丘山一郎1 小时前
Spring 中的IOC控制反转 和DI依赖注入
java·后端·spring
码视野2 小时前
基于 Spring Boot + Vue3 的【大学英语四六级 (CET-4/6) 作文智能评分与句式润色系统】设计与实现(含PRD/三端高保真源码/大屏)
java·前端·人工智能·spring boot·后端·vue3
MetaLite2 小时前
Spring-AOP自调用为什么失效-AopContext真能解决吗
java·后端·spring
大模型码小白2 小时前
AI 对话流性能调优:万级消息的虚拟滚动落地
java·大数据·前端·javascript·人工智能·算法·机器学习
MC皮蛋侠客2 小时前
Tauri 2.x 系列(五):调用系统 API——官方插件、Rust crate 与原生能力
开发语言·后端·rust
xm_xm_xm_12 小时前
react17版本以前类组件常用操作
前端·javascript·react.js