跳转到正文

一台 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=1network-concurrency=8
  • 停掉可恢复的服务:tailscaled、portainer、memos、exim4
  • 可用内存 640MB → 808MB

日志滚动与磁盘

journald 没设上限,悄悄占掉 1.1GB。设 SystemMaxUse=50MSystemKeepFree=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.9Mnode-pty(62M)、xterm、codemirror、fastify必带保留
pi-lens27M@ast-grep(108M)、web-tree-sitter、recheck、13 个 wasm/原生最高不常用就移出全局
pi-web-access7.3Mlinkedom、unpdf、turndown、undici按需
pi-mcp-adapter2.6MMCP client、keyring、recheck、zod不用 MCP 就移出
@juicesharp/rpiv-ask-user-question408Krpiv-config、typebox(TS 源码)用到就留
@juicesharp/rpiv-todo216Krpiv-config、typebox(TS 源码)同上
@jmfederico/pi-relay184K极低保留

2G 是「护栏上限」不是实际占用:实际启动峰值 ~600MB、稳态 ~200MB,设 2G 只为未来插件留余量。真正红线是物理内存 1.7G + 2G swap:swap 慢,峰值逼近 1.2-1.4G 会先明显卡顿,再上就内核 OOM 杀进程(可能误伤 caddy/docker)。插件按需装,重插件尽量项目级加载,每加一批插件重启一次看启动峰值;终极方案是 ECS 升配到 4G。全局 512MB 上限对 codex 等其它 node 进程仍有效,没动。