Netpath
学习路径39 / 49 · K8s 网络看全程 →
原理预计 25 分钟

CoreDNS 与 NetworkPolicy

集群里一半的"网络故障"其实是 DNS;另一半是策略拦了没人知道。

学完这节你能做到

  • 解释 Pod 内 DNS 查询的完整过程与 ndots 带来的额外查询
  • 排查 CoreDNS 延迟与超时
  • 写出一条最小放行的 NetworkPolicy 并验证它生效

集群里一半的"网络故障"是 DNS

这句话不夸张。DNS 的特点是它失败时不像 DNS 失败:表现为连接超时、 偶发 5xx、启动慢、健康检查抖动——全都指向"网络问题",而实际是解析出了问题。

所以排障顺序里 DNS 永远在第一位(Service 那节 的排障清单第一条就是它)。

一个 Service 域名有四段

web.default.svc.cluster.local
└┬┘ └──┬──┘ └┬┘ └─────┬─────┘
服务名 命名空间 类型   集群域名

Pod 里可以只写前面几段,靠 search 域补全:

kubectl exec -it mypod -- cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

于是这些写法都能通:

写法补全成
webweb.default.svc.cluster.local
web.otherweb.other.svc.cluster.local
web.other.svcweb.other.svc.cluster.local
web.other.svc.cluster.local.末尾有点 = 完全限定,不补全

ndots:5 —— 一个真实的性能陷阱

ndots:5 的含义是:域名里的点少于 5 个时,先依次拼 search 域去试,都失败了才当绝对域名查。

看一次 curl https://api.github.com 会发生什么:

api.github.com 有 2 个点 < 5  → 先走 search 域
① api.github.com.default.svc.cluster.local   → NXDOMAIN
② api.github.com.svc.cluster.local           → NXDOMAIN
③ api.github.com.cluster.local               → NXDOMAIN
④ api.github.com                             → 命中

一次外部域名解析变成 4 次查询,而且 A 和 AAAA 各来一遍,实际是 8 个 DNS 包。 集群里外部调用一多,CoreDNS 的 QPS 就被放大好几倍。

三种缓解,从省事到彻底:

# ① 给外部调用密集的 Pod 单独降低 ndots
spec:
  dnsConfig:
    options:
      - name: ndots
        value: "2"
# ② 代码或配置里把外部域名写成完全限定(末尾加点)
curl https://api.github.com./           # 注意 com 后面那个点,直接跳过 search

③ 部署 NodeLocal DNSCache:每个节点上跑一个本地缓存,Pod 查本地而不是跨节点查 CoreDNS。 这条收益最大——省掉网络往返,还挡住了大部分重复查询。

×AAAA 查询在纯 IPv4 集群里是纯浪费

glibc 默认同时发 A 和 AAAA。集群没有 IPv6 时,AAAA 全部返回 NXDOMAIN, 但每个包都真实占用了 CoreDNS 的处理能力。

而且有些基础镜像(Alpine 用的 musl libc)对 search 域和并发查询的处理与 glibc 不同, 会出现"同样的代码在 Alpine 镜像里解析慢或偶发失败"。

排查思路:先在 Pod 里手工验证一次解析要发多少个包。

kubectl exec -it mypod -- sh -c 'time nslookup api.github.com'

CoreDNS 容量与配置

kubectl get deploy -n kube-system coredns
kubectl get cm -n kube-system coredns -o yaml
.:53 {
    errors
    health { lameduck 5s }
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
      pods insecure
      fallthrough in-addr.arpa ip6.arpa
    }
    prometheus :9153
    forward . /etc/resolv.conf { max_concurrent 1000 }
    cache 30
    loop
    reload
    loadbalance
}

几个真正影响体验的点:

配置作用
cache 30缓存 30 秒。调大能显著降 QPS,代价是 Service IP 变化后有延迟
forward . /etc/resolv.conf集群外域名转发给上游。上游慢 = 集群里所有外部解析都慢
max_concurrent并发上限,打满会开始丢查询
副本数按 QPS 而不是按节点数定;通常配 HPA 或至少 2 副本反亲和

监控要看的两个指标:

# 请求量与延迟
coredns_dns_requests_total
coredns_dns_request_duration_seconds
# 转发到上游的失败
coredns_forward_healthcheck_failures_total
✓上游 DNS 是集群外的隐形依赖

forward . /etc/resolv.conf 意味着集群的外部解析能力等于节点 DNS 的能力。 上游 DNS 抖动时,症状是"集群里访问外部服务全部变慢",而集群内部一切正常。

所以排障要分清是内部域名还是外部域名慢:

kubectl exec -it mypod -- nslookup web.default.svc.cluster.local     # 内部
kubectl exec -it mypod -- nslookup github.com                        # 外部

只有外部慢 → 查上游与 forward 配置,不用动 CoreDNS 副本数。

NetworkPolicy:默认全通,一开就全拦

