
在 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 查询。

<!--more-->

## 现象

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

```conf
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 里访问一个外部域名：

```text
service-a.region.example.com
```

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

```text
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`：

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

也就是：

```text
<namespace>.svc.<cluster-domain>
svc.<cluster-domain>
<cluster-domain>
<host-search-from-node-resolv-conf>
```

最终 Pod 里可能看到：

```conf
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 生成哪些查询名。

```mermaid
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 不是二选一

这里最容易混淆的是 `search` 和 `nameserver` 的关系。

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

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

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

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

在 ClusterFirst Pod 里，这个 nameserver 通常是 CoreDNS：

```text
应用 -> Pod resolver -> CoreDNS -> 上游 DNS
```

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

```conf
forward . /etc/resolv.conf
```

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

```mermaid
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
```

## 为什么外部域名也会先 search

原因是 `ndots:5`。

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

```conf
options ndots:5
```

例如：

```text
service-a.region.example.com
```

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

```text
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
```

最后才查原始名字：

```text
service-a.region.example.com
```

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

## search 什么时候是健康的

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

```text
node1
```

resolver 自动补全成：

```text
node1.example.internal
```

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

健康行为应该是：

```text
node1.example.internal -> A 10.0.0.11
unknown.example.internal -> NXDOMAIN
service-a.region.example.com.example.internal -> NXDOMAIN
```

有问题的行为是：

```text
unknown.example.internal -> A 192.0.2.10
service-a.region.example.com.example.internal -> A 192.0.2.10
```

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

可以用 `dig` 直接确认：

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

如果返回：

```text
status: NXDOMAIN
```

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

## 怎么验证

先看 Pod 实际 DNS 配置：

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

重点看三项：

```conf
nameserver <cluster-dns-ip>
search <namespace>.svc.cluster.local svc.cluster.local cluster.local ...
options ndots:5
```

再确认 Pod 跑在哪个节点：

```bash
kubectl get pod -n <namespace> <pod> -o wide
```

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

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

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

如果要看 CoreDNS 的转发配置：

```bash
kubectl -n kube-system get cm coredns -o yaml
```

重点看 Corefile 里的 `forward` 配置。

## 规避方式

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

```text
service-a.region.example.com.
```

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

如果能控制 Pod 配置，也可以降低 `ndots`：

```yaml
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.conf` 的 `search` 和 `options ndots` 上，而不是只看 nameserver。

参考资料：

- [Kubernetes DNS for Services and Pods](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/)
- [Kubernetes Customizing DNS Service](https://kubernetes.io/docs/tasks/administer-cluster/dns-custom-nameservers/)
- [kubelet DNS configurer source code](https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/network/dns/dns.go)
- [resolv.conf(5) Linux manual page](https://man7.org/linux/man-pages/man5/resolv.conf.5.html)

