ᕕ( ᐛ )ᕗ Jimyag's Blog

Kubernetes ClusterFirst 下 DNS search 查询行为分析

Last modified:

在 Kubernetes 里,Pod 默认使用 dnsPolicy: ClusterFirst。很多人对它的直觉是:Pod 的 DNS 会优先走集群 DNS,外部域名再转发到上游 DNS。

这个理解大体没错,但容易漏掉一个细节:ClusterFirst 并不等于完全忽略节点上的 /etc/resolv.conf。kubelet 会把节点 resolver 配置作为基础配置读取进来,然后对普通 Pod 重新生成一份 DNS 配置。

其中,nameserver 通常会被替换成集群 DNS Service IP;但节点上的 search 域可能会被合并进 Pod 的 search list。再加上 Kubernetes 默认的 options ndots:5,一个看起来已经是完整域名的外部地址,也可能先被扩展成多个 search 查询。

现象

假设节点上的 /etc/resolv.conf 曾经有类似配置:

1
2
3
4
5
nameserver 127.0.0.1
nameserver 10.0.0.1
nameserver 10.0.0.2
search example.internal
options timeout:1 attempts:1 single-request-reopen

业务在 Pod 里访问一个外部域名:

1
service-a.region.example.com

抓包或查看 CoreDNS 日志时,可能会看到类似查询:

1
2
3
4
5
service-a.region.example.com.<namespace>.svc.cluster.local
service-a.region.example.com.svc.cluster.local
service-a.region.example.com.cluster.local
service-a.region.example.com.example.internal
service-a.region.example.com

如果把节点上的 search example.internal 注释掉,重建 Pod 后,*.example.internal 这类扩展查询消失。这说明扩展查询来自 Pod 内 resolver 的查询名生成逻辑。

但这还不是完整结论。search 本身是正常能力,真正的问题通常发生在下一步:拼接出来的名字本该返回 NXDOMAIN,却被某个 DNS 代理、wildcard 记录或 fake-ip 机制返回了 A 记录。resolver 收到 A 记录后会认为解析成功,后续就不会再查原始域名。

NXDOMAIN 是 DNS 返回码,含义是「这个域名不存在」。对 resolver 来说,这是一个很重要的信号:当前候选名不存在,可以继续尝试 search list 里的下一个候选名,或者最后回到原始查询名。

ClusterFirst 做了什么

Kubernetes 的 DNS 配置由 kubelet 在创建 Pod sandbox 时传给容器运行时。对 ClusterFirst Pod,kubelet 的处理逻辑可以简化成三步:

  1. 读取 kubelet 配置里的 resolvConf 文件,通常是节点 /etc/resolv.conf
  2. 把 Pod 的 nameserver 设置为 --cluster-dns 指定的集群 DNS。
  3. 生成 Pod 的 search list:先放 Kubernetes 集群内搜索域,再追加节点 resolv.conf 里的 search 域。

Kubernetes 源码里的关键函数是 generateSearchesForDNSClusterFirst

1
2
clusterSearch := []string{nsSvcDomain, svcDomain, c.ClusterDomain}
return omitDuplicates(append(clusterSearch, hostSearch...))

也就是:

1
2
3
4
<namespace>.svc.<cluster-domain>
svc.<cluster-domain>
<cluster-domain>
<host-search-from-node-resolv-conf>

最终 Pod 里可能看到:

1
2
3
nameserver 10.96.0.10
search app.svc.cluster.local svc.cluster.local cluster.local example.internal
options ndots:5

这里要注意:节点里的 nameserver 10.0.0.1 不会直接变成普通 ClusterFirst Pod 的 nameserver。Pod 发 DNS 请求的目标通常还是 CoreDNS。但是节点里的 search example.internal 会影响 Pod 生成哪些查询名。

  flowchart TD
    NodeConf["Node /etc/resolv.conf\nnameserver 10.0.0.1\nsearch example.internal"]
    Kubelet["kubelet DNS Configurer"]
    ClusterDNS["clusterDNS\nCoreDNS Service IP"]
    PodConf["Pod /etc/resolv.conf\nnameserver CoreDNS\nsearch ns.svc cluster + example.internal\noptions ndots:5"]

    NodeConf -->|"parse hostSearch"| Kubelet
    ClusterDNS -->|"replace nameserver"| Kubelet
    Kubelet -->|"write DNS config for Pod sandbox"| PodConf

search 和 nameserver 不是二选一

这里最容易混淆的是 searchnameserver 的关系。

search 决定「要查哪些名字」。

nameserver 决定「把这些查询发给谁」。

所以不是「优先走 search,而不是 nameserver」。准确说是:

  1. 应用调用 resolver 解析一个名字。
  2. resolver 根据 searchndots 生成一组候选查询名。
  3. resolver 把这些候选查询名依次发给 nameserver

在 ClusterFirst Pod 里,这个 nameserver 通常是 CoreDNS:

1
应用 -> Pod resolver -> CoreDNS -> 上游 DNS

对于非 Kubernetes 集群域名,CoreDNS 默认 Corefile 里通常有:

1
forward . /etc/resolv.conf

