华为云CCE-Pod-DNS-5秒超时根因复盘

举报
yd_221506059 发表于 2026/10/11 00:49:03 2026/10/11
【摘要】 线上 Java 服务每小时出现规律的 5 秒延迟尖刺,重启 Pod 只能安静一两小时。本文复盘定位过程:从 glibc 的 timeout:5 attempts:2 反推丢包模式,锁定 DNAT 后回包强依赖 conntrack、UDP 表项被 early_drop 淘汰导致静默丢包;ndots:5 又把一次解析放大成 4~10 次查询。文末给出修复方案与前后对比数据。

华为云 CCE 中 Pod 偶发 DNS 5 秒超时:一次从现象到根因的完整复盘

摘要:业务 P99 每小时出现几次规律性的 5 秒尖刺,压测复现不了,重启 Pod 就好一阵。本文不贴"改 resolv.conf 就好了"的结论,而是把内核 conntrack、glibc 解析器、kube-proxy DNAT 三条链路串起来,给出可复现的定位手段和修复前后数据。

一、现象:一个"重启就好"的幽灵故障

线上一个 Java 服务,日均调用量百万级,监控里出现了这样的特征:

  • P99 延迟出现 恰好 5 秒 的台阶,每小时 3~8 次,无规律
  • 应用日志偶发:java.net.UnknownHostException 或 lookup order-svc on 10.247.3.10:53: read udp 172.16.3.21:47xxx->10.247.3.10:53: i/o timeout
  • 重启 Pod 后安静一两个小时,之后复发
  • 用 wrk / JMeter 压测完全复现不出来

“重启就好” + "压测复现不了"这两个特征,基本可以排除业务代码,指向节点级共享状态。因为重启 Pod 只是换了 IP 和源端口,等于把状态重置了一遍。

二、先把 5 秒这个数字拆开

5 秒不是随便来的,它来自 glibc 解析器的默认参数:

/etc/resolv.conf
options timeout:5 attempts:2   # glibc 默认 RES_TIMEOUT=5, RES_DFLRETRY=2

含义是:向一个 nameserver 发查询,等 5 秒没回就重试一次,两次都失败才换下一个 nameserver。所以:

观测到的超时时长 推断
5 秒 第一次查询丢包,重试即成功
10 秒 同一 nameserver 两次都丢
20 秒 两个 nameserver 各丢两次

线上只出现 5 秒档,说明丢包是偶发的、非持续性的,重试能救回来。这排除了"CoreDNS 挂了"这类硬故障,问题在丢包路径上。

三、抓包:请求发出去了,回包没回来

在业务 Pod 所在节点上抓 DNS 流量:

# 节点上执行,注意容器网络在 veth 上,用 any 抓
tcpdump -i any -nn -s0 port 53 -w /tmp/dns.pcap

Wireshark 里过滤 dns,看到的模式是:

No.   Time      Source          Destination     Info
12    0.000000  172.16.3.21     10.247.3.10     Standard query A order-svc.default.svc.cluster.local
13    0.000012  172.16.3.21     10.247.3.10     Standard query AAAA order-svc.default.svc.cluster.local
      ↑ 两条查询同一源端口并发发出
      ↓ 5 秒内没有任何 response,随后客户端重发

两个关键点:

  1. A 和 AAAA 是并发查询、复用同一个源端口(glibc getaddrinfo 的默认行为)
  2. 回包完全没有出现在节点上——不是应用没收,是内核就没交给应用

注意:10.247.3.10 是 ClusterIP,不是 CoreDNS Pod 的真实 IP。这意味着流量要经过 kube-proxy 的 iptables DNAT,而 DNAT 的回包路径强依赖 conntrack 表项。

四、根因:三条链路叠在一起

根因 1:conntrack 表项被淘汰,回包变成"无主包"

链路是这样的:

Pod(172.16.3.21:47xxx) → ClusterIP(10.247.3.10:53)
   → iptables DNAT → CoreDNS Pod(172.16.3.88:53)
   → 回包 172.16.3.88 → 需要 conntrack 反向映射 → 才能改回 10.247.3.10 → 回到 Pod

UDP 是无连接的,回包能不能回去,全看 conntrack 里那条记录还在不在。而 UDP 表项的默认存活时间是:

$ sysctl net.netfilter.nf_conntrack_udp_timeout net.netfilter.nf_conntrack_udp_timeout_stream
net.netfilter.nf_conntrack_udp_timeout = 30
net.netfilter.nf_conntrack_udp_timeout_stream = 120

只要在 30 秒窗口内表项被提前淘汰(表满触发 early_drop)或者被覆盖(同一源端口并发两条查询,分别 DNAT 到不同的 CoreDNS Pod),回包就会因为找不到匹配项被内核直接丢弃。这类丢包在应用层看起来就是"查询石沉大海"。

