# 能力总览

这一页按「能力」组织：bm2 能做什么、怎么做、代价是什么。

具体命令与字段分别在[命令参考](/guide/commands)与[配置参考](/guide/configuration)里，这里不重复字段清单。

## 进程托管

一份 `bm2.toml` 描述一个项目：单个应用，一个或多个实例，上限 `1024`。

`bm2 serve` 校验配置、探测运行时、登记项目、启动实例、写状态。

`bm2 dev` 是同一套监督，换成开发模式与单实例，并前台跟随日志。

应用被拉起的命令固定是 `<runtime> <script>`，工作目录是 `cwd`，没有额外参数、没有 shell 包装。

每次启动都是一次完整重启：旧实例全部停下，再按当前配置重建。没有零停机重载，发布与灰度交给网关和你的发布流程。

## 自愈与重启预算

异常退出后按 `restart_delay_ms` 延迟重启，`spawn` 失败时按失败次数线性递增退避。

连续异常重启超过 `max_restarts` 就进入 `errored` 并停止自愈，等人介入。项目本身仍在册，`bm2 list` 看得到。

「异常」有三种：非零退出码、被信号杀掉、退出码 `0` 但没活过 `min_uptime_ms`。

预算只在一种情况下清零：干净退出并且活过 `min_uptime_ms`，随后按计划重启。所以一个每 15 分钟崩一次的进程，不会因为「活得够久」而把自己的记录洗掉。

代价要说清：这个预算按**实例**计，不是按项目。一个实例反复崩溃进入 `errored`，它的邻居继续服务。

## 内存上限

`max_memory_mb` 是单实例的内存上限，默认 `512`。

守护进程每 `5s` 采样一次 RSS，统计的是这个实例的**整棵进程树**：应用自己 fork 出来的 worker 也算在内。只盯主进程会漏掉真正吃内存的那部分。超过上限就记 `memory_limit` 事件，然后按异常重启处理：`SIGTERM`，超过 `stop_timeout_ms` 升级 `SIGKILL`，计数进重启预算。

进程树来自 `/proc/<pid>/task/<tid>/children` 的递归。单棵树最多看 `max_tree_processes` 个进程，默认 `512`，到顶就按已看到的部分统计，不会为了一个 fork 炸弹把守护进程拖死。

它不是容器级隔离。超限那一刻进程已经吃掉了那么多内存，bm2 只能在事后处理。真正的硬隔离要靠 cgroup 或容器。

另外还有一个判定：收到 `SIGKILL` 且最近采样已到上限的 90%，判为内核 OOM killer，记 `oom_kill`。这两条在通知里的级别不同，看到 `oom_kill` 说明内存压力已经打到系统层。

## 两种多实例方式

`cluster` 是默认值。所有实例共用一个端口，靠内核的 `SO_REUSEPORT` 分发，没有中间层也没有单点。条件是应用自己开 `reusePort`。bm2 启动后用 `bind` 探测确认，没开就停下整个项目并说明原因。

`fork` 让端口从 `port` 起连续分配，每个实例一个端口，分发交给网关。应用不需要 `reusePort`，但实例增减要同步网关的 upstream。

取舍很直接：`cluster` 少一层、少一个要维护的 upstream，代价是应用必须配合、分发由内核决定。`fork` 不要求应用配合、可以按实例分流，代价是端口规划与网关同步。

专页：[cluster 模式](/guide/mode-cluster)、[fork 模式](/guide/mode-fork)、[两种模式怎么选](/guide/load-balancing)。

## 守护进程崩溃不打断应用

守护进程用 pidfd 钉住每一个被托管的进程。pidfd 指向的是那个进程本身，不会因为 pid 被复用而认错。

它自己崩溃时应用继续运行。新的守护进程启动后：

1. 读 `project.json`，恢复注册。
2. 读实例状态文件，用 pidfd 打开记录的 pid。
3. 读 `/proc/<pid>/environ`，确认带着这个项目的 `BM2_APP_NAME`、`BM2_INSTANCE_ID`、`BM2_APP_PORT`。
4. 三项都对才接管，状态回到 `online`，PID 不变。环境不匹配的进程一律不碰，标为 `pid_conflict`。

代价是守护进程不在的这段时间里没人监督：那段时间内的崩溃不会被自动重启，直到新守护进程接手。

`bm2 reload` 与 `bm2 upgrade` 走的就是这条路：旧守护进程 detach，新守护进程接管，应用不重启。

## 优雅停止

`bm2 stop` 先向进程组发 `SIGTERM`，超过 `stop_timeout_ms` 仍未退出就 `SIGKILL`。

