一次备用节点 OOM 事故复盘:为什么 bot 会突然失联

    3

一次备用节点 OOM 事故复盘:为什么 bot 会突然失联

这次问题出在我的一台备用节点上。

它本来不是一台“只干一件事”的 VPS。备用 Agent worker、API proxy、Halo、数据库、缓存,还有一堆 Docker 服务,全都挤在这台 4G 内存的小机器上。更糟的是,当时没有 swap。

平时看着没事。服务都在,端口都开,bot 也能回。可这种堆法其实很脆,只要某个进程突然吃内存,整台机器就会被一起拖下水。

这次就是这样。

一开始,我以为是 bot 挂了

最早看到的现象很散:

  • 备用 Telegram bot 私聊没反应;
  • SSH 能连到端口,但卡在 banner 阶段;
  • 有些 HTTP 服务变慢、超时;
  • 外部扫端口,又能看到常见端口还是开的。

这类状态特别容易误判。

如果机器彻底挂了,反而简单。端口全关、服务全断,一眼就知道要进控制台救机器。麻烦的是它没有死透。它还在喘气,但已经没力气正常处理请求了。

所以从外面看,它像 Telegram bot 问题,像 SSH 问题,也像反代或者某个容器单独抽风。

但都不是根因。

真正的问题是宿主机 OOM

后来进 VPS 控制台看日志,才确认是宿主机级别的 OOM。

内核日志里能看到类似这样的记录:

Out of memory: Killed process ...

有高内存进程被 OOM killer 干掉,Java 进程也受影响,连 `systemd-journald` 这种基础日志服务都开始异常。

这就不是“某个容器挂了”那么简单了。

当时机器的状态大概是:

  • 总内存约 4G;
  • 内存基本打满;
  • 可用内存一度只剩几十 MiB;
  • swap 是 0。

没有 swap 的小机器很容易这样。内存一顶满,系统没有缓冲区,只能直接杀进程。杀到哪个算哪个,业务进程、bot、日志服务都有可能被波及。

对一台同时跑 Agent、API proxy、Halo、数据库和 Docker 服务的机器来说,这个配置太冒险了。

为什么它看起来不像 OOM

这次最迷惑人的地方是:机器没有完全死。

端口还开着,说明网络层看起来没断;HTTP 服务不是全部 502,而是有的慢、有的超时;SSH 端口能连上,却卡在 banner 阶段。

这通常说明系统已经非常吃力了。不是服务没启动,而是调度、内存、IO 这些底层资源已经被压到快动不了。

所以排查方向很容易被带偏:

  • 先怀疑 SSH 配置;
  • 再怀疑 bot token 或 gateway;
  • 然后怀疑反代;
  • 最后盯着某个业务容器看半天。

但真正该看的,是宿主机本身还有没有余量。

这次就是资源被打穿了,只是机器还没彻底断气。

救火时,我先把机器拉回来

这种情况下,第一目标不是修某个业务,而是让系统先恢复呼吸。

我当时的处理顺序是:

1. 先停掉明显高风险、高占用的进程;

2. 恢复基础系统服务,比如日志服务;

3. 恢复备用 Agent gateway;

4. 立刻补 swap;

5. 再确认 SSH、HTTP 服务和关键容器是否恢复。

补上 2G swap 后,效果很明显。

`free -h` 里能看到 swap 生效,系统从只剩几十 MiB 可用内存,恢复到接近 1G available。SSH 也能重新连上了。

swap 当然不能替代真正的内存。它慢,也不适合长期扛重负载。但在救火时,它能争取时间,避免一个瞬时尖峰直接把机器打死。

这一步很值。

后续怎么防复发

这次之后,我会把防复发分成几层做。

1. 小内存机器必须有 swap

4G 级别的小 VPS,至少要有 2G 到 4G swap。

这不是扩容方案,只是安全垫。它不能让机器变强,但能降低瞬时 OOM 的概率。

2. 重服务要加内存边界

Docker 容器不能继续全部无限额跑。

优先要限制这些东西:

  • Java 类服务;
  • 浏览器 / Electron 类进程;
  • API proxy;
  • 数据库相关容器;
  • 长期驻留的后台服务。

不然某一个服务突然吃内存,就可能把整台机器一起拖死。

3. 构建和运行要分开

小内存生产机不适合频繁直接构建重项目。

构建前至少要看几件事:

  • 可用内存够不够;
  • swap 是否存在;
  • DNS 是否正常;
  • 代理变量有没有污染环境。

条件不健康就不要硬跑。小机器最怕的不是慢,而是你以为它只是慢,结果它已经在 OOM 边缘了。

4. 预警不能依赖 bot 自己

这点很重要。

备用 bot 失联之后再靠备用 bot 报警,本身就是悖论。内存真的爆掉时,bot 可能已经卡死,甚至已经被系统杀了。

更稳的做法是在系统层放一个很轻的脚本,用 systemd timer 每分钟检查内存、swap、OOM 日志和 Docker 健康状态。

一旦可用内存低于阈值,或者内核日志里出现 OOM 关键词,就直接通过 Telegram Bot API 发消息。这个报警链路要尽量短,不能依赖那套已经可能被打挂的 Agent gateway。

我准备至少做这些提醒:

  • 可用内存低于 700MiB:提醒;
  • 可用内存低于 400MiB:高优先级提醒;
  • `journalctl -k` 出现 `Out of memory`、`oom-kill`、`Killed process`:立刻提醒;
  • 关键 Docker 容器 unhealthy 或频繁重启:提醒;
  • 构建前自动检查内存、swap、DNS 和代理变量。

这次最大的教训

备用节点不等于可以随便塞服务。

尤其是 4G 内存这种小 VPS,同时跑 Agent worker、API proxy、Halo、数据库和一堆 Docker 服务时,必须默认它会出现资源尖峰。

没有 swap、没有资源限制、没有预警时,机器不会很体面地坏掉。它更可能变成这种半死不活的样子:

  • 端口还开着;
  • SSH 卡住;
  • bot 不回;
  • 日志服务异常;
  • 容器随机被 OOM killer 杀掉。

从外面看,像网络问题,像 bot 问题,像服务问题。其实是宿主机已经撑不住了。

后面这类备用节点要重新定位:可以继续做 worker,但不能无限制叠高内存服务。关键容器要有资源边界,预警也要独立出来。

这样下次内存快打满时,我能先收到提醒,再介入处理。不要等 bot 都失联了,才发现机器已经在水下憋了半天。

消息盒子

# 暂无消息 #

只显示最新10条未读和已读信息