K8s 的默认行为是任意 Pod 可以访问任意 Pod。NetworkPolicy 的模型有一个关键特性:

一旦有任何一条策略选中了某个 Pod,这个 Pod 就从"默认全通"变成"只允许被明确放通的"。

所以第一条策略往往会造成大面积中断——不是策略写错了,是语义如此。

标准写法:先默认拒绝,再逐个放通

# ① 默认拒绝该命名空间的所有入向与出向
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: team-a
spec:
  podSelector: {}                    # 选中所有 Pod
  policyTypes: [Ingress, Egress]
# ② 放通 DNS —— 必须最先做,否则一切都解析不了
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: team-a
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
    - to:
        - namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: kube-system } }
          podSelector: { matchLabels: { k8s-app: kube-dns } }
      ports:
        - { protocol: UDP, port: 53 }
        - { protocol: TCP, port: 53 }
# ③ 放通业务:只让 frontend 访问 api 的 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-from-frontend
  namespace: team-a
spec:
  podSelector: { matchLabels: { app: api } }
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector: { matchLabels: { app: frontend } }
      ports:
        - { protocol: TCP, port: 8080 }
×忘了放通 DNS —— 第一次上策略必踩

默认拒绝 Egress 之后,Pod 连 CoreDNS 都访问不了。症状是所有域名解析失败, 业务日志里全是 no such host / Temporary failure in name resolution。

而且很容易误判:看起来像 CoreDNS 挂了,实际 CoreDNS 好得很,是流量被自己的策略拦了。

顺序永远是:先写 allow-dns,再写 default-deny。 反过来的话中间那段时间业务全挂。

验证策略真的生效了

写完不能只看 kubectl get networkpolicy。要双向验证:该通的通、该拦的拦。

# 该通:从 frontend 访问 api
kubectl exec -it frontend-xxx -- curl -m 3 -sS api:8080/health

# 该拦:从别的 Pod 访问 api,应该超时(不是 refused)
kubectl exec -it other-xxx -- curl -m 3 -sS api:8080/health
# 期望:curl: (28) Connection timed out

被策略拦掉的表现是超时而不是拒绝——因为包被静默丢弃,没有 RST 返回。 这一点能帮你区分"策略拦了"和"服务没起来"(后者是 refused)。

关键一步:在宿主机侧看 drop 事件。Pod 内部对此毫无痕迹(CNI 那节 讲过):

# 实时看被丢弃的包及原因
kubectl -n kube-system exec -it <cilium-pod> -- cilium-dbg monitor --type drop

# 看某个 Pod 的策略判定结果
kubectl -n kube-system exec -it <cilium-pod> -- cilium-dbg endpoint list
kubectl -n kube-system exec -it <cilium-pod> -- cilium-dbg policy get
xx drop (Policy denied) flow 0x0 to endpoint 1234, identity 5678->9012: 10.244.1.7 -> 10.244.2.9 tcp SYN
   ↑ 原因写得很明确

基于身份而不是 IP

最后一个概念上的跃迁。传统防火墙按 IP 写规则,但 Pod IP 每次重启都变, 按 IP 写的规则活不过一次滚动更新。

所以 CNI 的做法是:把 Pod 的标签算成一个身份(identity),策略针对身份生效。

kubectl -n kube-system exec -it <cilium-pod> -- cilium-dbg identity list | head

这解释了为什么 NetworkPolicy 用的是 podSelector(标签选择器)而不是 IP 段—— 标签稳定,IP 不稳定。同一个道理:ipBlock 只该用于集群外的地址, 集群内一律用选择器。

检查点

Checkpoint单选

Pod 里访问 api.github.com,抓包发现产生了 8 个 DNS 查询包。为什么?

Checkpoint单选

给命名空间加了默认拒绝策略后,所有 Pod 报 no such host。原因是?

Checkpoint单选

怀疑某个请求被 NetworkPolicy 拦了。最直接的确认方式是?

这节课的落点

  • DNS 失败时不像 DNS 失败:表现为超时、5xx、启动慢,所以排障永远先查它
  • Service 域名四段;search 域负责补全,末尾加点表示完全限定
  • ndots:5 让一次外部解析变成 8 个包;缓解三档:调 ndots、写完全限定名、 部署 NodeLocal DNSCache
  • CoreDNS 副本按 QPS 定;cache 30 调大最省 QPS;上游 DNS 是集群外的隐形依赖
  • 分清内部域名慢还是外部域名慢,只有外部慢就去查 forward 与上游
  • NetworkPolicy 语义:一被策略选中就从"全通"变成"只放通明确允许的"
  • 先写 allow-dns 再写 default-deny,否则中间那段业务全挂
  • 被策略拦是超时(静默丢弃),服务没起来是 refused
  • 验证要双向,并在宿主机侧看 drop 事件;集群内规则一律用选择器而不是 IP 段

延伸资料