验证:

# 表容量与当前用量,接近 1:1 就是高危
$ sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_count = 61402
net.netfilter.nf_conntrack_max = 65536

# 丢包计数器,insert_failed / early_drop 有增长即命中
$ conntrack -S
cpu=0  found=0 invalid=812 ignored=0 insert=0 insert_failed=37 drop=0 early_drop=41 ...
                                                       ↑ 就是它        ↑ 和它

为什么压测复现不了:压测用的是少量长连接、连接模式高度复用,conntrack 压力上不去;而线上是大量短连接 + 定时任务 + 健康检查长期累积,表项在 6 万上下贴着上限跑,偶发一次抖动就触发淘汰。

根因 2:ndots:5 把一次解析放大成 10 次

Kubernetes 默认给 Pod 注入 ndots:5,而 order-svc 只有 1 个点。glibc 会先把它当"不完整域名",逐个拼接 search 域去试:

order-svc.default.svc.cluster.local   ← 命中
order-svc.svc.cluster.local
order-svc.cluster.local
order-svc.default.svc.cluster.local.  ← 再补一次
...

一次业务解析实际发出 4~10 个 DNS 查询,而且是 A + AAAA 双份。查询量被放大了接近一个数量级,直接把 conntrack 表推向饱和——根因 1 和根因 2 是互相喂饭的关系。

根因 3:缓存穿透

CoreDNS 只有 2 副本,上游转发没做本地缓存优化。Pod 首次查询(冷缓存)要穿透到上游 DNS,这一段 RTT 抖动叠加在已经超时的路径上,把小概率丢包放大成了可见的 5 秒尖刺。

五、修复:按性价比排序

1. 部署 NodeLocal DNSCache(收益最大)

在 CCE 插件市场安装 NodeLocal DNSCache,节点上以 DaemonSet 形式跑本地缓存,Pod 的 DNS 请求优先走本机 169.254.20.10:

  • 请求不再经过 kube-proxy DNAT,直接绕开了 conntrack 这条脆弱链路
  • 本地缓存命中率高,穿透上游的次数大幅下降
  • 本地缓存对上游默认走 TCP,从机制上规避了 UDP 表项问题

装完在 Pod 里确认一遍:

$ cat /etc/resolv.conf
nameserver 169.254.20.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

2. 收敛 ndots

在 Deployment 里显式指定,或者代码里直接用 FQDN(结尾带点):

spec:
  template:
    spec:
      dnsConfig:
        options:
          - name: ndots
            value: "2"
// 尾点表示绝对域名,跳过 search 域展开
new URI("http://order-svc.default.svc.cluster.local.:8080/api");

3. 抬高 conntrack 上限并缩短 UDP 超时

# /etc/sysctl.d/99-conntrack.conf
net.netfilter.nf_conntrack_max = 262144
net.netfilter.nf_conntrack_udp_timeout = 10
net.netfilter.nf_conntrack_udp_timeout_stream = 60
sysctl --system

注意:nf_conntrack_max 越大,内存占用越高(每条表项约 300 字节),且必须小于 hashsize × 4,否则会打印 nf_conntrack: table full, dropping packet。

4. 应用侧减少解析次数

// JVM 默认 DNS 缓存只有 30 秒,短缓存会放大解析频率
// 启动参数或代码中设置
java.security.Security.setProperty("networkaddress.cache.ttl", "300");

对 Go 服务则相反:Go 默认使用纯 Go 解析器,没有 A/AAAA 并发查询的坑,但它不读 options ndots,行为需要单独验证。

六、效果验证

改动上线后观察一周:

指标 修复前 修复后
P99 延迟 5 秒尖刺 3~8 次/小时 0 次
单 Pod DNS QPS(均值) ~180 ~22
CoreDNS 上游转发 QPS ~9k ~1.1k
节点 conntrack 使用率 93% 31%
conntrack -S early_drop 持续增长 基本持平

DNS 查询量下降约 88%,主要来自 ndots 收敛 + 本地缓存命中,而不是靠加机器。

七、小结

这次排查真正有价值的三条经验:

  1. "重启就好"要优先怀疑节点级共享状态,而不是应用代码。Pod 重启换掉的是 IP 和源端口,等于重置了 conntrack。
  2. UDP 在 DNAT 场景下没有"连接"兜底,回包完全依赖 conntrack 表项,表满或表项冲突的后果就是静默丢包——应用层只能看到超时,看不到原因。
  3. 压测复现不了不等于不是性能问题,连接模式不同,压出来的路径根本不是出问题的那条。定位这类问题,抓包 + 内核计数器比加压力有效得多。

如果只记一句:Kubernetes 里 DNS 超时,先看 conntrack,再看 ndots。


建议标签:华为云、CCE、Kubernetes、CoreDNS、云原生

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。