这表示 CoreDNS 自己无法处理的查询会继续转发给它配置的上游 resolver。

  sequenceDiagram
    participant App as App
    participant Resolver as Pod resolver
    participant CoreDNS as CoreDNS
    participant Upstream as Upstream DNS

    App->>Resolver: resolve service-a.region.example.com
    Resolver->>Resolver: ndots:5, dots=3, expand with search list
    Resolver->>CoreDNS: query service-a.region.example.com.app.svc.cluster.local
    CoreDNS-->>Resolver: NXDOMAIN
    Resolver->>CoreDNS: query service-a.region.example.com.svc.cluster.local
    CoreDNS-->>Resolver: NXDOMAIN
    Resolver->>CoreDNS: query service-a.region.example.com.cluster.local
    CoreDNS-->>Resolver: NXDOMAIN
    Resolver->>CoreDNS: query service-a.region.example.com.example.internal
    CoreDNS->>Upstream: forward non-cluster query
    Upstream-->>CoreDNS: NXDOMAIN or A record
    CoreDNS-->>Resolver: response
    alt search result is A record
        Resolver-->>App: return search-expanded result
    else search result is NXDOMAIN
        Resolver->>CoreDNS: query service-a.region.example.com
        CoreDNS->>Upstream: forward
        Upstream-->>CoreDNS: answer
        CoreDNS-->>Resolver: answer
        Resolver-->>App: resolved original name
    end

原因是 ndots:5

Linux resolver 的规则是:如果待解析名字里的点数量少于 ndots,就先尝试追加 search list。Kubernetes 默认给 ClusterFirst Pod 设置:

1
options ndots:5

例如:

1
service-a.region.example.com

这个名字只有 3 个点,少于 5。resolver 会先把它当作「可能需要 search 补全的名字」,于是先查:

1
2
3
4
service-a.region.example.com.<namespace>.svc.cluster.local
service-a.region.example.com.svc.cluster.local
service-a.region.example.com.cluster.local
service-a.region.example.com.example.internal

最后才查原始名字:

1
service-a.region.example.com

如果节点 /etc/resolv.conf 里没有 search example.internal,那么 Pod search list 里就不会出现 example.internal,对应的扩展查询也不会发生。但只要仍然是 ndots:5,Kubernetes 默认的三个集群 search 域仍然可能先被尝试。

search 什么时候是健康的

search 的典型用途是短主机名补全。例如希望业务只访问:

1
node1

resolver 自动补全成:

1
node1.example.internal

这种用法没有问题,前提是 DNS zone 对不存在的名字返回 NXDOMAIN。也就是说,DNS 只应该给真实存在的主机名返回 A 记录;对不存在的补全名,应该明确告诉客户端「没有这个名字」。

健康行为应该是:

1
2
3
node1.example.internal -> A 10.0.0.11
unknown.example.internal -> NXDOMAIN
service-a.region.example.com.example.internal -> NXDOMAIN

有问题的行为是:

1
2
unknown.example.internal -> A 192.0.2.10
service-a.region.example.com.example.internal -> A 192.0.2.10

第二种情况里,resolver 会在 search 扩展阶段提前成功,应用拿到的是拼接域名的结果,而不是原始域名的结果。

可以用 dig 直接确认:

1
dig service-a.region.example.com.example.internal +noall +answer +comments

如果返回:

1
status: NXDOMAIN

说明这个 search 后缀对不存在的名字处理正常,resolver 可以继续尝试后续候选名。如果返回 NOERROR 并带 A 记录,就要继续检查 DNS 代理、fake-ip、fallback 或 wildcard 记录。

怎么验证

先看 Pod 实际 DNS 配置:

1
kubectl exec -n <namespace> <pod> -- cat /etc/resolv.conf

重点看三项:

1
2
3
nameserver <cluster-dns-ip>
search <namespace>.svc.cluster.local svc.cluster.local cluster.local ...
options ndots:5

再确认 Pod 跑在哪个节点:

1
kubectl get pod -n <namespace> <pod> -o wide

然后看 kubelet 实际使用哪个 resolv.conf:

1
2
kubectl get --raw "/api/v1/nodes/<node-name>/proxy/configz" \
  | jq '.kubeletconfig.resolvConf'

如果这里是 /etc/resolv.conf,就去对应节点确认文件内容。注意,已经创建的 Pod 不一定会因为节点 resolv.conf 变化而自动刷新,通常需要重建 Pod。

如果要看 CoreDNS 的转发配置:

1
kubectl -n kube-system get cm coredns -o yaml

重点看 Corefile 里的 forward 配置。

规避方式

最小影响的方式是在访问外部域名时使用 FQDN,也就是末尾加点:

1
service-a.region.example.com.

末尾的 . 表示这个名字已经是绝对域名,resolver 不再追加 search list。

如果能控制 Pod 配置,也可以降低 ndots

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
apiVersion: v1
kind: Pod
metadata:
  name: dns-example
spec:
  dnsPolicy: ClusterFirst
  dnsConfig:
    options:
      - name: ndots
        value: "1"

这样 service-a.region.example.com 这类包含多个点的外部域名会优先按原始名字查询。

如果问题来自节点上的 search 域,且这个 search 域不应该进入业务 Pod,可以清理节点 /etc/resolv.conf,或者让 kubelet 的 resolvConf 指向一份更干净的 resolver 配置文件。

小结

ClusterFirst 的关键不是「只用集群 DNS,不看节点 DNS 配置」。更准确的描述是:

  1. Pod 的 nameserver 通常被 kubelet 设置为集群 DNS。
  2. Pod 的 search list 会包含 Kubernetes 默认搜索域,并可能追加节点 resolv.conf 中的 search 域。
  3. ndots:5 会让许多外部域名先经过 search 扩展。
  4. search 生成查询名,nameserver 负责接收这些查询;二者不是互斥关系。

所以,当看到外部域名被扩展成 xxx.<search-domain> 时,排查重点应该放在 Pod 内 /etc/resolv.confsearchoptions ndots 上,而不是只看 nameserver。

参考资料:

#Kubernetes #DNS #CoreDNS #Kubelet #Linux