WEBKT

彻底告别 5 秒延时:Kubernetes 集群 DNS 解析丢包与超时的终极解决方案

4 0 0 0

在 Kubernetes (K8s) 生产环境中,你是否遇到过这种诡异的性能瓶颈:平时接口响应极快,但在高并发场景下,偶尔会有个别请求的耗时精准地卡在 5 秒(或者 5 秒的倍数)上?

这并不是代码里写了 Thread.sleep(5000),也不是后端数据库慢查询,而是 K8s 生态中极其经典、甚至可以说是“臭名昭著”的 DNS 解析 5 秒延迟与超时问题

本文将带你从内核底层逻辑出发,深度剖析这个问题的根本成因,并给出生产环境落地验证过的四大终极解决方案。


一、 还原现场:为什么偏偏是 5 秒?

在 Linux 系统中,当程序尝试解析一个域名(例如通过 curl 访问外部 API)时,底层通常会调用 glibcgetaddrinfo() 函数。

默认情况下:

  1. 并发查询glibc 会同时发起 A 记录(IPv4 地址)和 AAAA 记录(IPv6 地址)的 DNS 查询。
  2. 5秒超时重试glibc 的 DNS 解析默认超时时间正是 5 秒(即 timeout:5)。

如果其中一个查询包在传输过程中丢失,解析端就会死等 5 秒,直到触发超时机制后进行重试,才能拿到解析结果。这就是“5 秒延迟”的由来。

但是在 K8s 集群内部,网络丢包为什么如此频繁?这涉及两个底层的技术缺陷。


二、 深度剖析:两大底层“罪魁祸首”

1. 内核级痛点:Conntrack(连接跟踪)冲突丢包

K8s 的 Service 默认采用 iptablesIPVS 规则进行流量转发,而这一切都依赖于 Linux 内核的 netfilterconntrack(连接跟踪)模块。

当两个线程通过同一个 Socket 同时发送 UDP 数据包(比如 A 记录和 AAAA 记录查询)时,问题就来了:

  1. 端口并发与竞争:两个 UDP 包需要通过 iptables 进行 SNAT/DNAT 转换。
  2. Conntrack 表项创建:内核尝试为这两个包在 conntrack 表中创建跟踪记录。由于它们源 IP、源端口、目的 IP、目的端口完全一致(只是 Payload 不同),在极短的时间差内,内核在执行 nf_conntrack_confirm(确认连接)时,会发生哈希冲突和竞争锁
  3. 内核直接丢包:后到达 confirm 阶段的那个 UDP 包会因为冲突而被内核直接丢弃(报 INSERT_FAILED 错误)。

UDP 是无连接的,丢了就丢了,没有任何重传机制,只能干等客户端自身的 glibc 5 秒超时后触发重试。

2. K8s 机制设计:ndots:5 的放大效应

K8s 容器默认的 /etc/resolv.conf 配置文件通常如下:

nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

这里的 ndots:5 意味着:如果查询的域名中包含的点(.)个数小于 5 个,DNS 解析器会优先在 search 域路径中依次拼接并尝试解析。

例如,你在容器内请求 baidu.com(点数小于 5):

  1. 拼接查询 baidu.com.default.svc.cluster.local. (不存在,报错)
  2. 拼接查询 baidu.com.svc.cluster.local. (不存在,报错)
  3. 拼接查询 baidu.com.cluster.local. (不存在,报错)
  4. 最终查询 baidu.com. (成功)

每一次拼接,都会同时发起 AAAAA 查询。原本只需要 2 次网络交互的请求,直接被放大到了 8 次。这不仅给 CoreDNS 带来了数十倍的并发压力,更极大地提高了触发上述 conntrack 冲突丢包的概率。


三、 四大终极解决方案(从标本兼治到架构演进)

针对上述成因,业界总结出了几套行之有效的解决方案。你可以根据集群的规模和运维能力选择最适合的一款。

方案一:使用 NodeLocal DNSCache(官方强烈推荐,一劳永逸)

这是目前 K8s 社区最推崇的架构级解决方案。其原理是在每个 Node 节点上以 DaemonSet 方式部署一个轻量级的 DNS 缓存代理(通常是 CoreDNS 实例)。

Pod  --> [本地 Loopback IP: 169.254.20.10 (NodeLocal DNS)] --(TCP/UDP)--> 物理网卡 --> CoreDNS Service

