Pod 里解析个域名要 5 秒:ndots、search 域与 conntrack 丢包
定时任务偶发 dial tcp i/o timeout,耗时正好 5 秒。ndots:5 让短名字走五次查询,节点 conntrack 表满把 UDP 包丢了,最后用 NodeLocal DNSCache 收尾。
有个定时任务每隔十几分钟就会失败一次,日志里只有一句:
Get "http://report-api:8080/v1/daily": dial tcp: lookup report-api on 10.96.0.10:53: read udp 10.0.3.77:41288->10.96.0.10:53: i/o timeout
失败不规律,重试就好,kubectl exec 进去 curl 同一个地址又是通的。最迷惑的是同一时刻同一个节点上,另一个 Pod 调同样的服务完全正常。
我先怀疑是 CoreDNS 副本不够,看了下是 3 副本,CPU 不到 10% 的 limit 的 5%。又怀疑 DNS 缓存问题,改成走 Service 的 FQDN 也照样失败。真正把我拉到正确方向的,是在报错 Pod 里跑了一次 dig,看到查询耗时是 5 秒整。
一、5 秒这个数字是会说话的
DNS 解析超时不是随机数,它由两个参数决定:timeout 和 attempts。/etc/resolv.conf 里默认是 options ndots:5 timeout:5 attempts:2,glibc 的行为是每个 search 域依次尝试,单个请求超时 5 秒。
# 在故障 Pod 里执行
cat /etc/resolv.conf
# nameserver 10.96.0.10
# search default.svc.cluster.local svc.cluster.local cluster.local
# options ndots:5
time nslookup report-api
# Server: 10.96.0.10
# Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
# ... 5.0 秒后 ...
ndots:5 的意思是:如果查询的名字里点的数量少于 5 个,就先当成相对名,依次拼接 search 域去查。report-api 里一个点都没有,0 < 5,所以 glibc 会依次查:
report-api.default.svc.cluster.localreport-api.svc.cluster.localreport-api.cluster.localreport-api- 如果前四个都是 NXDOMAIN,才会用原始名字查一次
前三次必然 NXDOMAIN(因为服务在 prod 命名空间),第四次其实是集群外的公网域名解析,也是 NXDOMAIN。只有第五次才命中——不对,实际上目标是 report-api.prod.svc.cluster.local,如果 Pod 和 Service 不在同一个 namespace,search 域里根本拼不出正确结果,得靠第四步之后的绝对名查询。
这就是问题所在:正常情况下这些 NXDOMAIN 返回极快,几毫秒就结束,所以看不出问题;一旦某个 NXDOMAIN 请求的 UDP 包丢了,glibc 不会立刻重试,它会老实等满 5 秒。
二、UDP 丢在哪:conntrack 表满
内网丢包不是常态,但在 DNAT 场景下有个经典来源:nf_conntrack。K8s 的 Service 靠 iptables 做 DNAT,每个连接都要占一条 conntrack 记录;CoreDNS 走 UDP,每条 DNS 请求也会占一条,并且 UDP 的 conntrack 记录默认要等 30 秒才过期。
# 节点上查看 conntrack 使用率
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
dmesg -T | grep -i "conntrack: table full"
# 输出示例
# nf_conntrack: table full, dropping packet
count 到了 max 的 98%,dmesg 里有对应的丢弃日志。表满时内核直接丢包,不返回任何 ICMP,glibc 只能干等到超时。这解释了为什么「偶发」:只有当某个节点上的 conntrack 恰好被写满时,落在那台机器上的 Pod 才会出现 5 秒超时。
我们的 8 核节点 nf_conntrack_max 还是默认的 65536,高峰期对外连接多、Pod 又多,很容易顶到上限。
三、先把 ndots 调小:最便宜的收益
治本之前,先把无谓的查询次数砍掉。有两种做法,效果不一样。
第一种是应用侧写全限定名,末尾带点,这样 glibc 直接跳过 search 列表:
# 不推荐在代码里硬编码,但可以用于验证
env:
- name: REPORT_API_URL
value: "http://report-api.prod.svc.cluster.local.:8080"
末尾那个点很关键,没有点还是会被 search 域污染。实测把 ndots 的影响完全绕开了,一次查询 2ms 命中。
第二种是在 Pod 模板里声明 dnsConfig,把 ndots 降到 2:
apiVersion: apps/v1
kind: Deployment
metadata:
name: report-worker
spec:
template:
spec:
dnsPolicy: ClusterFirst
dnsConfig:
options:
- name: ndots
value: "2"
- name: timeout
value: "2"
- name: attempts
value: "2"
containers:
- name: worker
image: registry.internal/report-worker:1.8.3
ndots:2 之后,report-api.prod.svc.cluster.local(4 个点)会被当成绝对名直接查;report-api(0 个点)仍然走 search,但次数从 5 降到 3。绝大多数线上服务名都带了命名空间,受益明显。
timeout:2 把单个请求的等待时间从 5 秒压到 2 秒,attempts 保持 2。最坏情况下的总耗时从 5 秒降到 4 秒,而且正常路径几乎感知不到。
四、NodeLocal DNSCache:把查询留在节点上
ndots 只是减少了查询次数,没解决「包丢了就干等」的问题。真正让 DNS 变稳的是 NodeLocal DNSCache:在每个节点上跑一个 DNS 缓存,Pod 的 nameserver 指向本机 169.254.20.10,命中缓存直接返回,未命中才由它转发给 CoreDNS。
# 部署官方清单(注意 kube-proxy 模式与集群 CIDR)
kubectl apply -f https://raw.githubusercontent.com/kubernetes/kubernetes/v1.29.0/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml
# 校验:Pod 的 resolv.conf 应该指向 169.254.20.10
kubectl exec -it report-worker-6f8d9c7b5-x2m4p -- cat /etc/resolv.conf
好处有三层:本机缓存把 P99 从毫秒级抖动压到微秒级;UDP 查询不出节点,绕开了 conntrack 与跨节点丢包;CoreDNS 的 QPS 直接掉一个数量级,副本压力也小了。
代价是多了一个 DaemonSet,以及 169.254.20.10 这个 link-local 地址在某些 CNI 下需要额外放行。上线前要在预发验证一遍,尤其是用了 NetworkPolicy 的集群。
五、conntrack 该调多大
顺带把 conntrack 的上限调上去,这是节点级参数,改完要观察内存:
# 8 核 16G 节点,按每条约 300 字节估算
sysctl -w net.netfilter.nf_conntrack_max=262144
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=600
sysctl -w net.netfilter.nf_conntrack_udp_timeout=10
sysctl -w net.netfilter.nf_conntrack_udp_timeout_stream=30
nf_conntrack_max 不能无限调大,每条记录都占内核内存,26 万条大约 80 MB,可以接受。udp_timeout 从 30 秒降到 10 秒能明显减少 UDP 记录的堆积,对 DNS 这种短交互是安全的。
改完要写进 /etc/sysctl.d/,否则节点重启就回默认值——这个坑我们踩过一次,重启后第二天又开始偶发超时。
六、验证与监控
改动的验证不能靠「感觉好了」,要能对比:
| 指标 | 改造前 | 改造后 |
|---|---|---|
nslookup 平均耗时 |
8–5200ms | 1–3ms |
| 单次调用 DNS 查询数 | 5 | 1(全限定名) |
| CoreDNS QPS(峰值) | 14 000 | 1 900 |
nf_conntrack_count / max |
96% | 31% |
| 定时任务失败率 | 每 2 小时 1–2 次 | 7 天 0 次 |
监控上加了两个指标:CoreDNS 的 coredns_dns_request_duration_seconds P99,以及节点 node_nf_conntrack_entries / node_nf_conntrack_entries_limit。前者超过 50ms 就该看 CoreDNS 了,后者超过 70% 要提前扩容或清理长连接。
还有一个容易被忽略的细节:如果应用用了连接池或 HTTP 客户端的长连接,DNS 只在建连时解析一次,后续复用连接就不会再查 DNS。这类应用的 DNS 问题往往表现为「启动时慢、运行中正常」,排查时不要只盯运行期日志。
小结
dial tcp: lookup ... i/o timeout且耗时正好 5 秒,先怀疑ndots:5导致的多次 search 查询加上单次丢包。ndots:5会让短名字走 5 次查询,跨命名空间时前面的 NXDOMAIN 全是无用功。- UDP 丢包最常见的原因是节点
nf_conntrack表满,dmesg里会有table full, dropping packet。 - 应用侧写带尾点的全限定名最直接;Pod 模板里设
ndots:2、timeout:2是通用解法。 - NodeLocal DNSCache 把查询留在节点内,同时降低 CoreDNS 压力,是长期方案。
nf_conntrack_max调大后要落到sysctl.d,否则重启回滚,问题会再次出现。
正在加载评论…