一次“网络故障”背后的进程风暴
一次“网络故障”背后的进程风暴
EMU 离线告警远程排障复盘:从跨境链路、DNS 到 dhclient 内存耗尽
事件时间:2026 年 7 月 22日
影响对象:现场 EMU 设备及其远程运维通道
初始现象:EMU 离线;Tailscale 极不稳定;SSH 会话近乎不可用;域名解析失败
最终根因:wifi-auto-fix.service 循环启动 dhclient mlan0,形成 2051 个进程并耗尽内存
恢复状态:异常进程清理后内存恢复,tailscaled 成功启动并重新接入控制平面
一句话结论: 这不是一次单纯的跨境网络、Tailscale 或 DNS 故障。现场自愈脚本不断为 mlan0 启动新的 dhclient,累计形成 2051 个进程,占用约 2.77 GiB 内存;内存耗尽后,DNS、SSH 和 Tailscale 相继表现异常。
1. 事件背景:最像网络问题的系统资源故障
事件由一条 EMU 离线告警开始。由于当时运维人员身在拉脱维亚,而目标设备位于远端现场,第一反应自然是跨境链路质量、境外网络限制或 Tailscale 中继路径出现异常。尤其是目标设备上的 Tailscale 连接抖动明显,远程访问体验越来越差,这个判断在当时看起来十分合理。
不过,将同一地点或同一账户下的其他设备作为对照后发现,它们的 Tailscale 连接其实很稳定。这个对照排除了“所有 Tailscale 连接都受跨境网络影响”的解释,也提示故障更可能集中在目标设备自身。随后目标设备的 Tailscale 几乎完全不可用,常规远程通道基本中断。
2. 为了进入设备,我们搭了一条临时的远程访问链
现场还有一台 Windows 电脑,于是尝试通过向日葵远程进入。但从国外直接连接现场向日葵并不可行,这与既有经验一致。最终采用了一个“云端跳板”方案:先远程登录自己的一台 Windows 云电脑,在云电脑上安装向日葵,再从云电脑连接现场 Windows 7 主机。
现场主机是 Windows 7,不能直接作为顺手的 SSH 终端使用。好在桌面上已有 MobaXterm,于是通过它连接 EMU。设备 IP 可以 ping 通,但 SSH 登录过程极慢,交互几乎卡死。由于 Windows 版本的向日葵连接足够稳定,便让会话保持在那里等待;过了一段时间,SSH 竟然成功进入。进入后,先停止了几个已知但当时并非必需的服务,为后续排查争取资源和稳定性。
3. 第一条线索:域名解析失败
进入系统后,最直观的异常是域名无法解析。无论访问 www.baidu.com 还是其他域名,系统都提示 Temporary failure in name resolution。由于 /etc/resolv.conf 指向 systemd-resolved 的本地桩地址 127.0.0.53,排障一度集中在 DNS 服务、上游 DNS 和 Tailscale DNS 接管上。
图 1 早期症状:域名解析失败
当时依次检查了 /etc/resolv.conf、systemd-resolved、上游 DNS 地址,并尝试关闭 Tailscale 的 DNS 接管。resolvectl query 曾长时间无响应,随后出现与 D-Bus 断开的错误。这进一步强化了“DNS 子系统异常”的判断。
但直接 ping 223.5.5.5 可以稳定收到响应,说明基础 IP 网络仍然可达。DNS 确实坏了,却仍不足以解释为什么 SSH 会话如此迟钝、Tailscale 守护进程也无法正常工作。事实证明,DNS 不是根因,而是系统资源耗尽后的连锁症状。
4. 转折点:启动 tailscaled 暴露了真正方向
为了恢复远程维护通道,尝试重新启动 tailscaled。systemd 没有给出普通的网络或认证错误,而是报告启动任务超时,并出现 Failed to fork: Cannot allocate memory。这个错误非常关键:问题不再是“数据包走不通”,而是系统已经没有足够资源创建新进程。
图 2 tailscaled 启动失败,systemd 报告无法分配内存
随后查看内存,设备总内存约 3.8 GiB,已使用约 3.6 GiB,可用内存仅 44 MiB,而且没有 Swap。此时 DNS 查询、SSH 交互和 tailscaled 启动失败便有了共同解释:系统处于严重内存压力之下。
5. 从“最大进程”切换到“按程序汇总”
最初查看单个进程的 RSS 时,没有看到一个占用数 GiB 的明显大户。真正有效的一步,是按进程名汇总 RSS 和实例数量。结果显示 dhclient 有 2051 个实例,合计占用约 2767.6 MiB,远高于其他任何程序。
图 3 按程序汇总后,dhclient 的异常数量和内存占用一目了然
ps -eo comm=,rss= | awk '{ rss[$1]+=$2; count[$1]++ } END {
for (name in rss) printf "%.1f MiB\t%d\t%s\n", rss[name]/1024, count[name], name
}' | sort -nr | head -30
进一步查看父进程后发现,2049 个 dhclient 的 PPID 都是 1,命令行一致为 dhclient mlan0。这说明它们已经成为孤儿进程:启动者退出了,但 DHCP 客户端仍然常驻。
6. cgroup 给出了最终答案
虽然孤儿进程的父进程已经变成 PID 1,但它们仍保留在原始 systemd cgroup 中。读取任一异常进程的 /proc/<PID>/cgroup,直接指向 /system.slice/wifi-auto-fix.service。再结合进程树,可以看到该服务由 /usr/local/bin/wifi-auto-fix.sh 启动,并持续执行 dhclient mlan0。
图 4 cgroup 和进程关系共同锁定 wifi-auto-fix.service
[table]
7. 故障链条:为什么看起来像 Tailscale 和 DNS
- wifi-auto-fix.sh 周期性或循环执行 dhclient mlan0。
- dhclient 后台化后,启动它的 shell 退出,进程被 PID 1 接管,但仍留在 wifi-auto-fix.service 的 cgroup 中。
- 数日内积累到 2051 个 dhclient,合计占用约 2.77 GiB 内存。
- 设备只剩约 44 MiB 可用内存且没有 Swap,systemd 无法可靠 fork 新进程。
- systemd-resolved 查询异常、D-Bus 交互中断、SSH 极度迟缓、tailscaled 无法启动。
- 由于运维人员位于拉脱维亚,症状首先被解释为跨境链路或 Tailscale 中继问题,延长了定位路径。
8. 处置与恢复
确认来源后,停止并禁用 wifi-auto-fix.service,并只清理与 mlan0 相关的 dhclient,避免误伤为 usb0 服务的 DHCP 客户端。异常进程清理完成后,内存立即恢复:已用内存降至约 436 MiB,空闲约 3.1 GiB,可用约 3.3 GiB。
图 5 清理异常进程后,可用内存恢复到约 3.3 GiB
资源恢复后,重置 tailscaled 的失败状态并重新启动服务。tailscaled 成功连接控制平面,设备保持原有授权,不需要重新登录。为避免在稳定性验证阶段再次引入变量,暂时关闭 Tailscale DNS 接管并清空 Exit Node 设置。
图 6 tailscaled 恢复为 active (running)
9. 关键指标:处置前后对比
[table]
10. 哪些判断有效,哪些假设误导了我们
有效的做法
- 使用其他设备进行横向对比,证明 Tailscale 整体服务和跨境链路并非普遍不稳定。
- 保留现场 Windows 主机、Windows 云电脑和向日葵作为替代进入路径,在主要运维通道失效时仍能到达设备。
- 没有停留在单个大进程视角,而是按命令名聚合 RSS 和进程数量。
- 在 PPID 已变成 1 的情况下继续检查 cgroup,从而准确追溯到 systemd 服务。
- 清理时针对 dhclient mlan0,而不是无差别杀死所有 dhclient,避免影响 usb0 网络。
容易误导的地方
- 地理位置带来的先验偏差:人在拉脱维亚,使跨境网络和 Tailscale 中继成为最显眼的解释。
- DNS 错误是真实存在的,但真实症状不等于根因;它是内存压力下的受害者之一。
- ping 通只证明基础 ICMP 可达,不能说明 SSH、DNS 或守护进程具备足够系统资源。
- 单进程排序看不到由数千个小进程共同造成的内存耗尽。
11. 永久修复建议
11.1 修正 wifi-auto-fix.sh
首选方案是让 NetworkManager 或 systemd-networkd 统一管理 mlan0,不再由自定义脚本直接、重复地调用 dhclient。如果确实必须保留脚本,则至少应做到:
- 启动前检查 mlan0 是否已经存在有效 DHCP 客户端或有效地址。
- 使用明确的 PID 文件或锁,保证同一接口只有一个 dhclient。
- 使用有限时、可回收的执行方式,避免后台化进程在循环中累积。
- 为每次重试设置退避时间和最大次数,并将失败交给 systemd,而不是无限循环。
- 服务停止时清理整个 cgroup 中的子进程。
11.2 为 systemd 服务增加护栏
[Service]
KillMode=control-group
Restart=on-failure
RestartSec=15s
TasksMax=32
MemoryMax=128M
TimeoutStopSec=10s
[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5
这些限制不能代替修复脚本,但可以阻止单个自愈服务再次拖垮整台设备。具体数值应根据现场环境和脚本正常开销调整。
11.3 增加监控和告警
- 监控 MemAvailable,而不仅是总内存使用率。
- 针对 dhclient、wifi-auto-fix、tailscaled 设置进程数量和服务状态告警。
- 将 pgrep -c dhclient 超过合理阈值视为异常;对 mlan0 通常不应存在多个长期常驻实例。
- 记录 DNS、SSH、Tailscale 异常发生时的内存、进程数和 OOM 日志。
11.4 建立可验证的远程访问冗余
本次能够完成恢复,很大程度上依赖现场 Windows 主机和 Windows 云电脑形成的临时跳板。后续应把这套能力从“临时想起来”变成经过验证的预案:保留至少两条独立远程通道,定期从境外网络验证可用性,并在现场主机预装可用的 SSH 工具。
12. 可复用的排障命令
当远程设备同时出现 DNS、SSH 和组网工具异常时,以下顺序比直接重装网络组件更有效。
# 1. 区分 DNS 与基础网络
ping -c 3 223.5.5.5
ping -c 3 www.baidu.com
\
2. 检查资源\
free -h
swapon --show
\
3. 按程序聚合 RSS 与实例数\
ps -eo comm=,rss= | awk '{rss[$1]+=$2; n[$1]++} END {
for (c in rss) printf "%.1f MiB\t%d\t%s\n", rss[c]/1024, n[c], c
}' | sort -nr | head -30
\
4. 查看异常进程父关系和 cgroup\
ps -C dhclient -o pid,ppid,stat,etime,args --sort=ppid
cat /proc/<PID>/cgroup
\
5. 查看服务与日志\
systemctl status <service> --no-pager -l
journalctl -u <service> -n 100 --no-pager
13. 结语
这次故障最有价值的地方,不是某条命令,而是排障方向的几次修正:从跨境网络到单机问题,从 DNS 到系统资源,再从“有没有大进程”到“是否存在大量小进程”,最后借助 cgroup 把孤儿进程追溯到具体服务。
对远程运维而言,链路异常与资源耗尽经常呈现相似表象。最可靠的办法是持续建立对照、验证假设,并尽早检查最基础的系统事实:内存、进程数量、服务状态和 cgroup 归属。网络工具可能是第一个倒下的服务,但不一定是制造故障的那个组件。
CodeWatt