每个实例用 `setsid` 起在自己的会话里，所以信号发给整个进程组：应用 fork 出来的子进程一起收到。

停止是持久的：停止标记写在 `project.json` 里，守护进程重启、`bm2 reload` 都不会把它带回来。

需要连接排空的应用要自己处理 `SIGTERM`：拒绝新连接、处理完进行中的请求、再退出。

## 日志

每个实例三份日志：标准输出、标准错误、崩溃诊断，落在 `~/.bm2/<name>/logs/`。

守护进程每 `10s` 检查一次大小，超过 `[logs] max_size_mb` 即轮转，`keep` 决定保留几代，`0` 表示不限代数。

轮转对应用透明：应用自己持有的文件描述符继续有效，不会因为改名而写进一个失联的旧文件。代价是拷贝与截断之间写入的几行会丢。

崩溃诊断是人能直接读的键值行，写清 `reason`、退出码或信号、重启次数。

细节在[状态与日志](/guide/state-and-logs)。

## 事件

两个事件流：`bm2d.events.jsonl` 记守护进程与监督，`bm2.events.jsonl` 记 CLI 连接与重试。

每行一个 JSON 对象，只有管理元数据：时间、事件名、项目、实例、PID、原因。不含环境变量值、应用输出与通知凭据。

事件名是固定英文键，脚本可以长期依赖。

## 通知与上报

七个平台开箱可用：飞书、钉钉、企业微信、Slack、Discord、通用 webhook、邮件。

事件类通知经过去重、限流、队列与退避重试：同一签名在窗口内只发一条，按项目限流，按项目排队，最多三次尝试。

周期类报告用标准 cron 表达，模板有 `health` 与 `stats` 两种。

投递是独立任务，端点慢或挂都不占用监督心跳。监督循环里没有等待点，只往队列里放一份消息。

按项目配置：一个项目只上报自己的事件，只发给自己配置的目标。

细节在[通知](/guide/notifications)。

## bm2 不停机自升级

`bm2 upgrade` 查 mooncakes 上的最新版本，用 `moon install chensuiyi/bm2/...` 换入两个二进制，再让新守护进程接管。

应用全程继续运行，PID 不变。

`moon install` 失败时旧版本原样保留，不会留下半套新版本。机器上没有 `moon` 时它直接说明，不会自己去下载别的东西。

只换了二进制、不想重新安装时用 `bm2 reload`，效果相同。

## 每个项目的隔离

同一台机器上多个项目共享一个守护进程，但状态按项目分开：

- 端口范围不能重叠，登记时就检查。
- 日志、轮转策略、通知策略、重启预算都按项目。
- 名字全局唯一，名字就是身份。
- 注销一个项目会清掉它的全部内存状态：统计窗口、重启风暴历史、探测记录、通知队列与去重表。同名再登记不会继承上一代的数字。

## 安全边界

- 控制通道走本机回环 `TCP`，加一枚每代随机的共享令牌。令牌比较用常量时间。端点文件权限 `0600`，状态目录权限 `0700`。
- 并发控制连接上限 `64`，握手超时 `5s`，单条消息上限 `4 MiB`：本地跑飞的客户端不能把守护进程撑大。
- 配置是静态 TOML，逐字段校验，任何内容都不会被执行。
- 环境只传白名单、守护进程的 `BM2_*` 与项目自己的 `.env`，不继承调用方的整套环境。
- 应用启动前关闭除 stdio 以外的全部 fd：守护进程的 pidfd、socket、终端不会漏给应用。
- 事件、状态文件、CLI 输出里不出现环境变量值、通知地址与凭据。

## 它不做什么

- 反向代理、TLS、域名：交给 Nginx 或 Caddy。
- 开机自启：交给 systemd。bm2 不写 init 单元，也不在守护进程启动时把项目拉起来。
- 热重载、灰度发布、跨机编排、远程运维：交给发布流程与编排工具。
- 容器化与资源硬隔离：交给容器运行时。

这些边界是有意的。一个进程管理器把边界守住，才可能把托管这一件事做稳。

## 关键数字

| 项 | 值 |
| --- | --- |
| 监督心跳 | `50ms` |
| 内存采样间隔 | `5s` |
| 日志轮转检查间隔 | `10s` |
| cluster 端口探测间隔 | `500ms`，每次拉起的探测窗口 `60s` |
| 阈值与周期报告检查间隔 | `60s`（内存阈值随每次内存采样一起判断） |
| 即时通知最多尝试 | `3` 次，退避 `5s` 再 `15s` |
| 控制连接并发上限 | `64` |
| 命令应答超时（守护进程侧） | `15s` |
| 实例数上限 | `1024` |
| 端口范围 | `1 ~ 65535` |
