加载中...
DNS 故障引发 Passenger 进程拥塞事件分析
发表于:2026-10-21 | 分类: resultweb
字数统计: 1.9k | 阅读时长: 6分钟 | 阅读量:

DNS 故障引发 Passenger 进程拥塞事件分析

一、事件概述

一次手动重启服务器后,前端服务进程(Passenger)数量持续增长并超过配置上限(80),即使重新部署、重启服务也只能短暂缓解,几分钟后又恢复拥塞。内存充足(无 OOM 风险),但服务严重变慢、请求大量排队。最终定位为一台 DNS 服务器失效引发的连锁拥塞。

二、现象

  • Passenger 进程涨到 80(触顶 MaxPoolSize),请求队列持续累积(16 → 52 → 65 → 100)
  • 重启 httpd 无效,几分钟后再次拥塞
  • 临时把 MaxPoolSize 调到 120 也只好了一会
  • 内存从 10GB 一路涨到 32GB(120 worker 的代价),但始终未触发 OOM
  • 系统负载不高(24 核,load ~10)

三、排查过程与根因

逐层排查(排除了多个方向)

  1. 整机刚重启(up 6min)—— 怀疑启动抖动,但等待不自愈
  2. DNS 配置正常(nslookup 能解析)—— 一度排除 DNS
  3. NFS 挂载正常 —— 排除存储
  4. Passenger core 稳定、未反复重启 —— 排除进程崩溃
  5. Kafka 连通正常 —— 排除消息队列阻塞

定位到慢请求

从应用日志发现:TerminalsController#show 每个请求耗时 35 秒,且 Views: 35160ms / ActiveRecord: 91ms——慢在视图渲染,不是数据库。

定位到具体代码

该视图渲染时调用了两个方法,内部执行外部命令:

# 查询远程主机 DNS
Open3.capture2e("getent hosts #{hostname} | awk '{ print $1 }'")
# ping PDU 设备
Open3.capture3("ping #{host} -c 1")

这些 getent / ping 调用没有超时保护。

最终根因:DNS 服务器失效

测试两个 DNS 服务器响应:

nameserver A (排在第一) → connection timed out (15秒)
nameserver B (排在第二) → 0.017 秒正常

/etc/resolv.conf 里排第一的 DNS 服务器失效了。DNS 解析器总是先试第一个,超时 15 秒后才切到第二个。一个页面要解析多个主机名,累加起来正好 35 秒。

四、完整因果链

手动重启后,第一个 DNS 服务器失效(但仍列在 resolv.conf 首位)
        ↓
每个 getent/ping 调用先卡 15 秒超时才切到备用 DNS
        ↓
含这些调用的视图渲染耗时飙到 35 秒
        ↓
worker 被单个慢请求长时间占住
        ↓
请求积压 → Passenger 拼命扩容到上限 80/120
        ↓
进程数"持续增长超过上限" + 队列堆积(表象)

关键认知:进程失控是”表象”,真因是每个请求卡 35 秒。 加 worker(80→120)无效,因为慢请求产生速度 ≥ 处理速度,多少 worker 都会被填满。

五、为什么”以前没事,这次才出问题”

这是排查中的重要一问。关键在于区分常量和变量:

  • 常量(一直存在):密集的 terminal 状态轮询、代码里无超时的 getent/ping —— 平时设备/DNS 正常时毫秒返回,不出问题。
  • 变量(这次才变):手动重启后第一个 DNS 服务器失效。

所以真因是 DNS 可达性变化,不是轮询量,也不是任何近期的配置改动。无超时的代码是”易燃物”(一直在),DNS 失效是”火种”(这次才有)。

六、处置

立即止血

从 /etc/resolv.conf 删除失效的 DNS 服务器(注释掉),只保留可用的。解析从 15 秒降到 0.017 秒,慢请求从 35 秒降到约 45 毫秒。

