一台 2G 小机器,两次"假死",我终于查清了元凶
摘要:一台轻量应用服务器,两次出现"能 ping 通、SSH 连不上、像死机一样",重启后恢复正常。最后发现,凶手不是 OOM,不是 sshd,而是我自己跑的两个 Spring Boot 服务——没给 JVM 设内存上限,硬生生把 1GB 的 Swap 吃到 900MB,整机被拖慢到连不进去。
一、开局:服务器"失联"了
那天下午,我像往常一样连服务器,突然发现:
ping通 ✅SSH连不上 ❌- 控制台 VNC 也卡得动不了 ❌
第一反应是:坏了,被攻击了?被挖矿了?还是内核 panic 了?
赶紧重启,机器起来,一切正常。我心想,大概是偶然吧。
结果过了一阵,又来一次,表现一模一样。
这下不能当偶然了。
二、第一直觉:是不是被 OOM 杀进程了?
搞 Linux 的都知道一句话:"连不上,先查 OOM。"
于是我连敲三条命令:
dmesg -T | grep -i "oom|killed process"
journalctl -k | grep -i "oom|killed process"
journalctl -b -1 | grep -i "oom|killed"全!都!没!有!输出!
我愣住了。没有 OOM,没有 killed,连一条内存紧张的痕迹都没有。
踩坑 1:别凭经验下结论。dmesg缓冲区会滚动覆盖,-b -1只能看上一次启动。你没查到,不代表没发生过。
三、第二个线索:有个服务被 "KILL" 了
journalctl -b -1 里倒是有一条奇怪的记录:
ydt.service: Main process exited, code=killed, status=9/KILLstatus=9/KILL,就是被 SIGKILL 强杀了。
我第一反应是:这是内核 OOM Killer 干的吧? 于是我继续往"内存"这条路上死磕。
后来又看到 systemctl status 里一行:
Memory: 38.0M (peak: 308.3M swap: 204.9M swap peak: 211.9M)我大喊一声:"找到了!Swap 用满了!"
然后我犯了一个低级错误:我根据这条 swap peak: 211.9M,断定这台机器 Swap 只有 211MB,还被吃满了。
四、真相:Swap 根本不是 211MB
后来我跑了这条命令:
free -h输出让我脸一红:
Mem: 1.9Gi used 1.4Gi free 79Mi available 544Mi
Swap: 1.0Gi used 900Mi free 124MiSwap 有 1GB,而且已经用了 900MB,只剩 124MB!
踩坑 2:systemctl status里的swap peak是"这个服务启动以来"的峰值,不是系统的 Swap 总量。我拿它当总量,直接把诊断带偏了。要看 Swap,老老实实free -h。
那个 211.9M,只是那次 systemctl 采样时服务用过的 Swap 峰值。真正的 Swap 窟窿,远比我想象得大。
五、最迷惑的一幕:到底谁启动的 Java?
我想给 JVM 加内存限制,于是去看进程:
ps -ef | grep java输出两个:
oadba 1057 ... java -jar ydt-server.jar
oadba 4128 ... java -jar pitlog.jar我一眼看到 ppid 都是 1,心想:
"父进程是 1 号,那就是 systemd 管的,两个都是!"
我又错了。
PPID=1 只能说明"它爹死了,被 init 收养了",不能证明是 systemd 亲手拉的。你用 nohup ./xxx.sh & 起的进程,退出终端后一样会变成 PPID=1。
正确的验证方式只有一个:
systemctl status ydt.service | grep "Main PID"
systemctl status pitlog.service | grep "Main PID"结果啪啪打脸:
| 服务 | systemd 认不认 |
|---|---|
ydt-server PID 1057 | ✅ Main PID: 1057,systemd 亲生的 |
pitlog PID 4128 | ❌ 服务根本不存在,脚本裸奔 |
踩坑 3:PPID=1≠ systemd 管理。要验证,就看systemctl status显示的Main PID和ps的 PID 对不对得上。
六、元凶现身:吃内存的不是我想的那个
再看一眼那两个 Java:
pitlog 4128 oadba 27.0% 544MB java -jar pitlog.jar
ydt-server 1057 oadba 10.7% 215MB java -jar ydt-server.jarpitlog 占了 544MB,是 ydt 的 2.5 倍!
而且它:
- 没 systemd 服务 ❌
- 没
-Xmx❌ - 崩了没人拉 ❌
- 退出终端可能就没了 ❌
我之前把火力全集中在 ydt 上,方向偏了一半。真正的"内存大户"是那个我以为"只是个小日志程序"的 pitlog。
七、完整因果链,终于串起来了
两个 Java 都没设 -Xmx
↓
内存慢慢涨,堆外 + 堆一起往上堆
↓
触发 Swap,1GB 被吃到 900MB
↓
频繁换页,磁盘 I/O 被打满
↓
所有进程(包括 sshd)都卡在 I/O 上
↓
你这边:ping 通,但 SSH 握手完不成 → "连不上"
↓
重启 → Swap 清空 → 一切恢复 → 过几天又来这解释了所有症状:
- ✅ 为什么 ping 通但连不上(sshd 没死,只是慢)
- ✅ 为什么重启就好(Swap 清空了)
- ✅ 为什么是"间歇性复发"(内存是慢慢涨上来的)
- ✅ 为什么没 OOM 记录(不是被杀,是慢死)
踩坑 4:Swap 是"慢性毒药"。它不让程序干脆崩掉,而是把整机拖慢到"连运维都登不进去",比直接 OOM 更难排查。
八、解法:四步止血
1️⃣ 两个 Java 都交给 systemd,并设死内存
关键是算账。我这 1.9GB 内存,扣掉系统预留和堆外内存,两个 Java 的 -Xmx 合计不能超过约 512MB:
# ydt.service / pitlog.service 的 drop-in
[Service]
Environment="JAVA_OPTS=-Xms128m -Xmx256m -XX:+UseG1GC -XX:MaxMetaspaceSize=96m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/springboot/heapdump"两个都是 -Xmx256m,加起来正好 512m。
踩坑 5:别给 -Xmx512m 就完事。这台机器两个 Java,你各给 512m,堆合计 1GB,再加堆外内存直接超卖,Swap 照样打满。多服务同机,一定要"合计"着算。2️⃣ 新建服务后,记住这条铁律
systemctl daemon-reload # 让 systemd 重读配置(全量,不会重启任何服务)
systemctl restart xxx # 让新配置对运行中的服务生效踩坑 6:daemon-reload是"全量"的,不能指定单个 service,但它不会重启任何服务,大胆用。restart才是"只针对那一个服务生效"的动作。很多人改完只daemon-reload,然后纳闷参数为啥没变。
3️⃣ 关掉 Swap
swapoff -a && sed -i '/swap/s/^/#/' /etc/fstab注意:别在内存紧张时直接关。 先把 Java 压到 256m、腾出余量,再 swapoff -a,否则它会把 900MB 数据往内存倒,可能当场 OOM。
4️⃣ 顺手挡 SSH 爆破
journalctl -u sshd --since "7 days ago" | grep -c "Failed password"一跑,几千条。难怪连接槽老满。装个 fail2ban,5 分钟搞定。
九、这次排查,我学到的 6 条教训
| # | 教训 |
|---|---|
| 1 | dmesg 无输出 ≠ 没发生过 OOM,缓冲区早被冲掉了 |
| 2 | 看 Swap 用 free -h,别信 systemctl status 里的 swap peak |
| 3 | PPID=1 ≠ systemd 管理,看 Main PID 对不对得上 |
| 4 | Swap 让机器"慢慢死",比 OOM 更难查,小内存机器建议关 |
| 5 | 多 Java 同机,内存要"合计算账",别各写各的 |
| 6 | daemon-reload 是全量重读;要生效还得 restart |
十、一句话总结
这次"连不上"的元凶,不是黑客,不是 OOM,不是 sshd,是我自己那两个没戴"内存紧箍咒"的 Spring Boot。给 JVM 设个 -Xmx,把服务交给 systemd,关掉 Swap——三次复发,一刀切断。