跳过正文
CoreDNS 深度排障:K8s DNS 问题完全指南

CoreDNS 深度排障:K8s DNS 问题完全指南

·1064 字·5 分钟·
目录
K8s 完全指南 - 这篇文章属于一个选集。
§ : 本文

K8s DNS 解析链路
#

在排障之前,必须先搞清楚一个 Pod 里的 DNS 请求是怎么走的。

Pod 内应用
    │ DNS 查询 (UDP 53)
    ▼
/etc/resolv.conf 中的 nameserver(通常是 CoreDNS Service ClusterIP)
    │
    ▼
CoreDNS Pod(通常2-3个副本,运行在 kube-system)
    │
    ├─ cluster.local 域 → 查 K8s 内部 Service/Pod DNS
    ├─ 反向查找 → 查 K8s 内部
    └─ 其他域名 → Forward 到上游 DNS(通常是节点的 /etc/resolv.conf)

查看 Pod 的 DNS 配置:

kubectl exec -n production my-pod -- cat /etc/resolv.conf
# nameserver 10.96.0.10       ← CoreDNS Service IP
# search production.svc.cluster.local svc.cluster.local cluster.local
# options ndots:5

这三行是理解所有 DNS 问题的起点。


ndots:5 的坑
#

ndots:5 是 K8s 给每个 Pod 设置的默认值,意思是:如果查询的域名中点的个数少于5个,就先在 search 列表中逐一尝试追加后缀,失败后才查原始域名。

一次请求变多次
#

假设 Pod 查询 api.example.com

api.example.com → 点数=2,< 5,先走 search 列表:

1. api.example.com.production.svc.cluster.local → NXDOMAIN
2. api.example.com.svc.cluster.local            → NXDOMAIN
3. api.example.com.cluster.local                → NXDOMAIN
4. api.example.com.                             → 外部 DNS 解析成功 ✓

一次看似简单的 DNS 查询,在 Pod 里实际发出了 4 次 UDP 请求。在高并发下,这 3 次无效查询会显著增加 CoreDNS 负载,也会增加应用的 DNS 解析延迟。

解决方案
#

方案1:域名末尾加点(FQDN)

# Python
import requests
# 改成 FQDN(末尾加点),跳过 search 列表
requests.get("http://api.example.com./v1/data")

方案2:在 Pod 的 dnsConfig 中调整

apiVersion: v1
kind: Pod
spec:
  dnsConfig:
    options:
      - name: ndots
        value: "2"   # 减小阈值,只有1个点才走 search
      - name: timeout
        value: "5"
      - name: attempts
        value: "2"

方案3:在 Deployment 中统一设置

spec:
  template:
    spec:
      dnsConfig:
        options:
          - name: ndots
            value: "2"
          - name: single-request-reopen   # 解决 5 秒延迟,后文详解

CoreDNS 配置详解
#

CoreDNS 通过 ConfigMap corednskube-system 命名空间中配置:

kubectl get configmap coredns -n kube-system -o yaml

默认 Corefile:

.:53 {
    errors
    health {
        lameduck 5s
    }
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
        pods insecure
        fallthrough in-addr.arpa ip6.arpa
        ttl 30
    }
    prometheus :9153
    forward . /etc/resolv.conf {
        max_concurrent 1000
    }
    cache 30
    loop
    reload
    loadbalance
}

配置调优
#

.:53 {
    errors

    # 健康检查宽限期(滚动重启时给 CoreDNS 优雅退出时间)
    health {
        lameduck 10s
    }
    ready

    kubernetes cluster.local in-addr.arpa ip6.arpa {
        pods insecure
        fallthrough in-addr.arpa ip6.arpa
        ttl 30
    }

    prometheus :9153

    # 上游 DNS Forward 调优
    forward . 8.8.8.8 8.8.4.4 {
        max_concurrent 1000
        prefer_udp         # 优先 UDP(避免 TCP 连接开销)
        health_check 5s    # 上游健康检查间隔
    }

    # 缓存调优
    cache {
        success 9984 30    # 成功响应缓存:最多9984条,TTL 30s
        denial 9984 5      # 失败响应(NXDOMAIN)缓存:TTL 5s
        prefetch 10 1m 10% # 缓存命中率高的域名提前刷新
    }

    loop    # 防止 DNS 转发循环
    reload  # 自动 reload ConfigMap(无需重启 Pod)
    loadbalance round_robin  # 多 A 记录时轮询
}

修改配置后:

kubectl edit configmap coredns -n kube-system
# reload 插件会在 30 秒内自动生效,无需重启 CoreDNS Pod

常见故障排障
#

故障1:DNS 解析失败(NXDOMAIN)
#

现象:服务启动时 dial tcp: lookup svc-name: no such host

排查步骤

# 1. 确认 CoreDNS Pod 是否正常
kubectl get pods -n kube-system -l k8s-app=kube-dns
kubectl logs -n kube-system -l k8s-app=kube-dns --since=5m

