文章目录
  1. 1. 背景与根因
  2. 2. 实施步骤
    1. 2.1. 第 1 步:确认根因(5 分钟)
    2. 2.2. 第 2 步:升级 opencode(社区已合并多个泄漏修复)
    3. 2.3. 第 3 步:用 systemd 托管,替代 tmux 裸跑(核心)
    4. 2.4. 第 4 步:看门狗——健康探活 + RSS 内存阈值(防”假死”和防 OOM 硬杀)
    5. 2.5. 第 5 步:低峰定期重启(可选兜底)
    6. 2.6. 第 6 步:加 swap 兜底(如果机器没有 swap)
  3. 3. 验证
  4. 4. 不建议的方向

适用环境: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
2
3
4
5
6
7
# 确认是 OOM killer 杀的
dmesg -T | grep -i -E "oom|killed process" | tail -20
journalctl -k --since "-2 days" | grep -i oom

# 记录当前版本与内存基线
opencode --version
free -h

预期能看到类似 Out of memory: Killed process ... (opencode) 的记录。

第 2 步:升级 opencode(社区已合并多个泄漏修复)

1
2
opencode upgrade   # 或用安装时的方式重装最新版
opencode --version

上游 20+ 位贡献者已针对内存泄漏提交过多个修复 PR,升级本身可能大幅缓解。

第 3 步:用 systemd 托管,替代 tmux 裸跑(核心)

tmux 只保住终端会话,进程死了不会拉起。

先准备环境文件(注意:systemd 服务不读 ~/.bashrc——那只对交互式登录 shell 生效,所以 Basic 认证凭证必须放专用环境文件,见官方认证文档):

1
2
3
4
5
6
mkdir -p /etc/opencode
cat > /etc/opencode/env <<'EOF'
OPENCODE_SERVER_USERNAME=你的用户名
OPENCODE_SERVER_PASSWORD=你的密码
EOF
chmod 600 /etc/opencode/env

然后写 /etc/systemd/system/opencode.service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[Unit]
Description=opencode server
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/root/opencode
# Basic 认证凭证:systemd 不读 ~/.bashrc,必须用 EnvironmentFile 注入
EnvironmentFile=-/etc/opencode/env
ExecStart=/usr/local/bin/opencode serve --port 4096 --hostname 127.0.0.1
Restart=always
RestartSec=3
StartLimitIntervalSec=0

# 资源隔离:泄漏时先杀 opencode 而不是拖垮整机
MemoryMax=4G
MemoryHigh=3G
OOMScoreAdjust=500
OOMPolicy=kill

# 日志进 journal,方便排查
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
1
2
systemctl daemon-reload
systemctl enable --now opencode

要点:

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/bash
# opencode 看门狗:健康探活 + RSS 阈值,任一触发即优雅重启
# RSS 阈值设为 MemoryMax(4G) 的 75%,确保在 systemd/内核硬杀之前先行动
RSS_LIMIT_KB=$((3 * 1024 * 1024)) # 3GB
PORT=4096

# cron 同样不读 ~/.bashrc,从与 systemd 共用的环境文件读取 Basic 认证凭证
CURL_AUTH=()
if [ -f /etc/opencode/env ]; then
. /etc/opencode/env
if [ -n "$OPENCODE_SERVER_PASSWORD" ]; then
CURL_AUTH=(-u "${OPENCODE_SERVER_USERNAME:-opencode}:${OPENCODE_SERVER_PASSWORD}")
fi
fi

PID=$(systemctl show -p MainPID --value opencode)
if [ -z "$PID" ] || [ "$PID" = "0" ]; then
exit 0 # 服务未运行,交给 systemd Restart=always 处理
fi

