首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >3.7GB小服务器跑19个容器,OOM杀掉MySQL致站点全挂:排查与修复实录

3.7GB小服务器跑19个容器,OOM杀掉MySQL致站点全挂:排查与修复实录

原创
作者头像
用户11922017
发布于 2026-10-15 17:07:43
发布于 2026-10-15 17:07:43
60
举报

背景:一台贴着天花板跑的小服务器

这台 2 核 / 3.7GB 内存的云服务器上常年跑着一套不小的家当:

  • 19 个 Docker 容器(nginx 反代、两套应用数据库、缓存、监控面板、文档站……)
  • 宝塔面板 + 主机版 MySQL + Redis 各一套
  • 两个 Node 应用 + 一个文档系统 + 一个 AI 编码工具的 web 进程

内存占用常年在 2.6GB / 3.7GB 附近,swap 早已用掉 1.8GB / 1.9GB——也就是说,机器已经贴着天花板硬撑了很长时间,只差一次波动。

故障在凌晨爆发。下面是完整的时间线、排查过程和修复方案,全部是本次真实记录。

一、故障时间线

时间

现象

约 00:10

站点开始无法访问,HTTPS 443 连接建立失败

00:11:02

内核 OOM killer 触发,MySQL 进程被杀

00:12~00:15

mysqld_safe 自动重启 MySQL,磁盘 IO 拉满,load 飙到三位数

00:15 后

负载逐渐回落,站点恢复

00:20 排查时

load average: 0.48, 49.60, 106.52

最刺眼的是 load:1 分钟前 0.48、5 分钟前 49.6、15 分钟前 106——对一台 2 核机器来说,这是彻底的雪崩。

二、排查过程(四步定位)

第 1 步:先判断是「机器挂了」还是「服务挂了」

代码语言:javascript
复制
ping -c 3 服务器IP        # 通,但丢包 33%,rtt ≈ 60ms
# TCP 22 能握手,SSH banner 却超时
# TCP 443 直接连不上

机器活着,但端口层面一通一不通——这个组合通常指向系统级过载(CPU/内存/IO 打满),而不是服务进程崩溃。方向定为:先抢 SSH 进去看系统状态。

第 2 步:SSH 进去,一眼看到元凶

代码语言:javascript
复制
load average: 0.48, 49.60, 106.52
Mem: total 3724  used 2625  free 316  available 1098
Swap: 1987 total / 1876 used / 111 free

swap 只剩 111MB,内存 available 只剩 1GB——内存和 swap 双双见底。磁盘(63%)和负载回落速度都正常,排除磁盘满和持续性攻击。

第 3 步:内核日志给出确凿证据

代码语言:javascript
复制
journalctl --since "00:00" --until "00:20" | grep -i oom
代码语言:javascript
复制
kernel: Out of memory: Killed process 1625616 (mysqld)
        total-vm:2066996kB, anon-rss:763988kB, UID:1003
kernel: oom-kill:constraint=CONSTRAINT_NONE, global_oom,
        task_memcg=/system.slice/mysqld.service, task=mysqld
systemd: mysqld.service: A process of this unit has been killed by the OOM killer.

根因坐实:00:11:02 全局 OOM,RSS 763MB 的 mysqld 被选中杀掉。

第 4 步:排除「被攻击」的可能

代码语言:javascript
复制
# 查看故障时段的 nginx 访问日志
awk '$0 ~ 日期' /www/wwwlogs/站点.log | 统计来源 IP 和状态码

故障 20 分钟内全站只有 4 个请求、全部 200——没有流量攻击,纯粹是自己的内存问题。

三、根因分析:为什么偏偏是 MySQL?

OOM killer 按「谁占内存大杀谁」的原则选择目标。这台机器当时的内存构成是:

进程

内存占用

备注

主机 mysqld

664MB(被杀时 763MB)

innodb_buffer_pool_size = 512M

AI 编码工具 web 进程

272MB

Node 应用 ×2

88 + 75MB

nginx worker

73MB

文档系统

73MB

19 个 Docker 容器

数百 MB~1GB+

各自波动

整条因果链是:

代码语言:javascript
复制
配置总量 > 物理内存上限
  → 系统长期依赖 swap 撑着(已用 1.8G/1.9G)
  → 某次正常业务波动(查询/连接数上升)
  → RAM + swap 双满
  → 内核 OOM → 杀掉最大进程 mysqld
  → mysqld_safe 自动重启(全量初始化、刷脏页、刷缓存)
  → IO/负载风暴 → load 106 → 443 无响应 → 站点全挂

值得注意:站点挂掉不是 OOM 直接造成的,而是 MySQL 重启风暴的次生灾害。这也是为什么「重启恢复了」不等于问题解决——不加缓冲,下一次波动还会重演。

四、修复:先止血,再治本

4.1 止血:追加 2GB swap 作为缓冲垫

机器原有 2G swap 文件(几乎用尽),磁盘还有 20G+ 空闲,追加 swap 是零风险、立即生效的手段:

代码语言:javascript
复制
fallocate -l 2G /swapfile2
chmod 600 /swapfile2
mkswap /swapfile2
swapon /swapfile2

# 写入 fstab,重启后自动挂载
echo "/swapfile2 none swap sw 0 0" >> /etc/fstab

# 控制换出倾向(有 swap 后可以稍调低)
sysctl vm.swappiness=10

验证:

