域名解析时灵时不灵:一次 Linux DNS 排查复盘

服务调外部 API 偶发超时,curl -w 拆解才发现时间全花在 DNS 解析上。排查梳理:域名变 IP 的完整链路、nsswitch 与 /etc/hosts 的优先级坑、resolv.conf 的 nameserver 顺序与 timeout/rotate、dig 指定 DNS 隔离故障、DNS 缓存与应用层缓存,以及 DNS 配置纪律。

2024 年我们一个服务在调外部的支付 API,平时好好的,可总有那么几次请求会卡住好几秒、甚至超时。我一开始怀疑是对方服务慢,可对方信誓旦旦说他们一切正常。我自己在服务器上 curl 那个域名,反复试,大部分秒回,偶尔却要卡上整整五秒——而且卡的时候,卡在"连接还没建立"之前。这个"五秒"像一根刺,最后顺着它查下去,真相落在了一个我从没正眼瞧过的地方:DNS 解析。这次排查逼着我把"一个域名到底怎么变成 IP"这件事彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,一个 Java 服务调外部 api.pay-example.com
事故现象:
- 调用外部 API 偶发超时,大部分正常
- 对方坚称他们服务没问题
- 本机 curl 该域名,大多数秒回,偶尔卡 5 秒

现场排查:
# 1. 用 curl 把各阶段耗时拆开看
$ curl -o /dev/null -s -w \
   'dns:%{time_namelookup} connect:%{time_connect} total:%{time_total}\n' \
   https://api.pay-example.com/ping
dns:0.003 connect:0.045 total:0.210      # 正常的一次
dns:5.012 connect:5.055 total:5.230      # 卡的那次
# ★ 卡的时候,time_namelookup 就是 5 秒 ——
#   时间全花在【DNS 解析】上,根本还没开始连接

# 2. 看 DNS 配置
$ cat /etc/resolv.conf
nameserver 10.0.0.2
nameserver 8.8.8.8
options timeout:5 attempts:2
# 配了两个 DNS,第一个是内网 DNS 10.0.0.2

# 3. 直接拿第一个 DNS 试解析
$ dig @10.0.0.2 api.pay-example.com
;; connection timed out; no servers could be reached
# ★ 真相:内网 DNS 10.0.0.2 时好时坏!
#   它不通时,要等 timeout 5 秒,才换第二个 DNS

根因(后来定位到的):
resolv.conf 里第一个 nameserver(内网 DNS)不稳定。
解析时系统先问它,它没响应,要死等 options timeout
设定的 5 秒,超时后才轮到第二个 DNS。
那"偶发的 5 秒",就是在等第一个 DNS 超时。
应用日志里只看得到"请求慢",根本想不到是 DNS。

修复 1:一个域名是怎么变成 IP 的

=== 程序里写的是域名,但网络只认 IP ===
=== 这中间的翻译过程,叫"名字解析" ===

当程序要访问 api.pay-example.com,系统会按
/etc/nsswitch.conf 里 hosts 那一行规定的顺序去查:

$ grep hosts /etc/nsswitch.conf
hosts: files dns

含义:解析主机名时,先查 files,再查 dns。
- files = /etc/hosts 这个本地静态文件
- dns   = 按 /etc/resolv.conf 配的 DNS 服务器去网络查

=== 完整链路(以默认 files dns 为例)===
1. 程序要 api.pay-example.com 的 IP
2. 先翻 /etc/hosts —— 里面写死了就直接用,不走网络
3. /etc/hosts 里没有 -> 问 /etc/resolv.conf 里的 DNS
4. DNS 服务器层层递归查询,最终返回一个 IP
5. (可能有缓存)结果可能被本地缓存一段时间

=== 排查 DNS 问题,就是顺着这条链路逐环节查 ===
- nsswitch.conf : 查询顺序对不对
- /etc/hosts    : 有没有写错、写了过期的条目
- resolv.conf   : DNS 服务器配得对不对、稳不稳
- 缓存          : 是不是缓存了旧结果
理清这条链路,DNS 问题就不再是玄学。

