容器化部署的一个隐形坑:WebSocket 重连上限把服务「熬」死了
## 背景
一套自研的机器人网关,NapCat 负责对接 QQ,宿主服务通过 WebSocket(forward 模式)连它的 3001 端口。跑了大半年没出过事,直到某天重启 NapCat 后,宿主服务的日志开始刷这个:
```
[dsh-onebot] forward WS error: connect ECONNREFUSED 172.17.0.3:3001
[dsh-onebot] forward WS error: connect ECONNREFUSED 172.17.0.3:3001
...
```
刷了几百条,**但没有一条说"我要放弃了"**。等到发现时,机器人已经掉线几十分钟。
## 第一反应:IP 变了
Docker 容器重启后 IP 变化是最经典的坑,先查这个:
```bash
docker inspect napcat --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
# 172.17.0.3 ← 没变
```
IP 没变,那问题在哪?
## 第二个怀疑:端口没起来
查容器内监听端口:
```bash
docker exec napcat sh -c 'cat /proc/net/tcp' | awk 'NR>1 {split($2,a,":"); print a[2]}'
```
输出一堆十六进制。转换成十进制:
```bash
# 0BBA = 3002 (HTTP)
# 0BB9 = 3001 (WebSocket) ← 这个没出现
```
所以 NapCat 起来后只监听了 HTTP 的 3002,WebSocket 的 3001 还没起。
**为什么?** 因为 QQ 客户端本身还没登录成功——NapCat 必须等 QQ 登录后才开 WebSocket 服务。而当时正在等扫码。
## 真正的问题:重连上限
找到源码里的重连逻辑:
```javascript
const RECONNECT_BACKOFF = [2, 5, 10, 30, 60]; // 秒
const index = Math.min(this.reconnectAttempts - 1, RECONNECT_BACKOFF.length - 1);
const delay = RECONNECT_BACKOFF[index] * 1000;
```
配置里还有个:
```typescript
/**
* Forward-mode reconnect attempt limit before giving up with a recovery hint.
* Undefined keeps the built-in default (100).
*/
reconnectMaxAttempts?: number;
```
**默认 100 次**。按阶梯算:
| 次数 | 间隔 |
|---|---|
| 1-5 | 2s / 5s / 10s / 30s / 60s |
| 5-100 | 全部 60s |
100 次跑完 ≈ 100 分钟。之后插件放弃,只剩一个慢速自愈循环(每小时有次数上限)。
**我遇到的场景**:重启 NapCat → 等你扫码(可能几分钟)→ 你扫码登录 → NapCat 开 3001。如果超过 100 分钟,插件就永久放弃了。
## 修复:改成永久重试
```yaml
- id: dsh-onebot
config:
mode: forward
url: "ws://172.17.0.3:3001"
reconnectMaxAttempts: 0 # 0 = 永不放弃
```
改完实测:重启 NapCat,21 秒后自动恢复,零人工干预。
## 三条经验
**1. 日志刷屏 ≠ 有进展**
`ECONNREFUSED` 刷几百条,看起来像"在努力重连",实际上每次都是同一秒失败。真正需要关注的是"**成功**"和"**放弃**"这两种状态变化。
**2. 后退退避的「上限」要慎设**
退避到 60 秒没问题,但给它设一个总次数上限就很危险——因为**你不知道对端什么时候会回来**。容器化环境下,对端重启 30 秒很常见,但等待扫码登录可能几十分钟。这种情况下,"永不放弃"比"重试 N 次"安全得多。
**3. 依赖启动顺序的服务,要么重试到底,要么做健康检查**
顺带记一个 docker 的坑:`--network container:` 绑定的是固定 netns,`docker restart` 会重建容器 ID,依赖它的容器直接断网。这种情况只能 `rm -f` + `run` 重建。
## 小结
| 现象 | 真实原因 | 修复 |
|---|---|---|
| ECONNREFUSED 刷屏 | 对端 WebSocket 未监听(等登录)| — |
| 重连刷了几百条就停了 | `reconnectMaxAttempts` 默认 100 | 设成 0 |
| IP 没变也不通 | 服务端口还没开 | 加健康检查 |
修复后,容器重启从"必须人工介入"变成"等 20 秒自己好"。这类改动只有一行配置,但省下来的排查时间值几百倍。
- 点赞
- 收藏
- 关注作者
评论(0)