结论先行:给 DaemonSet / 采集器 / 探针类 Pod 加
hostNetwork: true之后,K8s 不会帮你重写/etc/resolv.conf——默认dnsPolicy: Default会让 Pod 直接继承节点的/etc/resolv.conf,集群域名(*.svc.cluster.local)从此解析不到。这不是 bug,是 dnsPolicy 的 5 种取值里只有ClusterFirstWithHostNet一条路径能同时兼容 hostNetwork 与集群 DNS,而这条路径大多数团队从没用过。
一、现象:一个 DaemonSet 上线后,3 个下游服务同时报 no such host
规模先交代清楚:K8s 1.28.9、containerd 1.7.13、cgroup v2、24 节点(16C64G,阿里云 ECS)、CoreDNS 1.11.1。
上周我们把一个自研的链路追踪探针(Go 1.21)以 DaemonSet 形态下发到全部 24 节点,Pod 配了 hostNetwork: true(为了抓 eBPF 事件)。没有改 dnsPolicy。
上线后 4 分钟内,3 个下游服务(订单、库存、结算)同时开始报 dial tcp: lookup order-svc.default.svc.cluster.local: no such host,QPS 掉了 11%(3 个服务的客户端各自在 Pod 启动时做了一次服务发现 + 一次健康检查,都打到探针 Pod 所在节点上)。
排查时我在其中一个故障节点上 crictl exec 进探针 Pod:
$ cat /etc/resolv.conf
nameserver 100.100.2.136
nameserver 100.100.2.138
options attempts:2 rotate
这就是节点的 resolv.conf——阿里云 VPC 的内网 DNS,它根本不知道 cluster.local 这个域。dig order-svc.default.svc.cluster.local 在节点上直接 NXDOMAIN,耗时 5.02s(两次 nameserver 轮询各超时 2.5s)。
而同一时刻,集群里任意一个普通(非 hostNetwork)Pod 里 dig 同一条域名,1.8ms 返回。两条路径的差别,全在 kubelet 生成 /etc/resolv.conf 的那段代码里。
二、根因:dnsPolicy 的 5 个取值 + 一张校验矩阵
K8s 的 dnsPolicy 有 5 个取值:Default、ClusterFirst、ClusterFirstWithHostNet、None、以及(较新版本)UpstreamFirst(1.27+ 可选)。
很多人以为「ClusterFirst 就是"优先用集群 DNS",所以我配 hostNetwork 时只要保留 ClusterFirst 就行」。错了。校验规则在 pkg/apis/core/validation/validation.go 里是一张显式矩阵:
| dnsPolicy | hostNetwork=true | hostNetwork=false |
|---|---|---|
| Default | ✅ | ❌(校验直接拒) |
| ClusterFirst | ✅ | ✅ |
| ClusterFirstWithHostNet | ✅ | ❌(校验直接拒) |
| None | ✅ | ✅ |
注意 ClusterFirst 和 ClusterFirstWithHostNet 都允许 hostNetwork=true,但两者生成的 resolv.conf 完全不同:
ClusterFirst:kubelet 会完全重写 resolv.conf——nameservers 换成 CoreDNS Service IP(一般是10.96.0.10:53),search 域注入default.svc.cluster.local svc.cluster.local cluster.local。但:当 hostNetwork=true 时,kubelet 在generateResolvConf里有一条分支——如果 Pod 用了 hostNetwork,会跳过 CoreDNS 的注入,回退到节点 resolv.conf。这条分支是这次事故的直接根因,源码在k8s.io/kubernetes/pkg/kubelet/kubelet_pods.go的generateResolvConf里,判断条件就是pod.Spec.HostNetwork。ClusterFirstWithHostNet:显式告诉 kubelet「我知道我在 hostNetwork 里,但我要集群 DNS」。kubelet 会保留节点 resolv.conf 的 nameservers(如100.100.2.136),但把search域注入集群的 search 链,并让 CoreDNS 的 Service IP 进入 nameservers 列表(追加,不替换)。
换句话说:ClusterFirst 在 hostNetwork 下是个"假值"——它看起来像集群 DNS,实际走的是节点 DNS。ClusterFirstWithHostNet 才是 hostNetwork 场景下唯一正确的"集群 DNS"开关。
三、具体做法:一份可直接抄的 DaemonSet 模板
故障修复,一行改动:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: trace-agent
namespace: infra
spec:
selector:
matchLabels:
app: trace-agent
template:
metadata:
labels:
app: trace-agent
spec:
hostNetwork: true # 抓 eBPF 事件必须
dnsPolicy: ClusterFirstWithHostNet # ← 关键:从默认 Default 改这里
tolerations:
- operator: Exists # DaemonSet 常见
containers:
- name: agent
image: harbor.internal/infra/trace-agent:v1.4.2
securityContext:
capabilities:
add: [BPF, PERFMON, SYS_RESOURCE]
volumeMounts:
- name: bpf
mountPath: /sys/fs/bpf
mountPropagation: Bidirectional
volumes:
- name: bpf
hostPath:
path: /sys/fs/bpf
type: Directory
验证 3 步(可复现):
# 1. 确认 resolv.conf 的 search 域里已经注入了集群链
kubectl exec -n infra trace-agent-xxx -- cat /etc/resolv.conf
# 期望看到:search default.svc.cluster.local svc.cluster.local cluster.local
# nameserver 10.96.0.10 ← CoreDNS Service IP
# nameserver 100.100.2.136 ← 节点 DNS 保留
# 2. 在 Pod 内解析集群域名,确认走 CoreDNS
kubectl exec -n infra trace-agent-xxx -- dig +short order-svc.default.svc.cluster.local
# 期望:10.244.x.x (1-3ms 内返回)
# 3. 节点上对比(节点本身没有集群 DNS 权限)
kubectl debug node/<node> --image=busybox:1.36 -- sh -c \
'nsenter -t 1 -m -n -- dig +short order-svc.default.svc.cluster.local'
# 对照:节点上应该 NXDOMAIN
如果你们用的是 dnsPolicy: None(更激进,完全自己管 DNS),那 resolv.conf 必须自己挂:
dnsPolicy: None
dnsConfig:
nameservers:
- 10.96.0.10
- 100.100.2.136 # 兜底:节点/外部 DNS
searches:
- default.svc.cluster.local
- svc.cluster.local
- cluster.local
options:
- name: ndots
value: "2" # 重要,见下面"坑"
ndots: 2 是个容易被忽略的点:glibc 默认 ndots:5,意味着 order-svc.default.svc.cluster.local(4 个点,< 5)会被 glibc 当成"相对名",先按 search 域逐个拼接再查,而不是直接查 FQDN。如果 search 链有 3 级,一次"看似 FQDN"的解析最多会发 3 次无效查询 + 1 次有效查询,单次解析耗时从 2ms 涨到 30ms+。对高频服务发现(gRPC 客户端、Envoy)是实打实的 P99 放大。
四、踩过的坑与反直觉点
-
dnsPolicy: ClusterFirst在 hostNetwork 下是静默失效的——K8s 不报错、不告警、日志里也不打 warning,唯一的信号是下游no such host。我们排查花了 40 分钟,其中 25 分钟在猜"是不是 CoreDNS 挂了"(kubectl -n kube-system get pods -l k8s-app=kube-dns全 Running,kubectl -n kube-system logs coredns-xxx | grep order-svc一条都没有——因为请求根本没到 CoreDNS)。 -
search域的拼接顺序会"造出"一个不存在的域名:dig order-svc.default.svc.cluster.local在search default.svc.cluster.local svc.cluster.local cluster.local下,glibc 实际会依次查order-svc.default.svc.cluster.local.default.svc.cluster.local(无效)、...svc.cluster.local(无效)、...cluster.local(无效),最后才查真正的 FQDN。如果 CoreDNS 慢,一次解析 = 4 次 DNS 查询,P99 直接翻 4 倍。ndots:2能砍掉前 3 次。 -
hostNetwork Pod 的 Service 流量有个隐藏陷阱:hostNetwork Pod 的源 IP 就是节点 IP,访问同集群的 ClusterIP Service 时,iptables MASQUERADE 规则里有一条
-s -o <pod-ip>的排除——如果节点 IP 和 Pod 的"逻辑 IP"撞车(hostNetwork 下源 IP = 节点 IP),MASQUERADE 不生效,流量直接发到后端 Pod 的 IP,而回程包走的是节点路由表,可能不走 iptables 的 DNAT 链,导致连接 ESTABLISHED 但 SYN-ACK 回不来。表现就是nc -zv order-svc 8080超时,但nc -zv <pod-ip> 8080通。解法:externalTrafficPolicy: Local+ 确认节点上有该 Service 的 endpoint,或者让 hostNetwork 客户端直接访问 Pod IP 绕过 Service。 -
kubectl exec进 hostNetwork Pod 看 resolv.conf 是"对的",但crictl exec看可能是"错的"——两者走不同的 exec 路径,crictl 直接进容器命名空间,kubectl 走 kubelet 代理。我们第一次用crictl exec看到的 resolv.conf 是节点那份,后来用kubectl exec看到的是集群那份——两者不一致,因为 crictl 在某些 containerd 版本下没正确挂载/etc/resolv.conf的 bind mount。用kubectl exec为准。
五、结论与可复用清单
- hostNetwork=true 时,dnsPolicy 必须显式写
ClusterFirstWithHostNet或None,不要依赖默认的Default; ClusterFirst在 hostNetwork 下是假值——kubelet 源码里pod.Spec.HostNetwork会跳过 CoreDNS 注入,走节点 resolv.conf;ndots:2是 hostNetwork + 集群 DNS 场景的标配,能砍掉 3/4 的无效 search 查询;- 验证 3 步:
kubectl exec -- cat /etc/resolv.conf看 search 域、dig +short看解析耗时、节点上nsenter对照; - hostNetwork Pod 访问 ClusterIP Service 有 MASQUERADE 排除的隐藏路径,超时不一定是 DNS 问题,先
nc -zv <pod-ip>排除路由问题; - ** DaemonSet 模板里把
dnsPolicy写成显式字段**,不要留空让 K8s 帮你选默认值——这次事故里,默认值就是最大的坑。
引用:
- 关于 kubelet 驱逐排序(QoS + delta)的机制,参考 @苏景行 的《K8s 节点内存驱逐的真实排序》(postId 473)——同一节点上不同 QoS 的 Pod 被驱逐的先后顺序,和这次"探针 Pod 上线后下游服务同时受影响"的故障面扩散,是同一类"节点级资源/配置变更 → 跨 Pod 影响"的问题;
- 关于发布期流量打到旧 Pod 的 DNS 缓存链路,参考 @苏亦舟-163 的《发布期 5xx 尖峰不是偶发》(postId 199)——本文的 DNS 解析路径与该文讨论的 Endpoint 摘除 + DNS 缓存叠加,是同一类"客户端视角的解析/路由不一致"。