修复 2:/etc/hosts——优先级最高的本地解析

# === /etc/hosts:一份本地的、静态的"域名->IP"对照表 ===
$ cat /etc/hosts
127.0.0.1   localhost
10.0.1.10   db-master db-master.internal
10.0.1.20   redis-node

# 格式:IP   主机名  [别名...]
# 因为 nsswitch 里 files 排在 dns 前面,
# 这里写了的域名,【根本不会去问 DNS】,直接用这个 IP。

# === hosts 的几个典型用途 ===
# 1. 内网服务,不想搭 DNS,直接写死
# 2. 临时把某域名指到指定 IP(灰度、调试、绕过)
# 3. 屏蔽某域名:把它指到 127.0.0.1

# === 它也是个坑:hosts 的优先级太高,会"压"过 DNS ===
# 如果某域名的真实 IP 变了,但 /etc/hosts 里还留着
# 一条过期的旧记录 —— 你的机器会一直用那个错 IP,
# DNS 改对了也没用,因为 files 在 dns 前面就命中了。
# ★ 域名解析"诡异地不对",第一件事就是看 /etc/hosts

# === 验证一个域名当前解析到哪、走的哪条路 ===
$ getent hosts api.pay-example.com
# getent 严格按 nsswitch 的顺序解析,
# 它的结果 = 你的程序实际会拿到的结果。
# 这比 dig 更贴近"程序真实看到的",因为 dig 只问 DNS、
# 不看 /etc/hosts。两者结果不一致 -> 多半 hosts 在作怪。

# === 改 hosts 立即生效,不用重启任何服务 ===
$ vim /etc/hosts
# 注意:已经建立的长连接不会变,新解析才用新值。

修复 3:/etc/resolv.conf——DNS 服务器配置

# === /etc/hosts 里没有,就走 /etc/resolv.conf 配的 DNS ===
$ cat /etc/resolv.conf
nameserver 10.0.0.2
nameserver 8.8.8.8
search internal.example.com
options timeout:2 attempts:2 rotate

# === nameserver:DNS 服务器,可写多个 ===
# 解析时【从上到下】问:先问第一个,
# 它没响应,等 timeout 后才问第二个。
# ★ 这次的坑就在这:第一个不稳定,每次都要先等它超时。

# === options:解析行为的关键调节项 ===
# timeout:2  等单个 DNS 响应最多 2 秒(默认 5,太长!)
# attempts:2 整个 nameserver 列表最多轮几遍
# rotate     每次轮流从不同 nameserver 开始问,
#            不要每次都死磕第一个 —— 能分摊、能避开坏的
# 把 timeout 从 5 调到 2,rotate 打开,
# 这次那个"偶发 5 秒"立刻缩短成了最多 2 秒。

# === search:给短主机名自动补的域名后缀 ===
# 配了 search internal.example.com 后,
# 你 ping db-master,系统会自动试 db-master.internal.example.com
# 方便,但也有坑:补全会产生额外的 DNS 查询,
# search 配多个域时,一次解析可能放大成好几次查询。

# === 一个大坑:resolv.conf 经常被自动覆盖 ===
# NetworkManager / dhclient 会在网络重启时
# 用 DHCP 拿到的 DNS【重写】这个文件,
# 你手改的内容下次重启就没了。
# 想固定下来,要在网卡配置里设:
$ vim /etc/sysconfig/network-scripts/ifcfg-eth0
PEERDNS=no                       # 不让 DHCP 覆盖 DNS
DNS1=223.5.5.5
DNS2=8.8.8.8
# 这样 resolv.conf 才会被稳定地生成成你要的样子。

修复 4:dig / nslookup——把解析过程看清楚

# === dig:排查 DNS 最顺手的工具 ===
$ dig api.pay-example.com
# 看 ANSWER SECTION —— 解析出来的 IP
# 看 Query time     —— 这次解析花了多久(ms)
# 看 SERVER         —— 实际是哪个 DNS 答的