代码语言:javascript
复制
$ swapon --show
NAME         TYPE  SIZE USED PRIO
/swap.img    file  1.9G 1.8G   -2
/swapfile2   file  2.0G   0B   -3

$ free -m
Swap: 4035 total / 1875 used / 2160 free

swap 余量从 111MB 变成 2.1GB——下次内存波动有足够的腾挪空间,不会再直接触发 OOM kill。

补充一点:fallocate 创建的 swap 文件在多数主流文件系统(ext4/xfs)上可用;如果内核报 swapon: swapfile has holes,改用 dd if=/dev/zero of=/swapfile2 bs=1M count=2048 重新生成即可。

4.2 治本方向(三项,按性价比排序)

  1. 调小主机 MySQL 内存参数:innodb_buffer_pool_size 512M → 256M、max_connections 500 → 150(500 对小机器明显过大),可回收约 400MB。动手前先 SHOW DATABASES 确认主机 MySQL 真有业务在用——如果没有,甚至可以停掉它,让业务全走容器。
  2. 清理闲置容器:19 个容器逐一确认 docker ps 里谁还在被使用,停掉不用的,收益立竿见影。
  3. 限制单个进程的膨胀:给 MySQL 配 OOMScoreAdjust,或用 cgroup 给非核心服务设 MemoryMax,避免「谁大谁被杀」杀到业务核心。

4.3 防复发:一个 30 分钟一次的轻量看门狗

把「站点可用性 + 内存 + swap + 负载 + 关键容器」打包成一个纯脚本巡检(无外部依赖、正常时静默零输出):

代码语言:javascript
复制
#!/bin/bash
# vps_watchdog.sh — 正常时无输出;异常时输出一行告警
SSH="ssh -o ConnectTimeout=20 root@服务器IP"
ALERT=""

# ① 关键站点 HTTP
for u in https://站点A/ https://站点B/; do
  code=$(curl -s -o /dev/null -w '%{http_code}' -m 15 "$u")
  [ "$code" != "200" ] && ALERT="$ALERT 站点异常:$u→HTTP${code:-超时}"
done

# ② 内存 / swap / load
MEM=$($SSH 'free -m | awk "/^Mem:/{print \$7}"; free -m | awk "/^Swap:/{print \$4}"; uptime | grep -oE "[0-9.]+" | head -1')
AVAIL=$(echo "$MEM" | sed -n 1p)
SWAPFREE=$(echo "$MEM" | sed -n 2p)
LOAD=$(echo "$MEM" | sed -n 3p)
[ "${AVAIL:-9999}" -lt 250 ] && ALERT="$ALERT 内存告急:available=${AVAIL}MB"
[ "${SWAPFREE:-9999}" -lt 150 ] && ALERT="$ALERT swap将尽:剩${SWAPFREE}MB"
awk "BEGIN{exit !($LOAD > 8)}" && ALERT="$ALERT 负载过高:1m_load=${LOAD}"

# ③ 关键容器是否退出
DOWN=$($SSH 'docker ps -a --filter "status=exited" --format "{{.Names}}" | grep -E "mysql|nginx|app" | tr "\n" " "')
[ -n "$DOWN" ] && ALERT="$ALERT 容器退出:$DOWN"

[ -n "$ALERT" ] && echo "⚠️ 服务器告警:${ALERT# }"
exit 0

接到任意调度器上(每 30 分钟一次):正常时零输出、零打扰;异常时立刻收到一行可执行的告警,不用等用户来报「网站打不开了」。

五、经验总结

教训

对策

swap 用掉 80%+ 仍是「健康」状态,监控不会报警

把 swap 剩余量纳入监控阈值,它比内存更早示警

「机器 load 高」和「网站挂了」可能是同一件事的两个阶段

先查 journalctl -k 找 OOM/硬件事件,再查应用日志

OOM 杀掉的是最大进程,不一定是肇事者

主动限制核心服务的内存上限,别让「谁大谁死」

MySQL 自动重启本身会引发次生雪崩

看门狗同时盯 load 和站点,两者一起异常时优先怀疑重启风暴

一切「自愈」都可能复发

加 swap 缓冲 + 持久化 fstab + 周期巡检,三件套缺一不可

结语

小内存服务器跑全家桶,本质上是在做「配置总量」和「物理上限」的长期博弈。这次故障没有数据损失(业务数据库在独立容器里,毫发无伤),但它暴露的信号很明确:swap 见底就是 OOM 的倒计时。把 swap 余量放进监控、给核心进程上内存约束,比事后追查 OOM killer 日志要省心得多。


本文记录的是一次真实故障的排查与处置全过程,机器配置、日志时间线、内存数据均来自现场。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 背景:一台贴着天花板跑的小服务器
  • 一、故障时间线
  • 二、排查过程(四步定位)
    • 第 1 步:先判断是「机器挂了」还是「服务挂了」
    • 第 2 步:SSH 进去,一眼看到元凶
    • 第 3 步:内核日志给出确凿证据
    • 第 4 步:排除「被攻击」的可能
  • 三、根因分析:为什么偏偏是 MySQL?
  • 四、修复:先止血,再治本
    • 4.1 止血:追加 2GB swap 作为缓冲垫
    • 4.2 治本方向(三项,按性价比排序)
    • 4.3 防复发:一个 30 分钟一次的轻量看门狗
  • 五、经验总结
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档