一台 512MB 的 Ubuntu 云主机(跑 sing-box 代理)某天突然"整机失联":云控制台显示实例正在运行、静态 IP 没变,SSH、代理端口、云厂商的浏览器终端却全都不通,只有重启才恢复。一天之内,两台同配置的机器先后中招。根因不是网卡、不是运营商、也不是云厂商,而是无人值守自动更新(unattended-upgrades)把内存打满,全局 OOM 连带把负责网络配置的 systemd-networkd 拖进 Failed,默认路由与 DNS 随后消失——机器"活着但没网"。适用环境:Ubuntu 22.04 LTS(内核 6.8.0-1063-aws)、systemd-networkd、512MB 云主机,时间 2026-09-09 至 09-10。
一、症状与第一手数据 #
两台机在不到一天里先后病倒。本文主角(下称 A 机)于 2026-09-10 早晨 08:00(CST)前后失联;另一台同配置机器(下称 B 机)在约 19 小时前——09-09 06:18 UTC——以同样的链条先倒下,失联约 18 小时才被发现。
A 机失联时的观测数据:
- 云控制台:实例状态"正在运行",静态 IP 未变;实例历史无任何停止/回收动作 → 云侧没动过它。
- 外部探测:SSH(非标准端口)与代理入站端口全部 TCP 超时、无 SYN-ACK(不是拒绝),ICMP 不通。
- 浏览器终端:报
UPSTREAM_ERROR [515]——云厂商会话管理 agent(SSM)连不上元数据服务169.254.169.254,控制台通道本身也断了。 - 自建监控 agent 主动出站上报心跳,最后一次心跳 23:55:24 UTC。入站与出站同时中断 → 故障在整机/网络层,不在 sing-box 进程层。
- 对照:其余地域节点全部正常,云厂商状态页无对应事件 → 排除全局网络故障与区域事故。
排查期间还踩了个坑:本机经代理/TUN 测端口会"乐观握手"——本地应用立刻拿到 SYN-ACK,实际转发到上游才算成功,第一轮误报"已恢复"。改用两台异地干净观测点直连复测,才确认 09:13 控制台重启后 sing-box 09:13:52 自动拉起、监控 09:13:55 恢复上报,失联时长约 74 分钟。
重启后从前一次启动的 journal 里挖出完整的故障链日志:
# A 机(最后一段崩溃现场)
Sep 10 07:07:33 systemd-networkd[342]: ens5: Could not set route: Connection timed out
Sep 10 07:07:33 systemd-networkd[342]: ens5: Failed
Sep 10 07:08:13 kernel: oom-kill: ... global_oom, task_memcg=/system.slice/apt-daily-upgrade.service, task=unattended-upgr
Sep 10 07:08:13 kernel: Out of memory: Killed process 14467 (unattended-upgr) total-vm:469680kB, anon-rss:150328kB ... oom_score_adj:0
Sep 10 07:08:38 kernel: Out of memory: Killed process 14650 (unattended-upgr) anon-rss:163744kB
Sep 10 07:12:15 kernel: agent invoked oom-killer ... Out of memory: Killed process (unattended-upgr)
Sep 10 08:00:09 sing-box: ERROR router: missing default interface
Sep 10 08:00:23 agent: lookup <监控域名> on 127.0.0.53:53: server misbehaving
Sep 10 08:04:51 ssm-agent: 169.254.169.254 connect: network is unreachable
Sep 10 09:13:36 systemd[1]: systemd-networkd.service: Deactivated successfully. ← 重启# B 机(早 19 小时的同一套日志)
Sep 09 06:18:11 systemd-networkd[352]: eth0: Could not set route: Connection timed out
Sep 09 06:18:11 systemd-networkd[352]: eth0: Failed
Sep 09 06:18:11 kernel: Out of memory: Killed process 51149 (localedef) total-vm:127980kB, anon-rss:115752kB两机同源:都是 512MB 内存、Swap: 0、日常 apt-daily-upgrade(unattended-upgrades)触发全局 OOM。B 机先倒、无人察觉;A 机随后复现,才把问题翻出来。
| 时间(UTC) | 日志/事件 | 含义 |
|---|---|---|
| 06:52 | apt-daily-upgrade.timer 启动 |
每日升级开始了 |
| 07:07:33 | networkd Could not set route: Connection timed out → Failed |
内存耗尽、内核极端迟滞,设置 DHCP 路由超时,网络配置服务死亡 |
| 07:08:13 / 07:08:38 / 07:12:15 | Out of memory: Killed process (unattended-upgr) |
全局 OOM,三次杀掉升级进程;apt 被杀后又被拉起、再次被杀 |
| 08:00:09 | sing-box missing default interface |
networkd 死后无人续租约,默认路由消失 |
| 08:00:23 | 监控 agent DNS 查询失败 | DNS 失效 |
| 08:04:51 | SSM agent 连元数据服务失败 | 控制台终端 515 的直接原因 |
| 09:13 | 控制台重启 | sing-box 09:13:52 拉起、监控 09:13:55 恢复上报 |
二、根因 #
链条是这样走的:
- 内存被打满。512MB 实例、无 swap,日常占用已接近一半(journald 约 28MB、multipathd 约 27MB、sing-box 约 25MB、snapd 约 21MB,加 buff/cache 可用只剩约 200MB)。
unattended-upgrades升级时再吃 150MB+,内存耗尽。 - 负责网络的服务先被拖死。内存耗尽后内核迟滞,
systemd-networkd执行设置路由的操作超时,报Could not set route: Connection timed out后进入Failed状态——它一死,DHCP 租约不再续期。 - OOM killer 动手,但为时已晚。全局 OOM 触发,被杀的是
unattended-upgr(150MB)和localedef(115MB)这类升级进程;此时 networkd 已死,路由/DNS 只是时间问题。约 53 分钟后默认路由消失(A 机 08:00:09),DNS 随后失效。 - 机器"活着但没网"。内核还在跑、进程还在、云面板显示 running,但没有任何网络路径:出站不行、入站更不行,SSH、代理、控制台终端全部不可达。没有 panic、没有 oops、连 hung task 都没有——journal 里除了内核那几行 OOM 和 networkd 两行
Failed,再无痕迹。常规搜panic/error什么也扫不出来。
两个值得注意的点。其一,OOM killer 实际杀掉的是升级进程,networkd 是"超时自杀";但加固前它的 OOMScoreAdjust=0、Restart=on-failure,在全局 OOM 里一样是零保护的候选,这次没被挑中纯属运气——这决定了下面的加固方向。其二,这种故障不留明显崩溃痕迹,B 机失联 18 小时没有任何人发现,正是因为面板显示正常、日志"看起来很干净"。
三、处置过程(含走过的弯路) #
弯路一:本机测端口误报恢复。 本机经 TUN/代理测目标端口,本地应用立刻拿到 SYN-ACK,第一轮三个端口全部"OPEN";干净外部源直连才暴露真相:全部超时。结论必须以非本机的观测点为准。
弯路二:自动重启被误关又恢复。 处置中一度把 50unattended-upgrades 的 Automatic-Reboot 从 true 改成 false,理由"去掉多余的重启"。随后确认一个名实问题:机器每周日凌晨的重启不是 cron——手动重启 cron 早在几天前加固时已删除(末次触发 09-06);真正在做凌晨自动重启的是 unattended-upgrades 的 Automatic-Reboot-Time "02:00"(另一台机 08-20、08-22 两次 02:00 重启即它所致)。而站主平时不会定期登录机器,若关掉这个开关,装了内核更新也永不生效——最终改回 true 并以 apt-config dump 验证生效。这里想对读者说明取舍:关掉 Automatic-Reboot 的代价,就是内核安全更新要等下次人工重启才生效;无定期登录习惯的机器不建议关。
上机复核:另一套并行加固已生效。 出手前先复核现状,发现当天早些时候已经有另一批加固配置落在两台机器上(不是本次操作加的):apt 服务的内存 cgroup 上限(MemoryMax=250M / MemoryHigh=200M)、networkd 的 OOMScoreAdjust=-1000 + Restart=always、multipathd 停用、以及一个 net-watchdog 路由看门狗。逐项验证确实生效,且与本次新增的 swap 不冲突——fstab 里 /swapfile 只有一条、无重复挂载,findmnt --verify 通过。这套思路比"只加 swap"更对:卡住 OOM 源头(apt 内存升到 250M 就在自己的 cgroup 内被杀,整机不受影响),而不是等它打死网络栈再救。
本次补做的处置:
- 2GB swapfile:
fallocate -l 2G→mkswap→swapon,写进/etc/fstab(/swapfile none swap sw 0 0)。作用:吸收突发内存压力,让 OOM 尽量不发生。注意 swap 只是缓冲不是解药——OOM 一旦真的发生,netfilter 之外的任何进程都可能被随机挑中,网络栈仍可能中招。 - 清理纯代理机用不上的定时器:禁用
motd-news.timer、update-notifier-download.timer、update-notifier-motd.timer(每日拉资讯/弹更新通知,代理机上无用途,省网络与内存);保留apt-daily、apt-daily-upgrade(安全更新)、fstrim、e2scrub_all、logrotate、dpkg-db-backup、net-watchdog。顺手停用multipathd(无多路径存储,白占约 27MB)。 - 补上离线告警:自建监控的离线告警此前未开,B 机失联 18 小时无人知晓。开启后同类失联约 3 分钟(grace 180 秒)内推送到消息通道。
- 规格决策:站主否决升配(加内存每月约 +2 美元,对纯代理机不划算),以"swap + 内存 cgroup 上限 + OOM 保护 + 路由看门狗"组合替代。
看门狗逻辑(每 2 分钟,OnBootSec=3min、OnUnitActiveSec=2min、AccuracySec=15s):
#!/bin/bash
LOG=/var/log/net-watchdog.log
if ! ip -4 route show default | grep -q .; then
echo "$(date -Is) NO-DEFAULT-ROUTE -> restarting systemd-networkd" >> "$LOG"
systemctl restart systemd-networkd
sleep 10
if ip -4 route show default | grep -q .; then
echo "$(date -Is) recovered" >> "$LOG"
else
echo "$(date -Is) STILL-DOWN" >> "$LOG"
fi
fi它只治网络栈这一层,机器彻底卡死时它自己也跑不起来——所以它是兜底,不是主力;主力仍是掐住 OOM 源头。
四、验证 #
加固后用可观测指标逐项复核(两台机同批);另外注意 OOMScoreAdjust 对已运行的进程要下次启动才生效,核实取值即可,不必为此强行重启 networkd。
| 检查项 | 方法 | 结果 |
|---|---|---|
| 内存上限生效 | systemctl show apt-daily-upgrade.service -p MemoryMax |
262144000(250M)✅ |
| networkd 保护 | systemctl show systemd-networkd -p OOMScoreAdjust -p Restart |
-1000 / always ✅ |
| 看门狗运行且未误触发 | systemctl list-timers net-watchdog.timer;/var/log/net-watchdog.log |
enabled + active,每 2 分钟一次;日志为空 = 默认路由从未丢过 ✅ |
| swap 持久化 | swapon --show、grep swap /etc/fstab、findmnt --verify |
/swapfile 2G 生效,fstab 单条目无重复,校验通过;已在吸收压力(一台 8MB、一台 35MB 在用)✅ |
| 服务端口 | 异地干净源 TCP 探测 + openssl s_client REALITY 握手 |
全通;TLS1.3、Verify return code: 0 (ok) ✅ |
| 监控心跳 | 监控面板各节点最后上报 | 全部 5 分钟内(含故障过的两台)✅ |
| 自动重启配置 | apt-config dump | grep Automatic-Reboot |
true,与站主决策一致 ✅ |
| 保留的定时器 | systemctl list-timers |
apt-daily*/fstrim/logrotate/dpkg-db-backup/net-watchdog 全部 enabled ✅ |
实机后的观察:swap 建立后已开始承接压力(一台 8MB、一台 35MB),说明 512MB 上日常内存就是紧的。遗留的边界:apt 被限制在 250M 后,个别大升级可能因内存不足失败但机器没事——失败目前没有独立告警(监控只报在线/离线),需要的话可加一个"升级任务失败"探针。
五、留给后来者的清单 #
- 第一步永远是看内核日志里有没有 OOM:
journalctl -k --since "1 day ago" | grep -iE "Out of memory|Killed process"。整机"活着但没网"时,这是最快的定位动作。 - 面板 Running + 静态 IP 未变 + 入站出站同时断(监控 agent 的主动上报也停)= 先怀疑整机资源/网络栈层,别急着找运营商。
- 小内存机器给 apt 体系套内存 cgroup 上限:
apt-daily与apt-daily-upgrade各建 drop-in,MemoryMax=250M、MemoryHigh=200M。 - 给网络服务保命:
systemd-networkddrop-in 设OOMScoreAdjust=-1000、Restart=always、RestartSec=10s。 - 路由看门狗(上文的 shell 脚本)作为兜底;记住它只治网络栈,机器彻底卡死时无效。
- 加 swap 只解决"尽量不发生 OOM",不解决"OOM 发生时谁被挑中";两者定位不同,别混为一谈。
- 自动重启开关的取舍:无定期登录习惯就保留
Automatic-Reboot "true",否则内核更新永不生效。 - 本机经代理/TUN 测端口会乐观握手,端口存活判断一律用异地干净源。
- 这类故障不留崩溃痕迹,离线告警必须开——监控不告警等于没监控。
六、碎碎念 #
B 机先倒下 19 小时无人发现,A 机再倒才被重视——两台的差别只是先后,不是运气。回头想,“机器活着但没网"大概是运维里最容易被误判为运营商/云厂商问题的一类故障:面板一切正常,探测全部超时,谁都会先怀疑链路。实际上只要先花十秒钟看一眼内核日志里有没有 Out of memory,这整场排查就结束了。小内存机器上,自动更新这种"每天都在跑、从来不看结果"的任务,往往是隐藏的定时炸弹;给它套上 cgroup 上限、给网络服务戴上护具,再留一个看门狗兜底,这套组合拳对 512MB 级别的云主机都适用。