opencode server 高可用加固方案(不改源码)
适用环境:Linux + tmux +
opencode serve
问题现象:运行任务一段时间后进程被Killed,日志出现MaxListenersExceededWarning
背景与根因
opencode serve --port 4096 在运行任务一段时间后进程被 Killed。日志中的 MaxListenersExceededWarning 只是事件监听器泄漏的警告(不会杀死进程),真正杀死进程的是 Linux OOM killer——这是 opencode 的已知上游缺陷:多个 GitHub issue(#17908、#9743、#17237、#13230)报告 server 模式下内存无界增长直到被内核杀死,#28492 报告了同样的 MaxListeners 警告。
既然不能改源码,思路是:确认根因 → 升级止血 → 进程自愈(systemd)→ 资源隔离 → 看门狗(探活 + RSS 阈值)→ 可选定期重启。
实施步骤
第 1 步:确认根因(5 分钟)
1 | # 确认是 OOM killer 杀的 |
预期能看到类似 Out of memory: Killed process ... (opencode) 的记录。
第 2 步:升级 opencode(社区已合并多个泄漏修复)
1 | opencode upgrade # 或用安装时的方式重装最新版 |
上游 20+ 位贡献者已针对内存泄漏提交过多个修复 PR,升级本身可能大幅缓解。
第 3 步:用 systemd 托管,替代 tmux 裸跑(核心)
tmux 只保住终端会话,进程死了不会拉起。
先准备环境文件(注意:systemd 服务不读 ~/.bashrc——那只对交互式登录 shell 生效,所以 Basic 认证凭证必须放专用环境文件,见官方认证文档):
1 | mkdir -p /etc/opencode |
然后写 /etc/systemd/system/opencode.service:
1 | [Unit] |
1 | systemctl daemon-reload |
要点:
Restart=always+RestartSec=3:被 OOM 杀掉后 3 秒自动拉起,这是高可用的核心。MemoryMax=4G(按机器实际内存调整,建议留给系统 ≥25%):把泄漏爆炸半径限制在服务内,避免 #13230 那种拖垮整个内核的情况。OOMScoreAdjust=500:即使系统级 OOM,也优先杀 opencode 而不是 sshd 等关键进程。- tmux 以后只用来跑交互式 TUI,不再承载 server。
第 4 步:看门狗——健康探活 + RSS 内存阈值(防”假死”和防 OOM 硬杀)
systemd 只能处理进程退出,处理不了两种情况:进程活着但不响应(假死),以及内存缓慢泄漏逼近 MemoryMax(最终被 SIGKILL 硬杀,正在跑的任务直接丢失)。看门狗脚本同时覆盖这两种情况——RSS 超过阈值时用 systemctl restart 优雅重启(先 SIGTERM),抢在 OOM SIGKILL 之前主动处理,这比固定时间重启更精准:内存不涨就不重启,涨了就及时重启。
写 /usr/local/bin/opencode-watchdog.sh:
1 |
|
1 | chmod +x /usr/local/bin/opencode-watchdog.sh |
要点:
- **阈值 = MemoryMax 的 ~75%**(4G 限额对应 3G 阈值,机器内存不同按此比例调整)。留出 25% 余量是为了保证看门狗总能赶在 cgroup OOM SIGKILL 之前触发优雅重启。
- 探活失败时先重启、直接退出,不再读 RSS——假死进程的
/proc状态可能已不可信。 - 日志统一打到 syslog(tag:
opencode-watchdog),用journalctl -t opencode-watchdog可回溯每次重启的原因(假死 vs 内存超限)。 GET /global/health是官方文档定义的健康检查端点,返回{ "healthy": true, "version": "..." }。脚本校验的是响应体的healthy:true而非仅 HTTP 200,能识别”进程活着但内部不健康”的状态。若你的 opencode 版本较旧、该端点 404,先执行第 2 步升级;临时可用curl -sf http://127.0.0.1:4096/doc(OpenAPI 规范端点)代替。- Basic 认证凭证从
/etc/opencode/env读取(与 systemd 单元共用同一份),用户名缺省为opencode。**不要依赖~/.bashrc**:systemd 和 cron 都不会加载它,只有交互式登录 shell 才会——这是 “手动跑正常、服务里报OPENCODE_SERVER_PASSWORD is not set; server is unsecured“ 的典型原因。
第 5 步:低峰定期重启(可选兜底)
RSS 阈值检查已经覆盖了慢性泄漏的主要场景(内存涨到阈值自动优雅重启),定期重启降级为可选的双保险——适合任务有明显低峰期、希望重启时机可预测的场景:
1 | # crontab -e,每天凌晨 4 点重启(可选) |
如果观察一周后 watchdog 的 RSS 重启工作正常且频率可接受,这条可以去掉。
第 6 步:加 swap 兜底(如果机器没有 swap)
1 | swapon --show # 为空说明没有 swap |
swap 不解决泄漏,但把”瞬间被杀”变成”先变慢”,给看门狗留出反应时间。
验证
- 认证验证:
systemctl restart opencode后,journalctl -u opencode -n 20里不应再出现OPENCODE_SERVER_PASSWORD is not set; server is unsecured;不带凭证的curl -i http://127.0.0.1:4096/global/health应返回 401,带凭证的curl -u "用户名:密码" ...返回{"healthy":true,...}。 - 自愈验证:
kill -9 $(pgrep -f "opencode serve"),几秒后systemctl status opencode应显示已自动重启,带凭证的curl http://127.0.0.1:4096/global/health返回{"healthy":true,...}。 - 探活验证:临时用
kill -STOP $(pgrep -f "opencode serve")把进程挂起(模拟假死,进程活着但不响应),等 1-2 分钟,确认看门狗检测到探活失败并重启(journalctl -t opencode-watchdog)。 - RSS 阈值验证:把脚本里
RSS_LIMIT_KB临时改成一个低于当前实际占用的值(先用ps -o rss= -p $(pgrep -f "opencode serve")看当前 RSS),等 1 分钟确认看门狗以 “RSS exceeds limit” 为由优雅重启,然后把阈值改回 3G。 - 内存限制验证:跑一个真实任务,
systemctl status opencode观察 Memory 行;正常情况下 RSS 应在到达 3G 阈值时被看门狗优雅重启,永远到不了 4G 的 MemoryMax 硬杀线。 - 持续观察:
journalctl -t opencode-watchdog --since "-7 days"看重启频率和原因分布;如果 RSS 重启每天多次,说明泄漏严重,考虑调大 MemoryMax/阈值或反馈上游 issue。
不建议的方向
- 多实例 + nginx 负载均衡:opencode 的会话状态存在本地磁盘(
~/.local/share/opencode),多实例共享同一份数据有并发冲突风险,且客户端会话有粘性,收益低于风险,暂不做。 - 调
events.setMaxListeners:需要改源码/注入代码,且该警告本身无害,治标不治本。
