2024 年,一个服务调用一个外部 HTTP 接口,时快时慢。大多数时候几十毫秒就回来了,可隔三差五,某一次调用会卡上整整 5 秒——不多不少,就是 5 秒——然后才正常返回。我第一反应是对方接口慢,可我去问对方,对方说他们的接口从监控上看一直是几十毫秒,从没慢过。我又怀疑是网络抖动,可 ping 那个域名,延迟稳定得很,也没丢包。我把客户端的耗时打点拆细了看,发现卡的那 5 秒,根本不在"发请求到收响应"这一段——它卡在更早的地方,卡在请求都还没发出去的时候。我盯着这个"5 秒整"想了很久,这个数字太规整了,规整得不像是网络抖动那种随机的慢,它像是某个"超时时间"到点了。最后我才反应过来:我一直在排查"请求"本身,可一次 HTTP 调用,在真正发出请求之前,还有一件事必须先做完——把域名翻译成 IP。这个"翻译"的过程,就是 DNS 解析,而它,才是卡住我整整 5 秒的真凶。这件事逼着我把 Linux 的 DNS 解析链路、resolv.conf、nsswitch、search 域这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,一个服务,调用外部接口 api.example.com
事故现象:
- 调用外部接口大多数几十毫秒,偶尔某次卡【5 秒整】
- ★ 对方接口监控一直正常;ping 域名延迟稳定不丢包
- 客户端打点细看:卡的 5 秒不在收发响应,在【请求发出前】
现场排查:
# 1. 手动解析这个域名,多跑几次
$ for i in $(seq 5); do time getent hosts api.example.com; done
... real 0m0.003s
... real 0m0.004s
... real 0m5.012s # ★ 来了!某一次 5 秒整
... real 0m0.003s
# ★ "5 秒整" 这个数字太规整 —— 是某个超时到点了
# 2. ★ 看 DNS 配置
$ cat /etc/resolv.conf
nameserver 10.0.0.211 # ★ 第一个 DNS
nameserver 8.8.8.8 # 第二个 DNS
search prod.local svc.local # ★ search 域
# 3. ★ 探一下第一个 DNS 通不通
$ dig @10.0.0.211 api.example.com +time=2 +tries=1
;; connection timed out; no servers could be reached # ★ 第一个 DNS 死了!
# 4. 第二个 DNS 是好的
$ dig @8.8.8.8 api.example.com +short
93.184.216.34 # ★ 第二个 DNS 正常
根因(后来想清楚的):
1. ★ 一次 HTTP 调用,在发请求【之前】,必须先把域名
解析成 IP。这次卡的 5 秒,全卡在【DNS 解析】上。
2. resolv.conf 里第一个 nameserver 10.0.0.211 已经
【挂了】,但配置没人清理,它还排在第一位。
3. ★ 系统解析时【先问第一个】DNS。它问 10.0.0.211,
对方没任何响应 —— 于是它【死等】,直到 resolv.conf
的 timeout(默认 5 秒)到点,才放弃。
4. timeout 到了,它才【换第二个】nameserver 8.8.8.8,
这个是好的,立刻就解析出来了。
5. ★ 所以那次 5 秒 = 给死掉的第一个 DNS 白等的 5 秒。
不是每次都卡,是因为系统/库会缓存解析结果,
只有缓存失效、需要重新解析的那次,才会撞上。
DNS 解析是 HTTP 调用的隐藏前置步骤,它慢,整个调用就慢。
修复 1:"5 秒整"是 DNS 的指纹——先认出它
# === ★ 先学会:从"5 秒整"这个症状,认出是 DNS ===
# === 为什么"整 5 秒"这么可疑 ===
# 网络抖动造成的慢,是【随机】的:这次慢 1.2 秒,
# 下次慢 3.8 秒,没规律。
# ★ 而 DNS 卡的慢,常常是【非常规整的整数秒】:
# 5 秒、10 秒、15 秒…… 因为它卡的是一个
# 【固定的超时时间】,到点才放弃。
# 看到"卡的时间总是某个固定整数秒",第一个该
# 怀疑的,就是 DNS。
# === ★ 5 秒、10 秒这些数从哪来 ===
# resolv.conf 有两个关键参数(不写就是默认值):
# - timeout :问一个 DNS 服务器,最多等几秒。默认 5。
# - attempts:整个 nameserver 列表,重试几轮。默认 2。
# 于是:
# - 1 个死 DNS,1 轮 -> 卡 5 秒
# - 1 个死 DNS,2 轮重试 -> 卡 10 秒
# - 2 个死 DNS -> 卡更久
# ★ 你看到的"整数秒",基本都能被 timeout×次数 算出来。
# === 第一步:把"是不是 DNS"这件事坐实 ===
# 直接给【IP】发请求,绕开域名解析:
$ curl -w 'dns:%{time_namelookup} total:%{time_total}\n' \
-o /dev/null -s https://api.example.com/
dns:5.011 total:5.043 # ★ time_namelookup 占了 5 秒!
# ★ curl 的 -w 能把【DNS 解析耗时】单独打出来 ——
# time_namelookup 大,就实锤是 DNS 慢,与接口无关。
# === 第二步:反复解析,复现那次卡顿 ===
$ for i in $(seq 20); do
/usr/bin/time -f '%e' getent hosts api.example.com 2>&1 | tail -1
done
# ★ 跑 20 次,只要有一两次蹦出 5.0x,就确认无疑。
# === ★ 认知 ===
# 一次 HTTP 调用 = DNS 解析 + 建连 + 收发数据。
# 排查"调用慢",必须先把【DNS 这一段】单独拎出来量。
# 它是最容易被忽略、又最容易整段卡死的一环。
修复 2:DNS 解析这条链路,到底怎么走的
# === ★ 把"域名 -> IP"这条链路彻底走一遍 ===
# === 第一站:/etc/nsswitch.conf 决定"先查谁" ===
$ grep '^hosts' /etc/nsswitch.conf
hosts: files dns
# ★ 这行规定了主机名解析的【顺序】:
# - files:先查本地文件 /etc/hosts
# - dns :本地没有,再走 DNS 查询
# 顺序是从左到右,谁先命中就用谁。
# === 第二站:/etc/hosts —— 本地的"通讯录" ===
$ cat /etc/hosts
127.0.0.1 localhost
10.0.0.50 api.internal
# ★ 如果要解析的域名在这里有记录,【直接返回】,
# 根本不会去问 DNS —— 一行命中,零延迟。
# 这也是个排查/应急手段:把域名写死在 hosts 里,
# 能瞬间绕开有问题的 DNS。
# === 第三站:/etc/resolv.conf —— DNS 服务器在哪 ===
$ cat /etc/resolv.conf
nameserver 10.0.0.211
nameserver 8.8.8.8
search prod.local
options timeout:2 attempts:2
# ★ hosts 没命中,系统就按这里的 nameserver 去问 DNS。
# === ★ nameserver 是怎么被使用的(关键)===
# 系统【严格从上到下】用 nameserver:
# 1. 先问第一个 nameserver。
# 2. 它在 timeout 秒内回了 -> 用结果,结束。
# 3. ★ 它【超时没回】-> 才换下一个 nameserver。
# 注意:不是"几个一起问、谁快用谁",是【串行】的。
# ★ 所以第一个 nameserver 一旦挂了,每次解析都得
# 先为它白白等满一个 timeout —— 这就是我的 5 秒。
# === 完整链路一图 ===
# 程序要连 api.example.com
# -> nsswitch: 先 files 再 dns
# -> 查 /etc/hosts,没有
# -> 读 /etc/resolv.conf,问 nameserver 1
# -> nameserver 1 死了,等满 timeout
# -> 问 nameserver 2,拿到 IP
# -> 程序拿着 IP 去建连
# === 验证整条链路 ===
$ getent hosts api.example.com
# ★ getent hosts 走的就是这条【完整链路】(hosts+dns),
# 程序实际用的也是它 —— 比直接 dig 更贴近真相。
修复 3:resolv.conf 里的坑——顺序、timeout、死 DNS
# === ★ resolv.conf 配不好,就是慢解析的温床 ===
# === 坑 1:把不可靠的 DNS 排在第一位 ===
# 系统串行地、从上往下问 nameserver。
# ★ 第一个 nameserver 就该是【最快、最稳】的那个。
# 我这次的错:第一个是早就下线的内网 DNS 10.0.0.211,
# 没人清理,于是每次解析都先撞它、白等 5 秒。
$ cat /etc/resolv.conf
nameserver 10.0.0.211 # ★ 它已经死了,却排第一
nameserver 8.8.8.8
# === ★ 坑 2:nameserver 配太多也没用 ===
# resolv.conf 里的 nameserver,系统【最多只用前 3 个】。
# 写第 4、第 5 个,基本是摆设。
# 而且越往后的,只有在前面全超时后才轮得到 ——
# ★ 与其堆一串备胎,不如保证前 1~2 个是好的。
# === 坑 3:timeout / attempts 没调,白等很久 ===
$ cat /etc/resolv.conf
options timeout:2 attempts:1
# ★ timeout:2 —— 问一个 DNS 最多等 2 秒(默认 5,太长)
# ★ attempts:1 —— 整个列表只重试 1 轮(默认 2)
# 把这两个调小:就算真撞上一个死 DNS,
# 最坏也就卡 2 秒,而不是 5 秒、10 秒。
# ★ 这是【缓解】—— 根治还是得把死 DNS 清掉。
# === ★ 坑 4:resolv.conf 被自动改回去 ===
# 你手动改好了 resolv.conf,重启网络/重启机器后
# 发现又变回原样 —— 因为它被【自动管理】了:
# - NetworkManager 会根据网卡配置重写它
# - DHCP 获取地址时,会用 DHCP 下发的 DNS 覆盖它
# - 云服务器的 cloud-init 也可能改它
# ★ 直接 vi 改,常常【改了白改】。要从源头改:
$ vi /etc/sysconfig/network-scripts/ifcfg-eth0
DNS1=8.8.8.8 # ★ 在网卡配置里设 DNS
DNS2=114.114.114.114
PEERDNS=no # ★ 不让 DHCP 覆盖 DNS
$ systemctl restart network
# 这样改,resolv.conf 才会被【稳定地】生成成你要的样子。
# === 探测每个 nameserver 是死是活 ===
$ for ns in $(awk '/^nameserver/{print $2}' /etc/resolv.conf); do
echo -n "$ns: "
dig @$ns api.example.com +time=2 +tries=1 +short || echo DEAD
done
# ★ 这一段:逐个探测 resolv.conf 里的 DNS,
# 哪个 DEAD 一目了然 —— 死的就该从配置里删掉。
修复 4:search 域和 ndots——一次解析变成了好几次查询
# === ★ 另一类隐形慢:search 域让查询次数翻了好几倍 ===
# === search 域是干什么的 ===
$ grep search /etc/resolv.conf
search prod.local svc.local
# search 的本意:让你能用【短名字】。比如你敲 web,
# 系统会自动帮你补成 web.prod.local 去查。
# === ★ 坑:它会给"完整域名"也乱加后缀 ===
# 你要解析的是 api.example.com,一个完整域名。
# 但系统可能【先这样试】:
# api.example.com.prod.local <- 查,失败
# api.example.com.svc.local <- 查,失败
# api.example.com <- 查,成功
# ★ 一次解析,硬生生变成了 3 次 DNS 查询!
# 前两次注定失败的查询,每次都要走一遍网络、
# 可能每次都撞那个慢 DNS —— 慢就被放大了好几倍。
# === ★ 决定"加不加后缀"的,是 ndots ===
$ cat /etc/resolv.conf
options ndots:5
# ndots:n 的意思:你要查的名字里,如果【点的个数】
# < n,系统就认为它是个"短名",【先去试 search 后缀】;
# >= n,才直接当完整域名查。
# ★ 默认 ndots:1。但有些环境(尤其 K8s)会设成 5 ——
# api.example.com 只有 2 个点 < 5,于是它被当成短名,
# 先去拼 search 后缀,白查好几次。
# === ★ 解法 1:查询时给域名加个"."结尾 ===
$ getent hosts api.example.com.
# ^ ★ 末尾这个点
# 结尾带点 = 告诉系统"这是【绝对域名】,别加任何后缀",
# 直接查,一次到位。
# === 解法 2:把 ndots 调回小值 ===
$ vi /etc/resolv.conf
options ndots:1
# ★ ndots:1 -> 只要域名里有 1 个点,就直接当完整域名查,
# 不再拼 search 后缀。对"大多用完整域名"的场景最合适。
# === 解法 3:用不到 search 就删掉它 ===
# 如果你的服务都用完整域名,search 这行纯属添乱,
# 直接删了,省掉所有多余查询。
# === 看一次解析到底查了几次 ===
$ tcpdump -i any -n port 53 &
$ getent hosts api.example.com
# ★ 抓 53 端口(DNS),数一下飞出去几个查询包 ——
# 超过 1 个,就是 search 域在放大查询。
修复 5:正确解法——清死 DNS、调参数、上本地缓存
# === ★ 解法分三层:清理、调优、加缓存 ===
# === ★ 第一层(根治):清掉死掉的 nameserver ===
# 我这次的根因,就是一个死 DNS 排在第一位。
# 找到它、删掉它,5 秒卡顿当场消失。
$ vi /etc/sysconfig/network-scripts/ifcfg-eth0
DNS1=10.0.0.5 # ★ 换成确认活着、且快的 DNS
DNS2=114.114.114.114
PEERDNS=no
$ systemctl restart network
$ cat /etc/resolv.conf # 确认生成的结果是对的
# === 第二层(调优):收紧 timeout / attempts ===
$ vi /etc/resolv.conf # 或在网卡配置里通过 RES_OPTIONS
options timeout:2 attempts:2 ndots:1
# ★ 就算将来又有 DNS 出问题,最坏卡 2 秒,不是 5 秒。
# === ★ 第三层(提速):上一个本地 DNS 缓存 ===
# 每次解析都走网络问远端 DNS,既慢又脆弱。
# 在本机跑一个【DNS 缓存服务】,解析过的结果缓存下来,
# 下次直接本地命中 —— 又快又能扛远端 DNS 抖动。
# CentOS 7 上可以用 nscd,或更现代的 systemd-resolved /
# dnsmasq。以 nscd 为例:
$ yum install -y nscd
$ systemctl enable --now nscd
# ★ 之后高频解析的域名,基本都走本地缓存,
# 远端 DNS 偶尔抽风,也不会每次都拖累业务。
# === ★ 最稳的兜底:关键域名写进 /etc/hosts ===
# 对【固定不变】的关键依赖(比如内网核心服务),
# 直接在 /etc/hosts 里写死 IP:
$ cat /etc/hosts
10.0.0.50 api.internal
# ★ hosts 命中就【完全不走 DNS】—— 零延迟、零依赖。
# 注意:只适合 IP 基本不变的目标,变了得手动改。
# === 验证修完了 ===
$ for i in $(seq 30); do
curl -w '%{time_namelookup}\n' -o /dev/null -s https://api.example.com/
done | sort -rn | head -3
# ★ 解析耗时最大的几次,都该是毫秒级,
# 再也不该出现 5.0x —— 那就修干净了。
修复 6:DNS 解析排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ 调用慢,先把 DNS 解析这一段单独量出来 ===
$ curl -w 'dns:%{time_namelookup} total:%{time_total}\n' -o /dev/null -s URL
# time_namelookup 大 = DNS 的锅,和接口无关。
# === 2. ★ 卡顿是"规整的整数秒",优先怀疑 DNS ===
# 5 秒、10 秒…… 是 timeout 到点,不是网络随机抖动。
# === 3. 用 getent hosts 验证,它走完整解析链路 ===
$ getent hosts 域名 # 比 dig 更贴近程序的真实行为
# === 4. ★ 逐个探测 resolv.conf 里的 nameserver 死活 ===
$ dig @每个nameserver 域名 +time=2 +tries=1
# 死掉的 DNS 必须从配置里清掉,别让它排在前面。
# === 5. resolv.conf 第一个 DNS 必须最快最稳,最多用前 3 个 ===
# === 6. ★ search 域 + 大 ndots 会放大查询次数 ===
# 用完整域名时,末尾加"."或把 ndots 调小,避免空查。
# === 7. 改 resolv.conf 要从源头改(网卡配置),否则被重写 ===
$ vi ifcfg-eth0 设 DNS1/DNS2,PEERDNS=no
# === 8. 排查"DNS 解析慢"的步骤链 ===
$ curl -w '%{time_namelookup}' # ① 确认是不是 DNS 慢
$ cat /etc/resolv.conf # ② 看 DNS 配置
$ dig @每个ns 域名 +time=2 +tries=1 # ③ 找出死掉的 DNS
$ tcpdump port 53 # ④ 看一次解析查了几次
$ 清死DNS + 调timeout + 上缓存 # ⑤ 根治 + 调优 + 提速
# 按这个顺序,DNS 慢基本能定位、能根治。
命令速查
需求 命令
=============================================================
拆出调用里的 DNS 解析耗时 curl -w '%{time_namelookup}' -o /dev/null -s URL
走完整链路解析一个域名 getent hosts 域名
看 DNS 服务器配置 cat /etc/resolv.conf
看解析顺序(files/dns) grep '^hosts' /etc/nsswitch.conf
探测某个 DNS 死活 dig @1.2.3.4 域名 +time=2 +tries=1
看一次解析查了几次 tcpdump -i any -n port 53
绝对域名查询(不加后缀) getent hosts 域名. (末尾带点)
从源头配 DNS 改 ifcfg-eth0 的 DNS1/DNS2,PEERDNS=no
装本地 DNS 缓存 yum install -y nscd; systemctl enable --now nscd
关键域名写死 编辑 /etc/hosts 加一行 IP 域名
口诀:调用慢先拆 DNS 耗时,卡整数秒优先怀疑 DNS
死 DNS 排第一就每次白等 timeout,清死 DNS、调小 timeout、上本地缓存
避坑清单
- 一次 HTTP 调用在发请求前必须先做 DNS 解析,这一段慢整个调用就慢,易被忽略
- 卡顿时间总是规整的整数秒,多半是 DNS 超时到点,而非网络随机抖动
- 系统串行从上到下用 nameserver,第一个超时才换下一个,死 DNS 排第一最致命
- resolv.conf 最多只用前 3 个 nameserver,与其堆备胎不如保证前一两个又快又稳
- timeout 默认 5 秒 attempts 默认 2 轮,调小可缓解,但根治还得清掉死 DNS
- 直接 vi 改 resolv.conf 常被 NetworkManager/DHCP 重写,要从网卡配置源头改
- 解析顺序由 nsswitch.conf 的 hosts 行决定,files 在前则 /etc/hosts 命中就不走 DNS
- search 域加大 ndots 会把一次解析放大成多次查询,完整域名末尾加点可直查
- 用 getent hosts 验证解析,它走 hosts+dns 完整链路,比 dig 更贴近程序真实行为
- 本地装 nscd 等 DNS 缓存能提速并扛远端抖动,关键固定域名可直接写进 /etc/hosts
总结
这次"调用外部接口偶尔卡 5 秒整"的事故,纠正了我一个关于"一次网络请求"的、过于简化的想象。在我的脑子里,一次 HTTP 调用,长久以来就是一件干净利落的两步走的事:把请求发出去,把响应收回来。我所有的耗时直觉、所有的排查动作,都建立在这个"两步走"的模型上——所以当调用变慢时,我本能地、毫不犹豫地,把全部注意力扑到了这两步上:要么是我发得慢、要么是对方回得慢,要么就是中间的网络在抖。我去问对方的接口监控,我去 ping 那个域名,我把客户端"收发响应"那一段的耗时拆得很细——我做的每一件事,都没有错,可它们全都落在了同一个范围之内,而那个真正出问题的地方,恰恰在这个范围【之外】。我始终没有意识到,在我那个模型的最最开头,在"把请求发出去"这一步之前,还藏着一个我从未正眼看过的、沉默的前置步骤:我的程序手里攥着的,是一个域名,一串人类才看得懂的字母;而网络世界里真正能被寻址的,是 IP。在请求能够飞出去之前,必须先有一个人,默默地、在后台,把那个域名翻译成 IP。这个翻译的动作,就是 DNS 解析。它太基础、太理所当然了,以至于在我的心智模型里,它根本不占据任何"时间"——它仿佛是瞬间完成的、不存在的、被我直接忽略掉的一步。可正是这被我忽略掉的一步,卡了我整整 5 秒。复盘到根上,那 5 秒的来历清清楚楚:我的机器要做翻译,它手里有一张写着两个翻译官地址的名单,它老老实实地、一个一个地问。排在第一位的那个翻译官,其实早就不在了——那台内网 DNS 服务器很久以前就下线了,只是没人去名单上把它划掉。可我的机器不知道它已经不在了,它对着那个空无一人的地址礼貌地发问,然后开始等待。它等了整整 5 秒——那是它配置里"等一个 DNS 回话最多等多久"的上限——确认对方真的不会再回答了,才转身去问名单上的第二个翻译官。第二个是好的,一问就答。所以那 5 秒,不是任何东西"慢",而是我的机器,为一个早已不存在的翻译官,固执而忠诚地,守了整整 5 秒的空。它甚至不是每次都卡,因为翻译的结果会被缓存一阵子,只有缓存过期、需要重新翻译的那一次,才会再一次撞上那场注定 5 秒的等待——这也是为什么它显得那么随机、那么难以捉摸。这次最大的收获,是我意识到,我对一件事情的排查能力,被我对这件事情的"心智模型"死死地框住了。我的模型里有几个步骤,我就只会在那几个步骤里找问题;我的模型里没有的步骤,无论它出多大的问题,都永远落在我的视野之外——我不是查不到它,我是根本不会去查它,因为在我的认知地图上,那个地方是一片空白,我甚至不知道那里有东西存在。我那个"发请求、收响应"的两步模型,漏掉了"DNS 解析"这一整步,于是 DNS 出的任何问题,对我来说都是一桩无头悬案。所以下一次,当一个问题在我反复排查过的所有环节里都找不到原因时,我会强迫自己停下来,做一件比"再查一遍"更重要的事:我会去质疑我那张"认知地图"本身是不是完整的。我会一步一步地、不放过任何细节地,去重新走一遍这件事真实发生的全过程——不是走我脑子里那个简化版的模型,而是走它在系统里、在协议里真正经过的每一站。因为答案大概率不在我已经查过的地方,它就藏在我那张地图上,那片我以为什么都没有、所以从来不曾看过一眼的空白里。
—— 别看了 · 2026