一台 2G ECS 的 pi-web 运维记录 #ai
给 pi-web(我的 AI 编码助手控制台)做的一次整机优化:2G 内存 ECS,Debian 12,Caddy 反代 + docker 一堆服务。记下问题和解法,下次直接抄。
Caddy 认证修复
四个坑,都是配置层面:
| 问题 | 根因 | 修复 |
|---|---|---|
| 登录页 401 死路 | 路径匹配器 /auth/* 不匹配裸路径 /auth | 改 @portal path /auth /auth/* |
| 改 Caddyfile 后不生效 | compose configs.content 不随内容更新 | 改 bind mount Caddyfile:/etc/caddy/Caddyfile:ro,改文件即生效 |
| 登录后不跳回页面 | 缺 trust login redirect uri,redirect_url 被丢弃 | 加 trust login redirect uri domain <域名> path prefix / |
| 15 分钟重新登录 | JWT 令牌默认有效期 900 秒 | 加 crypto default token lifetime 172800(48 小时) |
教训:path /auth/* 不匹配 /auth 本体;compose 的 configs 内容改了不重发,配置源要 bind mount + caddy reload 热加载。
内存:2G 机器的三板斧
- 2G swapfile,swappiness=30,写入 fstab
NODE_OPTIONS=--max-old-space-size=512,防 node 子进程各自申请大堆导致 pnpm OOM- pnpm 并发调低:
child-concurrency=1、network-concurrency=8 - 停掉可恢复的服务:tailscaled、portainer、memos、exim4
- 可用内存 640MB → 808MB
日志滚动与磁盘
journald 没设上限,悄悄占掉 1.1GB。设 SystemMaxUse=50M、SystemKeepFree=256M 后 vacuum,回收 928MB。docker 构建缓存 prune 掉 3.9GB,再删一批无引用镜像,磁盘可用从 6.5G 回到 11G。
整个过程的先后顺序很重要:先建 swap 再动构建,先设 NODE_OPTIONS 再跑 pnpm,每一步都在 OOM 边缘试出来的。
sessiond 启动 OOM:全局插件一次性预热的代价
当天下午 pi-web 起不来,网页端会话接口全部 502/503,pi-web-sessiond 每 ~27 秒崩一次。日志反复出现:
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
先排掉三个嫌疑:浏览器连接(停掉 server 切断浏览器后依旧崩)、会话文件(移走 668 行/1.4MB 的旧会话依旧崩)、系统内存(崩溃是 V8 堆上限 ABRT,不是内核 OOM killer)。
根因是当天 12:27 给 pi agent 新装了一批包(rpiv-*、recheck、@ast-grep 等),settings.json 全局插件扩容后,12:28 一重启必崩。sessiond 启动会执行 global extension provider bootstrap:把全部全局插件一次性加载、编译、注册进共享 runtime 并冻结,瞬时 V8 堆需求超过 512MB。而 /etc/profile.d 里的全局 NODE_OPTIONS=--max-old-space-size=512 把堆卡死在 512MB,启动即 ABRT,无限重启。
验证方法:上限提到 1GB 后 18 秒完成引导(global extension provider baseline bootstrapped and frozen),稳定 ~200MB。峰值有界,不是内存泄漏。
设计取舍:sessiond 先「预热共享 runtime 再冻结」,换来所有会话秒开、共享 provider,代价是启动时一次性加载全部插件;jiti 编译 TS 插件 + provider/模型目录/工具注册,瞬时峰值高。pi CLI 没这个问题——按需加载,没有预热步骤,同一 512MB 限制下跑 TUI 峰值 ~270MB、稳定 ~190MB。
修复只放宽 pi-web-sessiond.service 的堆上限,全局 512MB 限制不动:
ExecStart=... bash -lc "export NODE_OPTIONS=--max-old-space-size=2048; exec pi-web-sessiond"
先改 1024 验证能启动,再改 2048 留余量;原文件备份在 ~/.config/systemd/user/pi-web-sessiond.service.bak-20260825。2G 上限下验证 90 秒+:稳定 ~194MB、0 OOM,系统可用 ~1GB,swap ~280MB。恢复方式:把那行 2048 改回 1024/512,systemctl --user daemon-reload && systemctl --user restart pi-web-sessiond。
全局插件重量清单(settings.json,agent npm 目录共 391MB):
| 插件 | 体积 | 关键依赖/原生组件 | 加载成本 | 建议 |
|---|---|---|---|---|
| @jmfederico/pi-web 本体 | 5.9M | node-pty(62M)、xterm、codemirror、fastify | 必带 | 保留 |
| pi-lens | 27M | @ast-grep(108M)、web-tree-sitter、recheck、13 个 wasm/原生 | 最高 | 不常用就移出全局 |
| pi-web-access | 7.3M | linkedom、unpdf、turndown、undici | 中 | 按需 |
| pi-mcp-adapter | 2.6M | MCP client、keyring、recheck、zod | 中 | 不用 MCP 就移出 |
| @juicesharp/rpiv-ask-user-question | 408K | rpiv-config、typebox(TS 源码) | 低 | 用到就留 |
| @juicesharp/rpiv-todo | 216K | rpiv-config、typebox(TS 源码) | 低 | 同上 |
| @jmfederico/pi-relay | 184K | 无 | 极低 | 保留 |
2G 是「护栏上限」不是实际占用:实际启动峰值 ~600MB、稳态 ~200MB,设 2G 只为未来插件留余量。真正红线是物理内存 1.7G + 2G swap:swap 慢,峰值逼近 1.2-1.4G 会先明显卡顿,再上就内核 OOM 杀进程(可能误伤 caddy/docker)。插件按需装,重插件尽量项目级加载,每加一批插件重启一次看启动峰值;终极方案是 ECS 升配到 4G。全局 512MB 上限对 codex 等其它 node 进程仍有效,没动。