# 检查 1:健康探活(防假死)
# 官方健康检查端点:GET /global/health → { healthy: true, version: string }
# 见 https://opencode.ai/docs/zh-cn/server/#全局
# 不只看 HTTP 200,还校验响应体里的 healthy:true
if ! curl -sf -m 10 "${CURL_AUTH[@]}" "http://127.0.0.1:${PORT}/global/health" | grep -q '"healthy":true'; then
logger -t opencode-watchdog "health check failed (pid=$PID), restarting"
systemctl restart opencode
exit 0
fi

# 检查 2:RSS 内存阈值(防泄漏累积到被 OOM 硬杀)
RSS_KB=$(awk '/^VmRSS/{print $2}' "/proc/${PID}/status" 2>/dev/null)
if [ -n "$RSS_KB" ] && [ "$RSS_KB" -gt "$RSS_LIMIT_KB" ]; then
logger -t opencode-watchdog "RSS ${RSS_KB}KB exceeds limit ${RSS_LIMIT_KB}KB (pid=$PID), restarting"
systemctl restart opencode
fi
1
2
3
4
chmod +x /usr/local/bin/opencode-watchdog.sh

# crontab -e,每分钟检查一次
* * * * * /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
2
# crontab -e,每天凌晨 4 点重启(可选)
0 4 * * * systemctl restart opencode

如果观察一周后 watchdog 的 RSS 重启工作正常且频率可接受,这条可以去掉。

第 6 步:加 swap 兜底(如果机器没有 swap)

1
2
3
4
swapon --show   # 为空说明没有 swap
# 若无,创建 4G swap:
fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

swap 不解决泄漏,但把”瞬间被杀”变成”先变慢”,给看门狗留出反应时间。

验证

  1. 认证验证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,...}
  2. 自愈验证kill -9 $(pgrep -f "opencode serve"),几秒后 systemctl status opencode 应显示已自动重启,带凭证的 curl http://127.0.0.1:4096/global/health 返回 {"healthy":true,...}
  3. 探活验证:临时用 kill -STOP $(pgrep -f "opencode serve") 把进程挂起(模拟假死,进程活着但不响应),等 1-2 分钟,确认看门狗检测到探活失败并重启(journalctl -t opencode-watchdog)。
  4. RSS 阈值验证:把脚本里 RSS_LIMIT_KB 临时改成一个低于当前实际占用的值(先用 ps -o rss= -p $(pgrep -f "opencode serve") 看当前 RSS),等 1 分钟确认看门狗以 “RSS exceeds limit” 为由优雅重启,然后把阈值改回 3G。
  5. 内存限制验证:跑一个真实任务,systemctl status opencode 观察 Memory 行;正常情况下 RSS 应在到达 3G 阈值时被看门狗优雅重启,永远到不了 4G 的 MemoryMax 硬杀线。
  6. 持续观察journalctl -t opencode-watchdog --since "-7 days" 看重启频率和原因分布;如果 RSS 重启每天多次,说明泄漏严重,考虑调大 MemoryMax/阈值或反馈上游 issue。

不建议的方向

  • 多实例 + nginx 负载均衡:opencode 的会话状态存在本地磁盘(~/.local/share/opencode),多实例共享同一份数据有并发冲突风险,且客户端会话有粘性,收益低于风险,暂不做。
  • events.setMaxListeners:需要改源码/注入代码,且该警告本身无害,治标不治本。
文章目录
  1. 1. 背景与根因
  2. 2. 实施步骤
    1. 2.1. 第 1 步:确认根因(5 分钟)
    2. 2.2. 第 2 步:升级 opencode(社区已合并多个泄漏修复)
    3. 2.3. 第 3 步:用 systemd 托管,替代 tmux 裸跑(核心)
    4. 2.4. 第 4 步:看门狗——健康探活 + RSS 内存阈值(防”假死”和防 OOM 硬杀)
    5. 2.5. 第 5 步:低峰定期重启(可选兜底)
    6. 2.6. 第 6 步:加 swap 兜底(如果机器没有 swap)
  3. 3. 验证
  4. 4. 不建议的方向