本文针对Linux服务器上的curl接口偶发卡顿问题进行了排查,发现原因多与DNS解析和systemd-resolved服务有关。主要介绍了两个问题:glibc并发查询导致的NAT防火墙冲突以及systemd-resolved服务成为瓶颈,并提出了相应的解决方法。
之前给客户排查过一个很邪门的线上问题。一个跑在云服务器上的 Go 后端服务,调用第三方支付接口和推送接口时,平时耗时都在 20 毫秒左右,但每天总有几十次请求会硬生生卡住 5 秒多。日志里能看到,业务逻辑本身没耗时,网络传输也没超时,但整个 HTTP 调用的耗时曲线就像突然被针扎了一下,直接飙到 5000 毫秒多一点。
最开始同事怀疑是对方服务不稳定,或者机房链路有抖动。后来把调用链路细化之后,把具体耗时拆开看,才发现时间全消耗在操作系统本身的 DNS 域名解析上,对端的网络和业务接口一点问题都没有。
如果你也在 Linux 服务器上遇到过类似的偶发 5 秒超时,或者容器里调外部 API 经常慢半拍,大概率是碰上了下面这几个 Linux 域名解析的经典坑。
现象定位:怎么确认耗时真的卡在 DNS 上?
排查这类偶发网络卡顿,最忌讳凭感觉调参。要确定到底是不是 DNS 的问题,用 curl 自带的格式化输出就能抓个现行:
curl -w "DNS 解析耗时: %{time_namelookup}s\nTCP 建立耗时: %{time_connect}s\nTLS 握手耗时: %{time_appconnect}s\n总耗时: %{time_total}s\n" -o /dev/null -s https://api.example.com
当时我们写了个简单脚本,在后台每隔 2 秒测一次。跑了不到 10 分钟,抓到了一条异常记录:
DNS 解析耗时: 5.004s
TCP 建立耗时: 5.019s
TLS 握手耗时: 5.038s
总耗时: 5.062s
可以看到,TCP 连接和 TLS 握手只花了十几毫秒,但 time_namelookup 占满了 5 秒整。这个刚好 5 秒的数字非常关键,因为在 Linux 标准的 C 运行库 glibc 里,默认的 DNS 查询超时时间正好就是 5 秒。
一旦确认是 DNS 的锅,排查方向就清晰了。
坑一:glibc 并发查询把 NAT 防火墙干懵了
这是最常见、也最隐蔽的一个坑。
Linux 解析一个域名时,通常不仅需要获取 IPv4 地址(A 记录),还要获取 IPv6 地址(AAAA 记录)。在旧版 glibc 时代,解析器是串行发请求的,先查 A 记录,拿到返回再查 AAAA 记录。后来为了提升速度,glibc 改成了并行查询:同时发出 A 记录和 AAAA 记录的两个 UDP 数据包。
问题出在实现方式上。glibc 在发送这两个包时,用的是同一个 UDP socket,这意味着两个包的源端口是一模一样的。
很多云厂商的 NAT 网关、安全组规则,或者硬件防火墙,在处理来自同一个内部 IP、同一个源端口、同一个目标 IP 和目标端口的并发 UDP 包时,状态跟踪表(conntrack)会产生冲突。结果就是第一个 A 记录请求发出去了,紧随其后的 AAAA 记录请求被防火墙悄悄丢弃了,甚至两只包都被判定为异常连接而直接 drop。
客户端没收到响应,只能傻等。glibc 的默认超时是 5 秒,等到 5 秒倒计时结束,它才触发重试。第二次发包成功了,于是整次请求的耗时就变成了 5 秒零十几毫秒。
怎么验证与解决?
在机器上抓包可以看得很清楚:
tcpdump -nn -i any udp port 53 -vv
你会看到发出两条并行的 DNS query,但只收到了一条 reply,另一条在第 5 秒整重新发送。
修复方案是在 /etc/resolv.conf 中加入选项:
options single-request-reopen
这个参数的作用是告诉解析器:发送完第一个查询后,关闭当前的 socket,重新打开一个新的 socket、换一个新的源端口来发第二个查询。这样 NAT 网关就不会误杀数据包了。
如果你的系统是特别老的 Linux 内核(CentOS 6 时代),也可以用 options single-request,强制回退到串行查询。但在现代 Linux 发行版上,统一使用 single-request-reopen 最省心。
坑二:systemd-resolved 成了隐形瓶颈
从 Ubuntu 18.04 开始,很多发行版默认启用 systemd-resolved 服务,/etc/resolv.conf 不再是真实的配置文件,而是一个指向 /run/systemd/resolve/stub-resolv.conf 的软链接,里面的 nameserver 填的是本机的 127.0.0.53。
这个机制初衷是提供本机 DNS 缓存和跨接口调度,但在实际生产环境里,经常引发两个麻烦:
- DNSSEC 校验失败超时:很多企业内网或者自建 DNS 并不支持 DNSSEC。systemd-resolved 默认可能会尝试做安全验证,上游返回 NXDOMAIN 或不匹配时,它会不断尝试降级重试,白白消耗上百毫秒甚至几秒。
- 多网卡上游探测打架:当服务器有内网网卡和外网网卡、或者装了虚拟网卡时,systemd-resolved 会把各个网卡的 DNS 服务混合起来探测。一旦某个内网 DNS 解析外部域名特别慢或者超时,systemd-resolved 就会在多个 nameserver 之间来回串行试探。
怎么排查?
先检查 systemd-resolved 当前的运行状态和统计:
resolvectl status
resolvectl statistics
看看当前使用的上游 DNS 是哪个,缓存命中率是多少。
如果要彻底避免它搞事情,通常有两个办法:
如果不需要本机复杂的按域名分流,最稳妥的做法是在 /etc/systemd/resolved.conf 明确关掉 DNSSEC,并指定靠谱的公共 DNS:
[Resolve]
DNS=223.5.5.5 119.29.29.29
FallbackDNS=114.114.114.114
DNSSEC=no
Cache=yes
修改后重启服务:
systemctl restart systemd-resolved
有些做高并发网关的团队甚至更彻底,直接停用 systemd-resolved,把 /etc/resolv.conf 的软链接删除,换成纯静态文本文件直接对接云厂商内网 DNS。
坑三:/etc/resolv.conf 缺少超时和重试控制
很多 Linux 服务器上,/etc/resolv.conf 只写了两个 nameserver,下面没有任何 options 配置:
nameserver 8.8.8.8
nameserver 1.1.1.1
默认情况下,glibc 的 DNS 解析策略有两个默认值:
timeout: 默认为 5 秒。attempts: 默认为 2 次。
假设配置的第一个 nameserver 偶尔抽风或遇到网络抖动,系统必须傻等整整 5 秒,确认第一个 nameserver 没反应之后,才会转头去查第二个 nameserver。
对于网页或者 API 接口来说,等待 5 秒已经意味着用户端大概率直接点了刷新或者触发了前端网关的 504 错误。
解决办法
在 /etc/resolv.conf 明确加上超时、重试和轮询配置:
options timeout:1 attempts:2 rotate single-request-reopen
这里的含义是:
timeout:1:单次查询超过 1 秒没返回,立刻判定超时,不要傻等 5 秒。attempts:2:最多重试 2 次。rotate:在配置的多个 nameserver 之间轮流查询,分散压力,避免把所有流量全打在第一个 server 上。single-request-reopen:避开前面提到的 NAT 丢包问题。
加上这一行之后,即使某个上游 DNS 节点挂了,客户端在 1 秒内就会切换到备用 DNS,业务感知到的毛刺会从 5 秒大幅缩减到 1 秒以内。
坑四:Docker 与 Kubernetes 里的 ndots:5 查询风暴
如果你是在容器或者 K8s 集群里调外部接口感觉慢,八成是碰到了 ndots:5 的问题。
在 Kubernetes Pod 的 /etc/resolv.conf 里,通常默认有这么两行:
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
Linux 解析域名时,如果域名里的点号(dot)数量少于 ndots 设定的值,解析器就会优先认为这是一个集群内部的短域名,先把 search 域拼接在后面进行查询。
比如你要请求 api.wechat.com,里面只有 2 个点,小于 5。
容器里的解析器就会老老实实按顺序去问 CoreDNS:
api.wechat.com.default.svc.cluster.local(返回 NXDOMAIN)api.wechat.com.svc.cluster.local(返回 NXDOMAIN)api.wechat.com.cluster.local(返回 NXDOMAIN)- 最后才真正发起对外部域名
api.wechat.com的查询。
原本一次 DNS 查询就能搞定,凭空放大了 4 倍,既拖慢了应用响应,又把集群的 CoreDNS 给打爆了。
解决办法
有两个改法:
第一种是在代码或者配置里,把外部域名的末尾显式加一个点,写成绝对完整域名(FQDN):
例如把 https://api.wechat.com 改成 https://api.wechat.com.。解析器看到末尾有点,知道这是根域,就不会再往后面拼接 search 域了。
第二种是在 Deployment 的配置里修改 Pod 的 dnsConfig,把 ndots 调小:
spec:
dnsConfig:
options:
- name: ndots
value: "2"
个人在生产服务器上的排查与配置清单
每次接手新的 Linux 服务器,我都会习惯性用下面这套流程过一遍网络配置:
第一步,抓包确认是否有单端口并发丢包:
tcpdump -nn -i any udp port 53
第二步,检查 /etc/resolv.conf 的 options 配置。如果使用的是云厂商提供的 ECS,优先使用云厂商内网提供的私网 DNS(内网解析不仅延迟低至 1-2 毫秒,而且没有外网流量开销与限制),并在下方追加安全选项:
nameserver 100.100.2.136
nameserver 100.100.2.138
options timeout:1 attempts:2 rotate single-request-reopen
第三步,检查 /etc/nsswitch.conf 中的解析顺序。确保 hosts 这一行配置清晰:
hosts: files dns
有些桌面或多用途 Linux 发行版会在这里挂载 mdns4_minimal [NOTFOUND=return],导致非 .local 的域名也可能先去跑局域网组播查询,空等几百毫秒才超时回退到常规 DNS。去掉多余的插件,只保留 files 和 dns,解析流程最轻快。
网络排错很多时候看起来毫无规律,偶发 5 秒、10 秒的超时尤其折磨人。顺着抓包和协议实现追下去,根因往往就藏在这些容易被忽略的系统底层参数里。
文章标题:服务器 curl 接口偶发卡顿 5 秒?排查 Linux DNS 解析慢与 systemd-resolved 我踩过的几个坑
文章链接:https://llbbs.cn/jishujaocheng/202.html
本站文章均为原创,未经授权请勿用于任何商业用途
评论一下?