注意:该文件由 DHCP 自动生成,持久化需在网卡配置里固定 DNS(PEERDNS=no + DNS1=可用服务器),否则重启会被覆盖。

回退过度配置

DNS 修复后,把临时调高的 Passenger 配置(120/120)调回合理值(80/20),释放内存压力——120 worker 把内存吃到只剩 1GB,已逼近危险线。

根治建议(代码层)

给视图里的 ping/getent 加超时:

Open3.capture3("ping #{host} -c 1 -W 2")           # 2秒超时
Open3.capture2e("timeout 2 getent hosts #{host} ...")

这样即使某个 DNS/设备不可达,单请求最多卡 2 秒而非 35 秒,防止”个别依赖故障”放大成”整个服务瘫痪”。

基础设施

失效的 DNS 服务器属于基础设施,应通知相关团队修复,resolv.conf 改动只是绕过。

七、关键经验

  1. 进程数失控常是表象,要找”为什么每个请求慢”。 盲目加 worker 对”慢请求拥塞”无效,反而加重内存压力。

  2. 区分常量与变量定位根因。 “以前好好的为什么突然出问题”——答案一定在”这次变了什么”(DNS 失效),而不在”一直存在的东西”(轮询、无超时代码)。

  3. 同步的外部命令调用(ping/getent/ssh)必须加超时。 没有超时保护时,一个外部依赖的故障会被放大成整个服务的雪崩。这是防御性编程的基本功。

  4. resolv.conf 里 DNS 的顺序很重要。 解析器按顺序尝试,排第一的失效会拖慢每一次解析,直到超时才切换。


八、通俗版复述

发生了什么

重启了一次服务器之后,网站变得巨慢,处理请求的”工人”(进程)越来越多、停不下来,甚至超过了设定的上限。重启服务也没用,过几分钟又卡。但奇怪的是,内存够、CPU 也不忙,就是慢。

为什么会这样

真正的凶手是:一台”电话查号台”坏了。

服务器每处理一个”查看终端状态”的网页请求,都要打几次电话查号(DNS 解析,把机器名字翻译成 IP 地址;还有 ping 一下设备看通不通)。平时查号台秒接秒回,没人察觉。

但重启后,配置里排第一的那个查号台坏了没人接。而规矩是”必须先打第一个,打不通等 15 秒再打第二个”。于是每次查号都要先白等 15 秒。一个网页要查好几次,加起来就是 35 秒才能出一个页面。

结果:每个”工人”接一个活就被卡住 35 秒动不了,活却source源不断地来。工人全被占死,系统只好拼命加人(进程暴涨),但加多少都没用——因为活堆积的速度比干完的速度还快。这就是为什么”加到 120 个工人还是卡”。

怎么修好的

  1. 把坏掉的查号台从名单里划掉。 只留能用的那个。查号立刻从 15 秒变成几乎瞬间,网页从 35 秒变回 0.05 秒,拥塞马上缓解。

  2. 把临时加的工人裁回去。 之前慌乱中加到 120 个,把内存快撑爆了,DNS 修好后赶紧调回正常的 80 个待命 20 个。

  3. (建议)给”打电话查号”设个闹钟。 以后不管哪个查号台坏了,最多等 2 秒就挂断换下一个,而不是傻等 15 秒。这样一个小毛病不会再拖垮整个系统。

得到的教训

  • “工人暴涨”只是表面现象,根子是”每个活被卡住了”。 光加人没用,得找为什么慢。

  • 排查”以前好好的为什么突然坏了”,要盯住”这次到底变了什么”。 一直存在的东西(查号这个动作)不是原因,这次新发生的(查号台坏了)才是。

  • 凡是要”问外面”的操作(查号、ping、远程连接),都得设个等待上限。 不然外面一个小毛病,就能把自己拖死。

上一篇:
ResultWeb 邮件服务迁移到 SMTPS
下一篇:
浏览器时间显示超前问题排查报告
本文目录
本文目录