一、为什么先怀疑名字解析
很多"服务器变慢"的排查最后都落到一个很别扭的结论上:CPU 不高、内存不缺、磁盘不忙、带宽没用满,但接口就是慢几百毫秒,而且是"时不时"地慢。如果你用 time 去量一个最普通的命令,发现它本身没干什么活却花了 5 秒,那基本可以把嫌疑锁定在名字解析这一层。
典型的现场是这样:登录服务器敲 sudo,回车之后要等三到五秒才出提示符;用 curl 请求本机服务,明明做了解析却慢得离谱;给数据库连本机地址,连接建立时间长得异常;写一个极简的上传接口,什么都没做就耗时五秒。这些症状的共同点是"操作系统在做一件你以为不需要联网的事"——把 IP 反查成名字、把名字解析成 IP,而它用的那条路径不通。
名字解析之所以坑人,是因为它不是一条路,而是一串按顺序尝试的路径。任何一条路径超时,整个调用都要等那一份超时时间,而每一次调用都可能重新走一遍。理解了这条链路,你就能判断"到底是 DNS 服务器慢了,还是本机在等一个根本不该问的地方"。
二、gethostbyname 的实际路径
大多数老程序仍走 glibc 的 gethostbyname,它在 Linux 上的实际行为由 /etc/nsswitch.conf 决定。默认的 hosts 一行通常是:
hosts: files dns myhostname含义是按顺序询问:先查 /etc/hosts,再查 /etc/resolv.conf 里配置的 DNS 服务器,最后用 systemd 提供的 myhostname 模块回答本机主机名。看起来三条路都很短,但坑恰恰埋在每一段里。
第一段 files:/etc/hosts 里如果写了本机主机名却写错 IP,或者写了 IPv6 地址但机器没有 IPv6,那么后续程序拿到这个错误的地址后连不上,再回头重试,时间就浪费在重试上。第二段 dns:resolv.conf 里若有一个不通的 nameserver,glibc 会按 timeout 与 attempts 参数等待它超时,然后才问下一个,这个等待是串行的。第三段 myhostname:只有在装了 systemd-resolved 或 libnss-myhostname 时才存在,缺失时这一项会被静默跳过,但缺失本身会让"本机主机名解析"退化到 DNS,而公网 DNS 不认识你的内网主机名。
所以第一个判断动作非常简单:打开 nsswitch.conf 看 hosts 那一行的顺序,再打开 resolv.conf 看 nameserver 有几个、有没有不通的。
三、正向与反查是两件不同的事
很多人以为"域名解析"只有正向:名字转 IP。实际上系统里大量时间花在反向:IP 转名字。SSH 登录、Postfix 收信、Apache/Nginx 的访问日志、PHP 的某些函数、甚至 sudo 的审计日志,都可能对客户端 IP 做一次 PTR 查询。
反查走的是 in-addr.arpa 区,由 IP 的持有者维护。你自己的内网 IP 比如 10.x 或 192.168.x,如果在内网 DNS 里没有配对应的反向区,这次的答复就是 NXDOMAIN 或超时。更糟的是,有些环境把内网网段的查询转发到公网 DNS,公网 DNS 对私有地址段的反查通常直接返回失败,多一次往返就是多一份延迟。
验证方法很直接:
time getent hosts 10.0.0.5
time getent hosts 127.0.0.1
time dig -x 10.0.0.5 +short如果 getent 花了几秒而 dig 很快,说明问题在 nsswitch 链路而不是 DNS 服务器本身;如果 dig 也慢,说明那台 DNS 服务器对你这个查询响应慢或根本不通。
另一个常被忽略的点是 127.0.0.1 和 ::1。有些发行版的 hosts 文件里硬编码了 localhost 的正向,却没有对应的反查;某些程序要拿 127.0.0.1 反查主机名,一旦 hosts 里没有 127.0.0.1 localhost 这行,它就会去问 DNS。查一下这行是否存在,是一分钟就能做完的检查。
四、resolv.conf 的五个参数决定超时形态
resolv.conf 不只是 nameserver 的列表,它有四个参数直接决定"卡多久":
nameserver:最多三个会被 glibc 使用,第四个开始被忽略。这一点很关键——你以为配了四个备用,实际只有前三个生效。timeout:单次查询等待秒数,默认 5。这就是 sudo 卡三到五秒的直接来源。attempts:重试全部 nameserver 的轮数,默认 2。最坏情况耗时约等于 timeout 乘以 attempts 乘以 nameserver 数量。options rotate:让查询在多个 nameserver 间轮询而不是总用第一个,能分摊压力,但也会让"随机变慢"变成常态。options single-request:把 A 与 AAAA 查询分成两个连接串行发送,用于绕过某些老设备不响应双查询的问题,代价是略慢但更稳。
按默认值算,如果配了三个 nameserver 且其中两个不通,一次解析最坏要等 5 乘以 2 乘以 2 等于 20 秒。这就是为什么有时候是"卡 5 秒"有时候是"卡 20 秒",取决于第几个服务器先答。
调参建议:内网环境把 timeout 降到 1 到 2 秒,attempts 保持 1 到 2;nameserver 只留真实可用的两个,第二个必须是同机房或同可用区的。宁可少配一个可疑的备用,也不要留一个不通的"心理安慰"。
还要注意 resolv.conf 会被覆盖。用 NetworkManager 的机器上改它会在重启后失效,正确做法是在 /etc/NetworkManager/conf.d/ 里加 dns 配置,或在 /etc/dhcp/dhclient.conf 里 prepend domain-name-servers;用 systemd-resolved 的机器上,resolv.conf 通常是指向 /run/systemd/resolve/stub-resolv.conf 的软链,真正要改的是 resolved.conf。改之前先 ls -l /etc/resolv.conf 看一眼它是不是软链,能省掉后面一次"改了却没生效"的困惑。
五、systemd-resolved 与 stub 监听
新版 Debian 和 Ubuntu 默认启用 systemd-resolved,它在 127.0.0.53 上开一个 stub 监听,resolv.conf 指向它。这意味着你用 dig 直接问 8.8.8.8 得到的结论,不能代表应用程序实际体验,因为应用问的是 127.0.0.53,再由 resolved 转发出去。中间多了一层,多了一层就可能多一层坑。
常用诊断命令:
resolvectl status
resolvectl query www.example.com
resolvectl statisticsresolvectl statistics 里的 cache hit 和 cache miss 很能说明问题。命中率极低说明 TTL 太短或者每次查询的域名都不同(典型的爬虫站或大量随机子域);如果 cache 完全没增长,可能是 resolved 没在跑,或者应用绕过它直接读 resolv.conf 去问了上游。
还有一个常见现象:docker 容器里的解析。容器默认继承宿主机的 resolv.conf,但如果里面写的是 127.0.0.53,在容器命名空间里这个地址并不存在,查询就会超时。正确的做法是让 docker 使用外部 DNS 地址,配置在 /etc/docker/daemon.json 的 dns 字段里,或者在 docker-compose 的 dns 选项里指定。这一点在"容器里访问数据库特别慢、宿主机上正常"的场景里命中率极高。
六、一个照做就能定量的排查脚本
与其凭感觉,不如把每一步都量出来。下面这段脚本按链路顺序逐一计时,输出单位是毫秒,哪一步异常一眼就能看出:
#!/bin/bash
# resolve_diag.sh
h="$1"
echo "== nsswitch hosts 行"
grep '^hosts' /etc/nsswitch.conf
echo "== resolv.conf 关键项"
grep -E '^(nameserver|options)' /etc/resolv.conf
echo "== hosts 文件是否含本机名"
grep -E '127.0.0.1|::1' /etc/hosts
echo "== getent 正向(走 nsswitch 全链路)"
/usr/bin/time -f '%e s' getent hosts "$h"
echo "== 每个 nameserver 单独测"
for ns in $(awk '/^nameserver/{print $2}' /etc/resolv.conf); do
t=$(dig @"$ns" "$h" +short +time=2 +tries=1 | head -1)
s=$(date +%s%N)
dig @"$ns" "$h" +time=2 +tries=1 >/dev/null 2>&1
e=$(date +%s%N)
echo "$ns 答案=$t 耗时=$(( (e-s)/1000000 ))ms"
done
echo "== 反查测试"
/usr/bin/time -f '%e s' dig -x 127.0.0.1 +short这个脚本的价值在于把"感觉慢"拆成可比较的数字。我遇到过一次:getent 走完要 6 秒,而每个 nameserver 单独测都在 20 毫秒内。结论不是 DNS 慢,而是我的 /etc/hosts 里写了一个指向已下线 IP 的主机名,getent 拿到结果后程序去连接失败再重试。这类问题只有分步计时才能定位。
如果确认是反查慢,最快的止血方式是在内网 DNS 里补一个反向区,或者在这台机器的 hosts 里把常用内网 IP 写死。写死不是长久之计,但在告警之夜它是最快的恢复手段。
七、六个高频坑
第一,/etc/hosts 里有重复主机名。文件是自上而下查找的,第一条匹配生效,后面写的"修正"根本没被用上。改完记得清一遍重复行。
第二,IPv6 的 AAAA 记录拖慢进程。程序拿到 AAAA 后优先尝试 IPv6 连接,如果本机 IPv6 出口不通,要等连接超时才回落 IPv4。表现是"首次请求慢、之后快",因为内核会记住连接失败。根治方式是修 IPv6 路由,临时缓解可以在应用层显式指定 IPv4 协议族。
第三,把公网 DNS 写进内网机器的 resolv.conf。内网服务器解析自己的内网域名,问公网 DNS 永远得不到答案,每次都是超时而不是快速失败。内网机器必须用内网 DNS。
第四,自定义的 search 域。resolv.conf 里的 search example.internal 会让一个不含点的短名字尝试拼接后缀查询,每次拼一个后缀就是一次完整查询。短名多的时候,解析次数会成倍增加。能用 FQDN 就用 FQDN,能去掉多余的 search 就去掉。
第五,容器与宿主机的 resolv.conf 不一致。宿主机上排查一切正常,进容器就慢,多半是容器里的 resolver 指向了宿主空间才有的地址。
第六,把 DNS 当业务依赖。业务代码里对每个请求都做一次域名解析,DNS 一抖动全站雪崩。正确做法是在进程内做带 TTL 的缓存,或者由本地的 127.0.0.1 缓存服务统一出口,让上游 DNS 挂掉时仍有缓存可答。
八、小结:先量,再改
名字解析的问题有个共同特点:它不像 CPU 或内存那样能在 top 里一眼看到,而是藏在每一次系统调用的等待里。所以排查顺序固定为三步:先用 getent 确认是不是 nsswitch 链路的问题,再用 dig 逐个 nameserver 排除上游,最后看反查与 IPv6 是不是在偷偷加时长。三步都在一分钟内完成,比盲目重启服务有效得多。
改配置时守住两条线:一是内网机器只问内网 DNS,二是 nameserver 只留真实可用的地址并把 timeout 收紧。至于那些"多配几个备用总没坏处"的想法,在这条链路上恰恰相反——一个不通的备用,就是几秒钟的固定延迟。