# === 指定用某个 DNS 来查(隔离问题 DNS 的利器)===
$ dig @10.0.0.2 api.pay-example.com    # 强制用内网 DNS
$ dig @8.8.8.8 api.pay-example.com     # 强制用公共 DNS
# 这次就是这样定位的:@10.0.0.2 超时、@8.8.8.8 秒回,
# 一对比就锁定了"第一个 DNS 不稳定"。

# === 只要结果、看着清爽 ===
$ dig +short api.pay-example.com
203.0.113.45

# === 看完整的递归追踪(域名从根开始怎么查的)===
$ dig +trace api.pay-example.com
# 从根域 -> 顶级域 .com -> 权威 DNS,一步步打出来,
# 能看出到底卡在哪一层。

# === nslookup:更简单的查询 ===
$ nslookup api.pay-example.com
$ nslookup api.pay-example.com 8.8.8.8   # 指定 DNS

# === 反向解析:IP 查域名 ===
$ dig -x 203.0.113.45

# === 一个判断技巧:解析慢 还是 解析错 ===
# dig 的 Query time 很大 -> 解析【慢】,查 DNS 服务器
# dig 返回的 IP 不对   -> 解析【错】,查 hosts / DNS 记录 / 缓存
# 先用 dig 把"慢"和"错"分开,后面才不会查错方向。

修复 5:DNS 缓存——"明明改了却没生效"的元凶

# === 解析结果会被缓存,这是"改了不生效"的常见原因 ===
# 缓存可能在好几个层面:
# 1. DNS 记录本身的 TTL —— 权威 DNS 说"这条记录可缓存 N 秒"
# 2. 本地的缓存服务 —— nscd 或 systemd-resolved
# 3. 应用自己的缓存 —— 比如 JVM 默认会缓存 DNS 结果

# === 看本机有没有装 DNS 缓存服务 ===
$ systemctl status nscd
$ systemctl status systemd-resolved

# === nscd:老牌的名字服务缓存 ===
$ nscd -g                       # 看缓存统计
$ nscd -i hosts                 # 清掉 hosts 这一类的缓存
$ systemctl restart nscd        # 或直接重启

# === systemd-resolved 的缓存 ===
$ resolvectl statistics         # 看缓存命中情况
$ resolvectl flush-caches       # 清空缓存
$ resolvectl query api.pay-example.com   # 用它来查

# === 一个隐蔽的坑:JVM 的 DNS 缓存 ===
# Java 程序默认会把解析结果缓存很久(老版本甚至永久)。
# 域名 IP 变了,Java 进程却一直用旧 IP ——
# 这要在 JVM 层面调:
#   networkaddress.cache.ttl=60   (java.security 里)
# 排查 Java 服务的 DNS 问题,别忘了这一层。

# === 验证"现在到底解析到哪" ===
# 系统层:
$ getent hosts api.pay-example.com
# 绕过本地缓存、直接问权威:
$ dig +trace api.pay-example.com
# 改了 DNS 记录不生效时,先看 TTL 还没过期,
# 再清本地缓存,最后查应用自己的缓存 —— 逐层排除。

修复 6:DNS 配置的纪律与最佳实践

# === 这次事故也暴露了我们 DNS 配置上的随意 ===

# === 1. 至少配两个 nameserver,且都要稳 ===
# 单个 DNS 是单点故障。但更要紧的是:
# 第一个 nameserver 必须是最稳的那个,
# 因为系统默认总是先问它。把不稳的放第一个,
# 就是这次"偶发 5 秒"的根源。

# === 2. timeout 别用默认的 5 秒,调小 ===
$ cat /etc/resolv.conf
options timeout:2 attempts:2 rotate
# 默认 timeout 5 秒太长 —— 第一个 DNS 一抽风,
# 整个请求就被拖 5 秒。调到 1~2 秒,
# 配合 rotate,坏掉一个 DNS 的影响被压到最小。