# 2. 在故障 Pod 中手动测试
kubectl exec -n production my-pod -- nslookup my-service
kubectl exec -n production my-pod -- nslookup my-service.production
kubectl exec -n production my-pod -- nslookup my-service.production.svc.cluster.local

# 3. 确认 Service 存在且 Endpoints 正常
kubectl get svc my-service -n production
kubectl get endpoints my-service -n production

# 4. 检查 Service 的 DNS 记录(格式:<service>.<namespace>.svc.<cluster-domain>)
kubectl exec -n production my-pod -- nslookup my-service.production.svc.cluster.local 10.96.0.10

常见原因

  • 跨命名空间访问没有带命名空间(my-service vs my-service.other-namespace
  • Service 名字和实际访问的域名不匹配(大小写、拼写错误)
  • CoreDNS 重启后缓存还没有热身,偶发 NXDOMAIN

故障2:DNS 查询 5 秒延迟
#

这是 K8s 最臭名昭著的 DNS 问题,根因是 Linux conntrack 竞态条件

背景

CoreDNS Service 通常绑定一个 ClusterIP,同一个 Pod 同时发出多个 DNS 查询时,Linux 内核的 DNAT 规则存在竞态:两个 UDP 包同时到达,conntrack 表插入产生冲突,导致其中一个包被丢弃。UDP 没有重传机制,只能等超时(默认 5 秒)。

复现验证

# 抓取 DNS 请求,观察是否有超时重传
kubectl exec -n production my-pod -- \
  tcpdump -i any -nn -w /tmp/dns.pcap port 53 &

# 触发 DNS 查询
kubectl exec -n production my-pod -- \
  for i in $(seq 1 100); do nslookup api.example.com &; done; wait

# 分析抓包(在本地)
tcpdump -r /tmp/dns.pcap -nn | grep "id:" | awk '{print $1, $NF}' | sort

解决方案

# 方案1:dnsConfig 中加 single-request-reopen
spec:
  dnsConfig:
    options:
      - name: single-request-reopen  # A/AAAA 记录查询用不同 socket,避免竞态
      - name: ndots
        value: "5"
# 方案2:CoreDNS 使用 TCP(完全避免 UDP 竞态)
# 在 Corefile 的 forward 中加 force_tcp
forward . 8.8.8.8 {
    force_tcp
}
# 方案3:调整 conntrack 参数(节点级别)
# /etc/sysctl.conf
net.netfilter.nf_conntrack_udp_timeout = 10
net.netfilter.nf_conntrack_udp_timeout_stream = 180

实际效果最好的是方案1(single-request-reopen),对应用无侵入,只需在 Pod Spec 中加一行。

故障3:CoreDNS OOMKill
#

现象:CoreDNS Pod 频繁重启,kubectl describe pod 看到 OOMKilled

kubectl top pods -n kube-system -l k8s-app=kube-dns
# NAME                    CPU(cores)   MEMORY(bytes)
# coredns-xxx             200m         450Mi   ← 接近 limit

原因:大集群中 CoreDNS 默认的内存 limit(170Mi)远不够用,缓存会持续增长。

解决

# 通过 HPA 或直接调整 resources
kubectl patch deployment coredns -n kube-system --patch='
{
  "spec": {
    "template": {
      "spec": {
        "containers": [{
          "name": "coredns",
          "resources": {
            "requests": {"cpu": "100m", "memory": "128Mi"},
            "limits": {"cpu": "500m", "memory": "512Mi"}
          }
        }]
      }
    }
  }
}'

同时配置 CoreDNS 的 HPA:

kubectl autoscale deployment coredns \
  --namespace=kube-system \
  --min=2 --max=10 \
  --cpu-percent=70

故障4:外部域名解析慢
#

现象:集群内访问外部服务域名(如第三方 API)延迟异常高。

排查

# 测试外部域名解析时间
kubectl exec -n production my-pod -- time nslookup external-api.example.com

# 对比直接查上游
kubectl exec -n production my-pod -- time nslookup external-api.example.com 8.8.8.8

如果直接查上游快,说明问题在 CoreDNS 的 forward 环节。

常见原因

  • 节点上的 /etc/resolv.conf 指向的 DNS 有问题(CoreDNS 默认把节点的 resolv.conf 作为上游)
  • 上游 DNS max_concurrent 不够,请求排队

解决

# 在 Corefile 中直接指定可靠的上游
forward . 8.8.8.8 8.8.4.4 1.1.1.1 {
    max_concurrent 2000
}

dnsutils 调试工具箱
#

# 部署调试 Pod
kubectl run dnsutils --image=registry.k8s.io/e2e-test-images/jessie-dnsutils:1.3 \
  --restart=Never --rm -it -- sh

# 在 dnsutils Pod 中常用命令
# 基础查询
nslookup kubernetes.default
dig kubernetes.default.svc.cluster.local

# 详细解析过程
dig +trace external-api.example.com

# 测试反向查询
dig -x 10.96.0.1

# 查询 SRV 记录(服务发现)
dig _http._tcp.my-service.production.svc.cluster.local SRV

# 测试解析速度
time for i in $(seq 1 10); do nslookup my-service.production.svc.cluster.local; done

抓包验证
#

# 在 CoreDNS Pod 上抓 DNS 请求
kubectl exec -n kube-system coredns-xxx -- \
  tcpdump -i any -nn port 53 -c 100

# 在业务 Pod 上抓(需要 Pod 有 tcpdump 或用 nsenter)
# 通过 nsenter 进入 Pod 网络命名空间(需要节点权限)
# 1. 找到 Pod 在哪个节点
kubectl get pod my-pod -o wide

# 2. SSH 到节点,找到 PID
crictl inspect $(crictl ps --name my-pod -q) | jq .info.pid

# 3. nsenter 进入网络命名空间
nsenter -t <PID> -n -- tcpdump -nn -i eth0 port 53

典型的 5 秒延迟抓包特征:

# 正常(两次查询,时间戳紧密)
14:00:00.001 A query: api.example.com
14:00:00.003 A response: api.example.com -> 1.2.3.4

# 异常(5秒超时重传)
14:00:00.001 A query: api.example.com
14:00:05.001 A query: api.example.com  ← 5秒后重试
14:00:05.003 A response: api.example.com -> 1.2.3.4

CoreDNS 监控指标
#

# Prometheus 告警规则
groups:
  - name: coredns
    rules:
      # DNS 请求错误率 > 1%
      - alert: CoreDNSHighErrorRate
        expr: |
          sum(rate(coredns_dns_responses_total{rcode!="NOERROR",rcode!="NXDOMAIN"}[5m]))
          / sum(rate(coredns_dns_responses_total[5m])) > 0.01
        for: 5m
        annotations:
          summary: "CoreDNS 错误率过高: {{ $value | humanizePercentage }}"

      # DNS P99 延迟 > 500ms
      - alert: CoreDNSHighLatency
        expr: |
          histogram_quantile(0.99,
            sum(rate(coredns_dns_request_duration_seconds_bucket[5m])) by (le, server, zone)
          ) > 0.5
        for: 5m
        annotations:
          summary: "CoreDNS P99 延迟过高"

      # CoreDNS Pod 不足
      - alert: CoreDNSDown
        expr: kube_deployment_status_replicas_available{deployment="coredns", namespace="kube-system"} < 2
        for: 2m
        annotations:
          summary: "CoreDNS 可用副本数不足"

总结
#

处理 K8s DNS 问题的优先级:

  1. 先确认 CoreDNS Pod 是否健康:Running、Ready、无 OOMKill
  2. 用 dnsutils 手工测试:区分"Pod 内 resolv.conf 问题"和"CoreDNS 本身问题"
  3. 区分内部域名 vs 外部域名:内部问题看 CoreDNS ↔ K8s 服务注册;外部问题看 forward 上游
  4. 5 秒延迟:十有八九是 conntrack 竞态,加 single-request-reopen
  5. 高并发下 NXDOMAIN 偶发:检查 CoreDNS 缓存容量和副本数

DNS 排障最大的陷阱是"间歇性"——问题复现率低,容易被误判为网络抖动。养成习惯:凡是应用层面的连接超时,先用 dig/nslookup 验证 DNS 解析是否正常,再往下排查。

Wenzhuo Huang
作者
Wenzhuo Huang
搞运维的工程师,写代码的运维人。专注 Kubernetes、AWS、GitOps 与基础设施可靠性。这个博客既是我的技术笔记本,也是我踩过的坑的受害者档案。
K8s 完全指南 - 这篇文章属于一个选集。
§ : 本文

相关文章

混沌工程实战:Chaos Mesh 在 K8s 中注入故障

·809 字·4 分钟
混沌工程不是破坏系统,而是在可控环境中提前暴露脆弱点。本文记录了我用 Chaos Mesh 在生产级 K8s 集群中设计并执行混沌演练的完整过程,包括安装、实验配置、Workflow 编排和游戏日流程设计。

OPA/Kyverno:K8s 准入控制策略实战

·895 字·5 分钟
没有准入控制的 K8s 集群就像一个没有门卫的机房——任何人都能随意进出。本文记录了我在多个生产集群部署 Kyverno 策略的实战经验,涵盖资源限制强制、镜像来源白名单、标签规范、以及与 OPA Gatekeeper 的对比选型思路。

Kubernetes 成本优化实战:系统性降本的四条路径

·1066 字·6 分钟
真实的降本案例:从发现成本异常到分析根因,通过 Karpenter 节点弹性伸缩、资源请求规格治理、大机型收敛等手段,系统性降低 AWS EC2 成本。包含具体配置和执行思路。