为什么它能解决 5 秒延迟?

  • 规避 Conntrack:Pod 直接向本节点上的虚拟 IP(默认 169.254.20.10)发起 DNS 请求,本地回环网络不经过物理网卡和 DNAT 转换,完全避开了 conntrack 冲突丢包的路径
  • 协议升级:NodeLocal DNSCache 与集群上游的 CoreDNS 之间默认使用 TCP 协议进行通信,TCP 本身具有重传机制,且没有 UDP conntrack 的并发限制。

部署方式:
官方提供了现成的清单文件,你可以下载并按需调整:

# 下载官方模板
wget https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml

# 替换模板中的占位符
# __PILLAR__DNS__SERVER__ 替换为你的 kube-dns Service IP(通常是 10.96.0.10)
# __PILLAR__LOCAL__DNS__ 替换为本地监听 IP(推荐 169.254.20.10)
# __PILLAR__DNS__DOMAIN__ 替换为集群域名(默认 cluster.local)

修改完成后执行 kubectl apply -f nodelocaldns.yaml 即可完成部署。

方案二:优化 Pod 的 resolv.conf 配置(单包规避)

如果你不方便在集群中部署 NodeLocal DNSCache,可以通过修改容器的 dnsConfig,强行改变 glibc 的解析行为。

1. 注入 single-request-reopen 参数

这个参数的作用是:当 A 查询和 AAAA 查询并发时,如果内核判定端口冲突,glibc 会关闭当前 Socket 并立即重新打开一个新 Socket 发送第二个请求。这样可以有效绕过连接跟踪表的并发锁竞争。

在 Deployment 的 YAML 中添加以下配置:

spec:
  template:
    spec:
      dnsConfig:
        options:
          - name: single-request-reopen
2. 注入 use-vc 强制使用 TCP

既然 UDP 有连接跟踪冲突,那就强制 DNS 解析使用 TCP 协议。TCP 连接具有状态窗口,不会在内核中引发上述 UDP conntrack 覆盖问题。

spec:
  template:
    spec:
      dnsConfig:
        options:
          - name: use-vc

注意:强制 TCP 会增加三步握手的时延,在高并发小请求场景下可能带来轻微的基准耗时上升,需压测评估。

方案三:合理缩减 ndots 级数(治本之策,减少无用查询)

如果你的应用程序大量访问外部域名(如各类第三方 API、云服务),你应该通过减小 ndots 的数值,直接跳过 K8s 内部 Search 域的盲目拼接。

spec:
  template:
    spec:
      dnsConfig:
        options:
          - name: ndots
            value: "2"
  • 原理解析:当 ndots 设为 2 时,像 api.github.com(含有 2 个点)这样的域名,解析器会直接当成绝对域名(Absolute Domain)去公网查询,不再拼接 cluster.local,查询次数直接从 8 次骤降到 2 次。
  • 避坑指南:一旦调低 ndots,如果你的 Pod 想通过短域名访问集群内的其他 Service(例如通过 mysql 访问 mysql.default.svc.cluster.local),解析可能会失败。此时你必须改用全限定域名(FQDN),如 mysql.default

方案四:在业务层或框架层实现绝对路径查询

最简单、非侵入基础设施的改动,是在代码中请求外部域名时,在域名末尾强行加上一个点(.

例如,不要访问 http://api.foo.com/v1/user,而是访问 http://api.foo.com./v1/user

  • 原理:末尾带点的域名在 DNS 规范中被称为绝对域名/根域名glibc 看到末尾有英文句号,会判定其已经达到了最顶级,从而完全忽略 ndotssearch 路径配置,直接对 api.foo.com 发起单次解析。
  • 优点:零侵入、零配置、无副作用。

四、 总结与最佳实践建议

5 秒延时问题本质上是 Linux 内核在 UDP SNAT 处的物理缺陷K8s 默认 DNS 搜索机制 共同作用导致的。

在实际生产治理中,建议采用以下进化路径:

  1. 临时救急:如果线上正在报警,立刻修改业务请求代码,在外部域名末尾加上点(.,或者在 Pod 的 Deployment 模板中注入 single-request-reopen 配置。
  2. 中短期优化:根据业务特点,评估是否有必要降低 ndots 值为 2 甚至 1,减少 CoreDNS 整体负载。
  3. 长期架构演进:对于大中型、高并发集群,无脑部署 NodeLocal DNSCache 是唯一的标准答案。它不仅能根治 5 秒超时,还能降低 70% 以上的 CoreDNS 资源开销,极大提升集群整体的吞吐量与稳定性。
SRE铁哥 KubernetesCoreDNS网络优化

评论点评