# 排障与常见问题

这一页按「你会遇到什么」组织，不按模块组织。

遇到问题先看三处：`bm2 list` 的状态、`~/.bm2/<项目名>/logs/` 里的日志、`~/.bm2/bm2d.events.jsonl` 里的事件。

## 守护进程起不来

```
bm2: bm2d did not start, see /root/.bm2/bm2d.log
```

先看那个日志。常见原因有三个。

`bm2d` 不在 `PATH` 里。它与 `bm2` 必须装在同一处，`moon install` 会一起装，手工拷贝时容易只拷一个。

当前用户对状态目录没有写权限。状态目录是 `~/.bm2`，权限 `0700`，属于启动它的那个用户。

内核低于 `5.3`。pidfd 是硬依赖，低于这个版本会直接拒绝，不会退化运行。

## 状态是 errored

`errored` 的意思不是「崩了」，而是「bm2 停止自愈了」。

连续异常重启次数超过 `max_restarts`，它就不再加码重试。重启预算耗尽的实例会一直躺着，直到你介入。

处理顺序是：看 `logs/<项目名>-<实例号>.crash.log` 找原因，修好，再 `bm2 serve`。

`bm2 serve` 会重置重启预算并把实例重新拉起。

如果是因为内存超限（`max_memory_mb`）触发的，要么调高上限，要么查内存泄漏，别只调上限。

## 集群模式启动即失败

```
reuseport_missing
```

`cluster` 模式下所有实例共用一个端口，靠内核的 `SO_REUSEPORT` 分发，应用必须自己开启这个能力。

Bun 的写法。

```ts
Bun.serve({ port: Number(Bun.env.BM2_APP_PORT), reusePort: true, fetch })
```

Node.js 的写法。

```js
const cluster = require('node:cluster')
```

bm2 在启动后实测，没生效就停下整个项目并报这个原因。

不想改应用也能跑：把 `exec_mode` 改成 `fork`，端口会从 `port` 开始连续分配。

代价是网关那侧要给每个实例写一条 upstream。

## 端口冲突

```
my-api port range 3000~3001 overlaps other-app 3001~3001
```

同一台机器上所有在册项目的端口范围不能重叠。报错会点名占着那段范围的项目。

三种改法：换端口、换实例数、先 `bm2 remove` 那个不用的项目。

`cluster` 模式只占一个端口槽，`fork` 模式占 `instances` 个。这一点在规划端口时容易算错。

## 实例反复重启

看 `crash.log`，里面写着退出码或信号，还有重启次数。

最常见的四个原因：

- 脚本路径写错，`script` 相对的是 `cwd`，不是 `bm2.toml` 的位置。
- 依赖没装，应用目录里缺 `node_modules`。
- 运行时版本低于下限，Bun 要 `1.4.0`，Node 要 `24.0.0`。
- 环境变量缺失，比如 `Bun.env.DB_URL` 是 `undefined` 导致启动即抛错。

依赖 `.env` 的项目注意模式：`bm2 serve` 读 `.env.production`，`bm2 dev` 读 `.env.development`。

## 日志在哪里

```
~/.bm2/<项目名>/logs/
├── <项目名>-0.out.log     标准输出
├── <项目名>-0.error.log   标准错误
└── <项目名>-0.crash.log   崩溃诊断
```

每个实例三个文件，按序号分开。

没有 `crash.log` 是好事，说明没崩过。它只在异常退出或启动失败时写。

`bm2d.log` 是守护进程自己的日志，`bm2d.events.jsonl` 是事件流，两者都在 `~/.bm2/` 下。

日志超过 `[logs] max_size_mb` 会轮转成 `.1`、`.2`，`keep = 0` 表示不设代数上限。

## 通知收不到

按这个顺序排查，前四步能覆盖绝大多数情况。

第一步，`enabled` 是不是 `true`。默认是关闭的。

第二步，事件有没有被放行。`[notify] events` 是总闸，上报条目里的 `events` 是第二次筛选，两级都命中才会发。

第三步，端点有没有写对。先用 `bm2 notify` 发一条测试消息，它绕过事件筛选，直接发给你配的目标。测试能收到，说明端点和凭据没问题，问题在筛选。

第四步，`min_level` 是不是卡住了。邮箱常配成只收 `error` 及以上，`crash` 是 `warn` 级别，会被挡掉。

剩下几种情况属于设计行为，不是故障。

同一条告警在 `dedupe_window_s` 内只发一次，默认一小时。

即时上报受 `max_per_minute` 限制，默认每分钟 30 条。

周期报告到点直发，不去重也不限流，但如果上一份还在投递中，这一份可能排在后面。

## 改了配置没生效

配置在下一次 `bm2 serve` 时读取。

`bm2 reload` 只换守护进程，不重读项目配置，它接管的是正在运行的那些实例。

所以改实例数、端口、脚本这类东西，必须重新 `bm2 serve`。

## 装了新版 Bun 或 Node.js 不生效

守护进程的 `PATH` 在它启动的那一刻定格。

装完新运行时先换班，再启动项目。

```bash
bm2 reload
```

## 提示协议版本不一致

`bm2` 与 `bm2d` 版本必须配套。

只换了其中一个二进制、或者升级到一半中断，就会撞上这个提示。

```bash
bm2 reload
```

它会让旧守护进程退场、新守护进程接管。两个二进制都在的时候，这一步就够了。

## 名字被占用

```
project api already registered by /srv/other-api/bm2.toml
```

同一个名字只能属于一份配置。

两条出路：改名，或者先 `bm2 remove api` 把旧的退役。

退役时名字立刻可用，旧项目的统计、重启历史、日志都不会被新项目继承。

## 停止之后又自己起来了

不该发生。`bm2 stop` 的停止标记是持久化的，守护进程重启、`bm2 reload` 都不会把它带回来。

如果确实又起来了，检查是不是有人在守护进程之外又执行了 `bm2 serve`，或者有 systemd 单元在拉起应用目录。

## 状态目录与迁移

状态目录固定是 `~/.bm2`，不跟随工作目录。

换机器时把 `~/.bm2` 一起带走即可，但要注意里面的记录带着旧路径与旧 PID，新机器上它们没有意义。

更稳的做法是只带走 `logs/`，在新机器重新 `bm2 serve`。

没人管的旧目录用 `bm2 clean` 扫，`--dry-run` 可以先看。

## 权限与用户

一个用户一个守护进程。

不要把 bm2 放在 root 下管一堆属于不同用户的应用：实例会以守护进程的用户身份运行，文件权限、环境变量、日志归属全部跟着走。

需要多用户隔离时，每个用户各自装、各自跑，互不可见。

## 想在 WSL 里用

可以跑，但它仍然只支持 Linux。

WSL2 是完整内核，pidfd 可用。WSL1 不行。

两个常见差异：WSL 的 `localhost` 与 Windows 侧共享端口空间，端口冲突要在两边一起看。WSL 里的进程随发行版实例存活，关掉实例应用就停了，长期服务请放在真正的服务器上。

## 收集诊断信息

报问题时带上这三样，基本能定位。

```bash
bm2 version
bm2 list --json
tail -50 ~/.bm2/bm2d.events.jsonl
```

再加一份出问题实例的 `crash.log`。

凭据不会出现在这些输出里，通知的地址、密钥与密码在事件、日志、CLI 输出中都被抹掉，可以放心贴。
