K8s Pod 里 DNS 解析偶发 5 秒超时?从 ndots 到 conntrack 的排查实战
现象:偶发 5 秒,重试就好
线上 Go 服务在 K8s 里跑得好好的,但每天总有几十次请求耗时精确卡在 5.00s,日志里躺着 dial tcp: lookup mysql-prod.internal on 10.96.0.10:53: read udp ...: i/o timeout 或 Python 的 getaddrinfo EAI_AGAIN。诡异的是:同一个 Pod 里 curl 10.20.3.11:3306 一秒通,业务高峰期问题明显,低峰期几乎不出现。第一次遇到很容易往"数据库慢"或"网络丢包"上猜,其实八成的锅在 DNS。
5 秒不是玄学。glibc 的 resolver 默认 timeout:5 attempts:2(容器里 /etc/resolv.conf 常常就这一行),一次 UDP 查询没响应就等 5 秒才重发,所以任何"只丢了第一个包"的场景,表现都是整齐的 5 秒。
第一步:先把 resolv.conf 看清楚
kubectl exec -it deploy/api -- cat /etc/resolv.conf
# nameserver 10.96.0.10
# search api.svc.cluster.local svc.cluster.local cluster.local corp.internal
# options ndots:5
重点是 ndots:5 这一行。它表示:只要域名里的点少于 5 个,就先把这个域名挨个拼上 search 后缀去查,全部失败才查原名。所以查一个 mysql-prod.internal(1 个点)实际会发出 5 次 查询:
mysql-prod.internal.api.svc.cluster.local -> NXDOMAIN
mysql-prod.internal.svc.cluster.local -> NXDOMAIN
mysql-prod.internal.cluster.local -> NXDOMAIN
mysql-prod.internal.corp.internal -> NXDOMAIN
mysql-prod.internal -> 命中
连接池里的长连接断了重连、每次 gRPC 调用都新建 http.Client,这些 NXDOMAIN 就在几毫秒内砸向 CoreDNS。CoreDNS 单副本扛不住时排队超时,或者上游 corp.internal 转发慢,第一个 UDP 包被丢掉,glibc 就干等 5 秒。
第二步:区分是"慢"还是"丢"
在 Pod 里直接对比三种查法的耗时,能立刻定性:
# 带 search 展开(默认行为),看总耗时
time nslookup mysql-prod.internal
# 关掉 search,直查全名,排除 ndots 影响
time dig +search +ndots=2 mysql-prod.internal @10.96.0.10 | grep -E "Query time|status"
# 重复 50 次统计,找出长尾
for i in $(seq 50); do dig +tries=1 +time=2 api.internal @10.96.0.10 | grep "Query time"; done \
| awk '{print $4}' | sort -n | tail -5
如果 +ndots=2 之后耗时骤降、长尾消失,问题就在 ndots 放大;如果单次直查也有 5s 长尾,往 CoreDNS 或 conntrack 上查。
第三步:抓包看丢在哪一跳
# Pod 内抓 DNS,看有没有发出、有没有回包
kubectl exec -it deploy/api -- sh -c \
'tcpdump -i any -nn "udp port 53" -c 40 -w /tmp/dns.pcap'
kubectl cp default/<pod>:/tmp/dns.pcap ./dns.pcap
tshark -r dns.pcap -Y "dns" -T fields -e frame.time_relative -e ip.src -e dns.qry.name -e dns.flags.rcode
看到"请求出了、回包没回来",基本都是 UDP conntrack 五元组冲突:短连接高频发 DNS,内核 NAT 表里同源端口复用了同一个五元组,回包被 DNAT 到别的 Pod。宿主机上验证:
conntrack -S | grep insert_failed
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
insert_failed 持续增长、conntrack_count 逼近 max,就是它了。
第四步:CoreDNS 侧有没有过载
kubectl -n kube-system get pods -l k8s-app=kube-dns -o wide
kubectl -n kube-system top pod -l k8s-app=kube-dns
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=200 | grep -iE "error|timeout|refused"
打开 Corefile 的 log、errors、cache 30 之后,日志里出现大量 plugin/forward: no upstream host 或 i/o timeout,说明上游 forward . /etc/resolv.conf 指向的节点 DNS 不稳。这时把上游换成两个可用的解析器,并给 cache 加上拒绝缓存:
kubectl -n kube-system edit cm coredns
# .:53 {
# errors
# health
# ready
# kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure; fallthrough in-addr.arpa ip6.arpa }
# cache 30
# forward . 100.100.2.136 100.100.2.138 { max_concurrent 1000 }
# reload
# }
五、治理方案:从"能查"到"快查"
1)收敛 ndots。内部短名用 svc.cluster.local 全名,其余场景把 ndots 降到 2,查询量直接砍掉 4/5:
spec:
template:
spec:
dnsConfig:
options:
- name: ndots
value: "2"
- name: timeout
value: "2"
- name: attempts
value: "2"
2)上 NodeLocal DNSCache。每个节点跑一个 DNS 缓存,走本机 169.254.20.10,把跨节点 UDP 变成 loopback,顺手绕开 conntrack 竞争:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/website/main/content/en/examples/admin/dns/nodelocaldns.yaml
kubectl -n kube-system get ds nodelocaldns
# 之后新建的 Pod resolv.conf 里 nameserver 应变成 169.254.20.10
3)应用层别每次请求都解析。Go 里复用一个 http.Client,或用 net.Resolver 配 PreferGo: true 并设置 Dial 超时;数据库客户端设 connectTimeout=2s 而不是默认无穷等。
4)容量。集群规模超过 1000 Pod 或 QPS 上 10 万,CoreDNS 至少 3 副本并打反亲和,--max-concurrent 调高,避免单点排队。
效果验证
# 改造前 CoreDNS 每秒查询数
kubectl -n kube-system logs -l k8s-app=kube-dns --since=1m | grep -c "A IN"
# 改造后再采一次,正常应下降 60%~80%
# 业务侧看 P99 与超时计数
kubectl exec -it deploy/api -- sh -c 'grep -c "lookup .* i/o timeout" /var/log/api.log'
我们在一个 800 节点的集群上做过这组操作:ndots 5→2 加 NodeLocal DNSCache,CoreDNS 峰值 QPS 从 4.2 万降到 9 千,接口 5 秒超时从每天 60+ 次降到 0。DNS 这类问题看着玄,其实只要抓住 resolv.conf、抓包、conntrack 三个入口,半小时内必能定位。
- 点赞
- 收藏
- 关注作者
评论(0)