2024 年,我们一个服务要调用一个外部的第三方 API。上线后一切正常,可某天起,这个调用开始大面积变慢——平时 100 毫秒就回来的请求,现在动不动就要五六秒。我第一反应是第三方 API 出问题了,可对方信誓旦旦说他们一切正常,我自己 curl 那个接口,有时快有时慢。真正让我觉得蹊跷的,是那个"慢"的耗时——它不是杂乱无章地慢,而是几乎每次都慢在五秒零几这个数字附近,像是有什么东西在精确地"卡表"。一个调用要么很快、要么不多不少卡你五秒,这种带着明显"整数感"的延迟,背后一定藏着某个固定的超时设置。我顺着这个五秒一路往下挖,最后挖到的地方完全出乎我的意料:问题既不在我的代码里,也不在第三方那里,而在一个我从来没打开看过的文件——/etc/resolv.conf。这件事逼着我把 Linux 的 DNS 解析、/etc/hosts、resolv.conf 这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,一个需要调外部 API 的服务
事故现象:
- 调用外部 API 大面积变慢,平时 100ms,现在常常 5 秒+
- 第三方坚称他们服务正常
- 自己 curl 那个接口,时快时慢
- ★ "慢"的时候,耗时几乎都卡在 5 秒零几
现场排查:
# 1. ★ 用 curl 把各阶段耗时拆开看
$ curl -o /dev/null -s -w \
'dns=%{time_namelookup} conn=%{time_connect} total=%{time_total}\n' \
https://api.thirdparty.com/x
dns=5.012 conn=5.015 total=5.103
# ^^^^^^^ ★ 真相!5 秒全花在 DNS 解析(time_namelookup)上!
# 连接本身、数据传输都很快 —— 慢的根本不是网络、不是对方
# 2. 直接测 DNS 解析,看是哪台 DNS 服务器的问题
$ cat /etc/resolv.conf
nameserver 10.0.0.99 # ★ 排在第一的内网 DNS
nameserver 114.114.114.114 # 第二个
$ dig @10.0.0.99 api.thirdparty.com
;; connection timed out; no servers could be reached # ★ 这台挂了!
$ dig @114.114.114.114 api.thirdparty.com
;; ANSWER SECTION: api.thirdparty.com. 300 IN A 1.2.3.4 # 第二个正常
根因(后来想清楚的):
1. ★ /etc/resolv.conf 里第一个 nameserver(10.0.0.99)
是一台内网 DNS,它【已经下线了】—— 我不知道。
2. ★ resolver 解析域名时,是【按顺序】问 nameserver 的:
先问第一个,无响应就【等超时】,再问第二个。
3. 单个 nameserver 的默认超时是 5 秒。所以每次解析:
先向已死的 10.0.0.99 发请求 -> 干等 5 秒超时 ->
才轮到能用的 114.114.114.114 -> 拿到结果。
4. ★ 那个精确的"5 秒",就是 resolver 等死掉的第一个
DNS 超时的时间 —— 一切谜题在此解开。
5. 慢的不是我的代码、不是网络、不是第三方,
是【域名变成 IP】这一步,卡在一台没人告诉我已经
下线的 DNS 服务器上。
接口莫名慢且耗时很"整",先把 DNS 解析这步单独拆出来量。
修复 1:一个域名怎么变成 IP——解析的完整链路
# === ★ 程序访问 api.thirdparty.com,第一步永远是:把它变成 IP ===
# 这一步叫【域名解析】。它发生在"建立连接"之【前】。
# ★ 解析慢,会让整个请求都慢 —— 而它常常是被忽略的一环。
# === 程序解析一个域名,大致会走这几步 ===
# 1. 先查【本地】:/etc/hosts 里有没有写死这个域名
# 2. 没有,再查【DNS】:向 /etc/resolv.conf 里配的
# nameserver 发 DNS 查询
# 3. 拿到 IP,才开始 TCP 连接
# ★ 先查本地、再查 DNS,这个顺序由 /etc/nsswitch.conf 定(下节讲)
# === ★ 第一招:把"解析"这一步单独拎出来量耗时 ===
$ curl -o /dev/null -s -w \
'namelookup=%{time_namelookup}\nconnect=%{time_connect}\ntotal=%{time_total}\n' \
https://api.thirdparty.com/x
# time_namelookup ★ 从开始到【域名解析完成】的耗时
# time_connect 到【TCP 连接建立】的耗时
# ★ 如果 namelookup 就占了大头 -> 慢在 DNS,不用再怀疑别的。
# === 直接验证某个域名解析快不快 ===
$ time getent hosts api.thirdparty.com
# getent hosts 会走和程序【完全一样】的解析流程
# (本地 hosts + DNS 都算),用 time 一卡就知道慢不慢。
# === ★ 为什么 ping 域名能说明问题 ===
$ ping api.thirdparty.com
# ping 在打印第一行之前,也要先解析域名。
# 如果 ping 卡很久才出第一行 -> 解析慢,实锤。
# === 看程序到底解析成了哪个 IP ===
$ getent hosts api.thirdparty.com
1.2.3.4 api.thirdparty.com
# ★ 解析【慢】是一个问题;解析到【错误的 IP】是另一个问题。
# 两个都要会查。
修复 2:/etc/hosts 与 nsswitch.conf——本地解析的优先级
# === ★ 解析域名,不是只有 DNS 一条路 —— 本地 hosts 优先级更高 ===
# === /etc/hosts:本地写死的"域名 -> IP"对照表 ===
$ cat /etc/hosts
127.0.0.1 localhost
10.0.0.50 api.internal.com # ★ 这行 = 写死的映射
# 只要这里写了,解析这个域名【直接出结果,根本不走 DNS】。
# === ★ /etc/hosts 的两面性 ===
# 好处:内网服务、临时联调,写一行 hosts 立刻生效,不依赖 DNS。
# ★ 坑:它优先级【高于 DNS】。如果你曾经为了测试在 hosts 里
# 写死了某个域名,后来那个 IP 变了,你会发现:
# DNS 早更新了,但你的机器死活解析到旧 IP —— 凶手就是 hosts。
# ★ 排查"解析到错误 IP",第一个就该看 /etc/hosts。
# === /etc/nsswitch.conf:决定"先查谁、后查谁" ===
$ grep hosts /etc/nsswitch.conf
hosts: files dns
# ^^^^^ ^^^
# files = /etc/hosts dns = 走 DNS 查询
# ★ 顺序是 files 在前 —— 所以【先查 hosts,hosts 没有才查 DNS】。
# 这就是为什么 hosts 优先级高于 DNS。
# === ★ 一个验证小技巧:临时用 hosts 绕过 DNS ===
# 怀疑是 DNS 慢?在 hosts 里把域名写死成正确 IP:
$ echo '1.2.3.4 api.thirdparty.com' >> /etc/hosts
$ curl ... api.thirdparty.com # 再测,如果立刻变快
# -> 实锤问题就在 DNS 解析。★ 测完记得把这行删掉,别留着。
# === hosts 的修改立即生效,不用重启任何服务 ===
# 它就是个文本文件,程序每次解析都会重新读它。
修复 3:/etc/resolv.conf——DNS 服务器配在哪
# === ★ 走 DNS 查询时,问哪台 DNS 服务器,由这个文件决定 ===
$ cat /etc/resolv.conf
nameserver 10.0.0.99 # ★ 第一个 DNS 服务器
nameserver 114.114.114.114 # 第二个(备用)
search internal.com # 域名补全后缀
options timeout:2 attempts:2 # 超时和重试次数
# === ★ nameserver:可以配多个,但解析是【按顺序】问的 ===
# resolver 解析时:
# 先问第一个 nameserver
# -> 它有响应:用它的结果,结束
# -> 它【无响应】:等到超时,再问第二个
# ★ 关键就在这:第一个 DNS 挂了,不是"立刻跳过",
# 而是要先【死等一个超时】。这正是这次 5 秒的来源。
# === ★ options:超时和重试 —— 决定"等多久" ===
# timeout:N 等单个 nameserver 响应的秒数(★ 默认 5 秒!)
# attempts:N 整个 nameserver 列表轮询几遍(默认 2)
# ★ 默认 timeout 5 秒 —— 这就是事故里那个精确的"5 秒"。
# ★ 把它调小,能大幅降低"第一个 DNS 挂掉"时的代价:
options timeout:1 attempts:2
# 这样第一个 DNS 死了,最多浪费 1 秒就 fallback。
# === ★ 最根本的修复:把死掉的 nameserver 删掉/换掉 ===
$ vi /etc/resolv.conf
- nameserver 10.0.0.99 # ★ 删掉这台已下线的
nameserver 114.114.114.114 # 把能用的提到第一位
nameserver 223.5.5.5 # 再配一个备用
# ★ 永远把【最快最可靠】的 DNS 放第一个 ——
# 因为绝大多数解析,只会用到第一个。
# === search:域名补全(★ 一个隐蔽的拖慢源)===
# 配了 search internal.com,你解析短名 "web" 时,
# resolver 可能先试 web.internal.com、再试 web ……
# ★ search 后缀配多了,每个不存在的组合都要查一遍,拖慢解析。
# 用【完整域名(FQDN)】可以跳过 search 补全。
修复 4:dig 与 nslookup——怎么诊断一次解析
# === ★ 排查 DNS,核心工具是 dig —— 它能精准定位问题 ===
# === 最基本:解析一个域名 ===
$ dig api.thirdparty.com
;; ANSWER SECTION:
api.thirdparty.com. 300 IN A 1.2.3.4
;; Query time: 12 msec # ★ 这次解析花了多久
;; SERVER: 10.0.0.99#53 # ★ 用的是哪台 DNS
# === ★ 关键技巧:指定向【某一台】DNS 服务器查 ===
$ dig @10.0.0.99 api.thirdparty.com # 强制问 10.0.0.99
;; connection timed out; no servers could be reached
# ★ 这一句话,直接锁定 10.0.0.99 这台 DNS 死了!
$ dig @114.114.114.114 api.thirdparty.com # 换一台问
;; ANSWER SECTION: ... 1.2.3.4 # 这台正常
# ★ 一台台 @ 着问,就能精确找出是哪台 DNS 出了问题。
# === 只要简洁结果 ===
$ dig +short api.thirdparty.com
1.2.3.4
# === 查不同类型的记录 ===
$ dig api.thirdparty.com A # IPv4 地址
$ dig api.thirdparty.com AAAA # IPv6 地址
$ dig api.thirdparty.com CNAME # 别名
$ dig api.thirdparty.com MX # 邮件服务器
$ dig api.thirdparty.com NS # 域名服务器
# === ★ 跟踪完整解析路径(从根域名一步步往下)===
$ dig +trace api.thirdparty.com
# 从根 DNS -> 顶级域 -> 权威 DNS,每一跳都列出来 ——
# 排查"解析结果不对/不一致"时特别有用。
# === nslookup:更老牌、更简单的等价工具 ===
$ nslookup api.thirdparty.com
$ nslookup api.thirdparty.com 8.8.8.8 # 指定 DNS 服务器
# === ★ 注意:dig 不读 /etc/hosts,但程序读 ===
# dig/nslookup 是【纯 DNS】工具,它们【不看 /etc/hosts】。
# 而你的程序解析时是看 hosts 的。所以:
# dig 结果对、程序解析却不对 -> 八成是 /etc/hosts 在捣鬼。
# 想看"程序实际会得到什么",用 getent hosts(它走完整流程)。
修复 5:resolv.conf 被覆盖、DNS 缓存的坑
# === 坑 1:★ 你改了 /etc/resolv.conf,过一会儿又变回去了 ===
# /etc/resolv.conf 这个文件,常常【不是给你手改的】——
# 它会被这些东西自动生成、自动覆盖:
# - NetworkManager
# - DHCP 客户端(每次续租 IP 就重写一遍)
# - systemd-resolved
# ★ 你手动改完,下次网络一波动,改动就被冲掉了。
# === 怎么确认是谁在管 resolv.conf ===
$ ls -l /etc/resolv.conf
# 如果是个【软链接】指向别处 -> 它被某个服务托管了
lrwxrwxrwx ... /etc/resolv.conf -> /run/systemd/resolve/resolv.conf
$ head -1 /etc/resolv.conf
# Generated by NetworkManager # ★ 文件头会写明谁生成的
# === 让 DNS 配置【真正持久】的几种正路 ===
# 方法 A:在网卡配置里写 DNS(CentOS 7)
$ vi /etc/sysconfig/network-scripts/ifcfg-eth0
DNS1=114.114.114.114
DNS2=223.5.5.5
$ systemctl restart network
# 方法 B:让 NetworkManager 别管 resolv.conf
$ vi /etc/NetworkManager/NetworkManager.conf
[main]
dns=none
# ★ 直接手改 resolv.conf 只是临时手段,要持久得从源头配。
# === 坑 2:★ DNS 是有缓存的,改了不一定立刻见效 ===
# 缓存可能在好几个地方:
# - DNS 记录本身的 TTL(上面 dig 看到的 300 = 300 秒)
# - 本地缓存服务:nscd / systemd-resolved
# - 应用自己的缓存(★ 比如 JVM 默认会缓存 DNS 结果!)
$ systemctl restart nscd # 清 nscd 缓存
$ systemd-resolve --flush-caches # 清 systemd-resolved 缓存
$ resolvectl flush-caches # 新版命令
# === ★ JVM 的 DNS 缓存是个经典暗坑 ===
# Java 默认会把 DNS 解析结果缓存很久(老版本甚至永久)。
# 域名 IP 变了,Java 服务却一直连旧 IP —— 要调
# networkaddress.cache.ttl 这个安全属性。排查 Java
# 服务的"DNS 不更新",别忘了这一层。
# === 验证缓存到底有没有清干净 ===
$ dig api.thirdparty.com # dig 不走本地缓存,看真实
$ getent hosts api.thirdparty.com # getent 走完整流程,看实际
修复 6:DNS 解析排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ 请求慢,先把"DNS 解析"这一步单独量出来 ===
$ curl -o /dev/null -s -w 'dns=%{time_namelookup} total=%{time_total}\n' URL
# dns 占了大头 -> 别再怀疑网络和对方,问题在解析。
# === 2. ★ 耗时很"整"(正好 5 秒等)往往是超时在作祟 ===
# resolv.conf 默认 timeout 5 秒 —— 第一个 DNS 挂了就卡 5 秒。
# === 3. 解析到【错误 IP】,第一个查 /etc/hosts ===
$ cat /etc/hosts # hosts 优先级高于 DNS
# === 4. ★ 用 dig @ 逐台 DNS 服务器排查 ===
$ dig @某DNS 域名 # 哪台挂了一目了然
# === 5. resolv.conf 把最快最可靠的 DNS 放第一位 ===
# 删掉下线的 nameserver;timeout 可调小到 1~2 秒。
# === 6. 改了 DNS 不生效,想到缓存和 resolv.conf 被覆盖 ===
$ ls -l /etc/resolv.conf # 看是不是被托管/软链接
$ systemd-resolve --flush-caches # 清缓存
# === 7. 排查 DNS 问题的命令链 ===
$ curl -w '%{time_namelookup}\n' URL # ① 解析这步慢不慢
$ getent hosts 域名 # ② 程序实际解析到啥
$ cat /etc/hosts # ③ 有没有被 hosts 写死
$ cat /etc/resolv.conf # ④ 配了哪些 DNS 服务器
$ dig @每个DNS 域名 # ⑤ 逐台 DNS 测,谁挂了
$ dig +trace 域名 # ⑥ 完整解析路径
# 按这个顺序,DNS 解析问题基本能定位。
命令速查
需求 命令
=============================================================
拆分请求各阶段耗时 curl -o /dev/null -s -w '%{time_namelookup}\n' URL
看程序实际解析到的 IP getent hosts 域名
解析一个域名(走 DNS) dig 域名 / nslookup 域名
只要解析结果 dig +short 域名
向指定 DNS 服务器查询 dig @8.8.8.8 域名
跟踪完整解析路径 dig +trace 域名
查特定记录类型 dig 域名 A / AAAA / CNAME / MX
看本地 hosts 表 cat /etc/hosts
看配了哪些 DNS 服务器 cat /etc/resolv.conf
清 systemd-resolved 缓存 systemd-resolve --flush-caches
口诀:请求慢先用 curl -w 看 time_namelookup 是不是大头
耗时正好卡 5 秒 -> 八成第一个 DNS 挂了在等超时
避坑清单
- 请求慢先用 curl -w 拆出 time_namelookup,判断是不是慢在 DNS 解析
- 耗时正好卡在 5 秒左右,往往是 resolv.conf 默认超时,第一个 DNS 挂了
- resolv.conf 的 nameserver 是按顺序问的,第一个无响应要等超时才换
- 把最快最可靠的 DNS 放第一位,删掉已下线的 nameserver
- options timeout 默认 5 秒,可调小到 1-2 秒降低 DNS 故障的代价
- /etc/hosts 优先级高于 DNS,解析到错误 IP 第一个查 hosts
- dig/nslookup 不读 /etc/hosts,dig 对但程序错八成是 hosts 在捣鬼
- dig @某DNS 域名可逐台测试 DNS 服务器,精确定位哪台挂了
- resolv.conf 常被 NetworkManager/DHCP 覆盖,手改不持久要从源头配
- DNS 有多层缓存,nscd、systemd-resolved、JVM 自身都会缓存解析结果
总结
这次"调用外部 API 莫名卡五秒"的事故,纠正了我一个长期存在的、关于"一次网络请求到底由几步组成"的认知盲区。在这次之前,我心里对"调用一个 API"的理解,是一个干净利落的两段式:建立连接,然后收发数据。当一个请求变慢,我的全部注意力,也就只会在这两段上来回打转——要么是连接建立得慢(网络不通畅、对方服务器忙),要么是数据传输得慢(响应体大、带宽不够)。在这个二段式的世界观里,我排查问题的对象,永远是"网络"和"对方那台服务器"。所以当这个调用开始大面积卡五秒,而我又确认了网络通畅、对方也信誓旦旦说自己正常时,我陷入了僵局——按我的模型,能慢的地方就那么两处,可那两处查下来都没问题,那这五秒到底是从哪冒出来的?复盘到根上,我才终于看清,我那个两段式的模型,从一开始就漏掉了最前面、也最隐形的一段。我的程序代码里,写的是一个域名 api.thirdparty.com;可计算机网络这台机器,它只认 IP 地址,它根本不知道这个域名是什么。所以在"建立连接"这一步真正开始之前,必然先有一个我从未正眼看过的步骤在默默发生:把这个域名,翻译成一个 IP 地址。这一步,就是 DNS 解析。它发生在连接之前,它快它慢,会一字不差地累加到整个请求的总耗时里——而我,从来没有把它当作一个"会出问题的环节"来对待过。我一直默认它是瞬间完成、不可能出错的,以至于在我的脑子里,它根本不占一个独立的位置。揭穿这一切的,是那个把请求耗时拆开来看的 curl -w:它清清楚楚地告诉我,time_namelookup——也就是域名解析这一步——独占了那整整五秒,而真正的连接和数据传输,加起来不过一百毫秒。慢的根本不是网络,不是对方,而是"把域名变成 IP"这一步。顺着这条线再往下,我才打开了 /etc/resolv.conf 这个我从来没看过的文件,看见了里面按顺序排列的两个 DNS 服务器。而 DNS 解析的规则,正是问题的最后一块拼图:resolver 解析域名时,是【按顺序】去问这些 DNS 服务器的——先问第一个,如果第一个没有响应,它不会立刻跳到第二个,而是要先【死等一个超时】。这个超时的默认值,恰恰就是五秒。我那个文件里排在第一位的,是一台早已下线、却没有人告诉我的内网 DNS 服务器。于是每一次解析,都要先向这台死掉的服务器发出请求、然后徒劳地等满五秒、确认它真的不会回应了,才终于轮到第二台能用的 DNS。那个带着诡异"整数感"的五秒,从头到尾就是 resolver 在为一台尸体般的 DNS 服务器默哀的时间。这次从一个"卡五秒"的谜题出发,我最大的收获,是在脑子里那个"连接 + 传输"的请求模型最前面,郑重地补上了"域名解析"这关键的一环。一次请求慢,可慢的地方,远不只是网络和对方——在数据包真正出发去远方之前,还有"把名字翻译成地址"这一步看不见的准备工作,它同样会出故障,同样会拖慢一切。而当一个延迟带着可疑的"整数感"——不多不少正好五秒——它几乎总是在向你提示:某个地方有一个固定的超时设置正在被触发,有什么东西,正卡在一个等待回应、却永远等不到的地方。
—— 别看了 · 2026