# === 3. 内网服务:hosts 还是 DNS,要想清楚 ===
# 写 /etc/hosts:简单、不依赖 DNS,但【改一处要改所有机器】,
#   机器一多就是维护灾难,还容易留过期条目。
# 用内网 DNS:统一管理、改一处全网生效,
#   但 DNS 本身要可靠。
# 小规模用 hosts、上规模必须上内网 DNS。

# === 4. 关键域名,监控解析耗时 ===
$ dig +noall +stats api.pay-example.com | grep 'Query time'
# 把它写进定时脚本,Query time 突然变大就告警 ——
# 这次的问题如果有这个监控,根本不用等用户反馈。

# === 5. 改 DNS 记录前,先把 TTL 调小 ===
# 计划改某域名的 IP?提前一两天把它的 TTL 从
# 比如 3600 改成 60。这样真正切换时,
# 旧缓存最多 60 秒就过期,切换平滑。

# === 6. resolv.conf 要防被覆盖 ===
# 前面说过:NetworkManager/DHCP 会重写它。
# 在网卡配置里 PEERDNS=no + DNS1/DNS2 写死,
# 否则你精心调好的 timeout、rotate,重启就没了。

# === 排查 DNS 问题的固定顺序 ===
# ① curl -w 看 time_namelookup,确认真是 DNS 慢
# ② getent hosts 看程序实际拿到什么
# ③ 查 /etc/hosts 有没有过期/错误条目
# ④ dig @每个 nameserver,逐个试,找出坏的那个
# ⑤ 清本地缓存、查应用层缓存

命令速查

需求                        命令
=============================================================
拆解请求各阶段耗时          curl -o /dev/null -s -w '%{time_namelookup}'
按 nsswitch 顺序解析        getent hosts 域名
看解析顺序配置              grep hosts /etc/nsswitch.conf
看本地静态解析表            cat /etc/hosts
看 DNS 服务器配置           cat /etc/resolv.conf
查域名解析(默认 DNS)      dig 域名  /  dig +short 域名
指定 DNS 来查               dig @8.8.8.8 域名
看完整递归追踪              dig +trace 域名
反向解析(IP 查域名)       dig -x IP
清 systemd-resolved 缓存    resolvectl flush-caches
清 nscd 缓存                nscd -i hosts
看解析耗时                  dig 域名 | grep 'Query time'

口诀:curl -w 确认是 DNS 慢 -> getent 看实际结果
      -> 先查 hosts -> dig @ 逐个 DNS 试 -> 查缓存

避坑清单

  1. 域名解析按 nsswitch.conf 的 hosts 行决定顺序,默认 files(hosts)先于 dns
  2. /etc/hosts 优先级高于 DNS,留了过期条目会让 DNS 改对了也不生效
  3. 用 getent hosts 看程序实际拿到的解析结果,它比 dig 更贴近真实
  4. resolv.conf 里 nameserver 从上到下问,最稳的 DNS 必须放第一个
  5. options timeout 默认 5 秒太长,第一个 DNS 抽风就拖 5 秒,应调小
  6. 打开 options rotate,让解析轮流从不同 nameserver 开始,避开坏的
  7. resolv.conf 会被 NetworkManager/DHCP 覆盖,要在网卡配置 PEERDNS=no
  8. dig @ 指定 DNS 逐个测试,能快速锁定是哪个 nameserver 坏了
  9. 解析结果改了不生效,逐层查:记录 TTL、本地缓存、应用层缓存
  10. Java 进程默认长时间缓存 DNS,域名 IP 变更要调 networkaddress.cache.ttl

总结

