华为云CCE-Pod-DNS-5秒超时根因复盘
华为云 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,随后客户端重发
两个关键点:
- A 和 AAAA 是并发查询、复用同一个源端口(glibc
getaddrinfo的默认行为) - 回包完全没有出现在节点上——不是应用没收,是内核就没交给应用
注意: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 收敛 + 本地缓存命中,而不是靠加机器。
七、小结
这次排查真正有价值的三条经验:
- "重启就好"要优先怀疑节点级共享状态,而不是应用代码。Pod 重启换掉的是 IP 和源端口,等于重置了 conntrack。
- UDP 在 DNAT 场景下没有"连接"兜底,回包完全依赖 conntrack 表项,表满或表项冲突的后果就是静默丢包——应用层只能看到超时,看不到原因。
- 压测复现不了不等于不是性能问题,连接模式不同,压出来的路径根本不是出问题的那条。定位这类问题,抓包 + 内核计数器比加压力有效得多。
如果只记一句:Kubernetes 里 DNS 超时,先看 conntrack,再看 ndots。
建议标签:华为云、CCE、Kubernetes、CoreDNS、云原生
- 点赞
- 收藏
- 关注作者
评论(0)