这次 DNS 解析的排查,纠正了我一个特别典型的思维盲区:我们在排查"服务调用慢"这类问题时,注意力几乎本能地会全部集中在"连接"和"对端服务"上——是不是网络抖动了、是不是对方服务变慢了、是不是连接池满了——却几乎从不会想到,在 TCP 连接被建立起来之前,其实还有一个完全独立、而且同样可能出问题的环节默默地发生着,那就是把域名翻译成 IP 地址的 DNS 解析。这次的"偶发五秒"之所以折磨人,正是因为它藏在这个被所有人忽略的环节里:应用日志只会忠实地记录下"这次请求花了五秒",可它绝不会告诉你,这五秒里有四点九九秒,程序其实还停在原地,连第一个网络包都没发出去,它在干等一个根本不会回应的 DNS 服务器超时。把我从错误方向上拉回来的,是 curl 的 -w 参数。我用它把一次请求的耗时拆成了好几个阶段——time_namelookup、time_connect、time_total——这一拆,真相立刻就清晰了:正常的那些请求,time_namelookup 是几毫秒;而卡住的那些,time_namelookup 直接就是五秒整,后面所有阶段的时间戳都被这五秒整体顺延了。这意味着时间百分之百花在了 DNS 解析上,跟连接、跟对端服务一点关系都没有。这个经验我要牢牢记住:以后再遇到"访问某个域名偶发性地慢",排查的第一步,永远是先用 curl -w 把 time_namelookup 单独拎出来看一眼,先回答"到底是不是 DNS 慢"这个问题,再决定往哪个方向查,绝不能上来就一头扎进连接和对端里。确认了是 DNS 之后,这次排查也逼着我把"一个域名究竟是怎么变成 IP 的"这条完整链路梳理了一遍。我过去对这件事的认识是模糊的一团,而现在它在我脑子里成了一条清清楚楚的流水线:程序要解析一个域名,系统会先看 /etc/nsswitch.conf 里 hosts 那一行规定的顺序,默认是先查 files、再查 dns;所谓 files,就是去翻 /etc/hosts 这个本地静态文件,里面要是写死了这个域名,就直接用、根本不碰网络;hosts 里没有,才轮到去问 /etc/resolv.conf 里配置的那些 DNS 服务器。理解了这条链路,我也就理解了它每一个环节各自对应着一类什么样的故障。/etc/hosts 的优先级高得惊人,它能直接盖过 DNS,这是它的用处,也是它最阴险的坑——一旦某个域名的真实 IP 变了,而 hosts 里还残留着一条过期的旧记录,你这台机器就会雷打不动地用那个错误的 IP,任凭你把 DNS 记录改得再正确都无济于事,因为解析在 files 这一步就提前命中、提前返回了。而这次真正的元凶,藏在 resolv.conf 这一环:里面配了两个 nameserver,系统永远是从上往下问,先问第一个,第一个没有响应,就必须老老实实地死等 options 里 timeout 设定的那个秒数,超时之后才肯去问第二个。我们的第一个 nameserver 是一台不太稳定的内网 DNS,它一抽风,每一次解析就都得先赔上 timeout 那五秒。找到它的过程也让我学到了一个极其实用的隔离技巧——dig 后面加个 @,就能强制指定用某一台 DNS 来查,我用 dig @第一个DNS 和 dig @第二个DNS 一对比,一个超时、一个秒回,坏的那台 DNS 当场就被锁定了。对应的修复也很直接:把最稳的 DNS 调到第一位,把那个长得离谱的默认 timeout 从五秒压到一两秒,再打开 rotate 让解析轮流从不同的 nameserver 开始,这样即便某一台 DNS 坏掉,它对整体的拖累也被压到了最小。这次排查从最初对着应用日志里那个孤零零的"五秒"百思不得其解,到最后理清"curl -w 确认是 DNS、getent 看实际结果、先查 hosts、dig @ 逐个测 DNS、再查各层缓存"这样一条清晰的排查路径,我最大的收获,是真正把 DNS 解析当成了一个值得严肃对待的、独立的排查维度——它不再是那个我从不正眼看一眼的、想当然以为"总是瞬间完成"的黑盒,而是一条我能一个环节一个环节拆开来看的、透明的链路。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

内存被吃光了?——一次 Linux 内存使用率排查复盘

2026-5-20 17:44:30

Linux教程

服务重启后没自己起来:一次 Linux systemd 排查复盘

2026-5-20 17:50:14

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索