1. 从“服务发现”说起:为什么Kubernetes离不开DNS

如果你在Kubernetes里跑过几个Pod,尝试过让一个Pod去访问另一个Pod,你大概率不会去记那个又长又难记的Pod IP,比如 10.244.1.5 。你更希望像在互联网上访问 www.google.com 一样,用一个好记的名字,比如 my-frontend-service 。这个“把名字变成IP地址”的过程,就是服务发现,而DNS(域名系统)是实现服务发现最自然、最通用的方式。Kubernetes将这套机制深度集成,让集群内的应用可以像访问外部网站一样,通过域名来访问内部服务、Pod甚至外部端点,极大地简化了微服务间的通信配置。

在Kubernetes的早期版本中,集群DNS功能由 kube-dns 组件提供。但从1.11版本开始,CoreDNS作为其继任者,成为了默认的DNS服务器。这个转变并非偶然。CoreDNS采用Go语言编写,以其模块化、灵活性和高性能著称。它不像传统的BIND那样拥有一个庞大而复杂的配置文件,而是通过一个名为 Corefile 的简洁配置文件,以插件链(Plugin Chain)的方式组织功能。这种设计使得CoreDNS能够轻松适应Kubernetes动态、声明式的环境,高效地响应服务、Pod和节点的频繁变化。

简单来说,没有DNS的Kubernetes集群,就像一座没有路标和门牌号的城市。服务之间只能通过原始的IP地址进行“盲找”,一旦Pod重启或服务扩缩容导致IP变更,整个通信链路就会断裂。CoreDNS就是这个城市里的“动态地址簿”和“智能查询中心”,它持续监听Kubernetes API Server,实时更新服务与IP的映射关系,确保无论后端如何变化,前端应用始终能用同一个稳定的域名找到它。

2. CoreDNS在Kubernetes中的核心工作机制

要理解CoreDNS如何工作,我们需要深入到它的部署和查询流程中。当你部署一个标准的Kubernetes集群(例如使用 kubeadm )时,CoreDNS通常会以Deployment的形式运行在 kube-system 命名空间下,背后有一个Service为其提供稳定的集群IP。

2.1 默认的域名解析逻辑

每个Pod在创建时,其 /etc/resolv.conf 文件会被Kubernetes自动注入DNS配置。这个文件决定了Pod如何进行域名解析。一个典型的内容如下:

nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

我们来拆解这几个关键配置:

  • nameserver 10.96.0.10 : 这是CoreDNS Service的集群IP。Pod发出的所有DNS查询,默认都会发送到这个地址。
  • search : 这是一个域名后缀列表。当Pod尝试解析一个不完全合格域名(例如,只写了服务名 redis )时,系统会依次拼接这些后缀进行查询。查询顺序是: redis.default.svc.cluster.local -> redis.svc.cluster.local -> redis.cluster.local 。这允许你在Pod内用短名称(如 redis )访问同一命名空间的服务,用 <service>.<namespace> (如 redis.prod )访问其他命名空间的服务。
  • options ndots:5 : 这是一个性能优化(有时也是坑点)相关的选项。它规定,如果一个域名中的点( . )数量 大于等于5 ,则会被视为绝对域名(FQDN),直接查询, 会走 search 域拼接。如果点数少于5,则会先尝试拼接 search 域查询,失败后再尝试直接查询。理解这一点对排查某些解析超时问题至关重要。

2.2 CoreDNS的配置核心:Corefile

CoreDNS的行为由一个名为 Corefile 的配置文件驱动。在Kubernetes中,这个配置通常以一个ConfigMap的形式存在,名为 coredns ,位于 kube-system 命名空间。让我们看一个最简化的默认配置:

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

Corefile 部分可能如下:

.:53 {
    errors # 将错误日志输出到标准输出
    health { # 健康检查端点,默认监听8080端口
       lameduck 5s
    }
    ready # 就绪检查端点,用于Kubernetes的Readiness Probe
    kubernetes cluster.local in-addr.arpa ip6.arpa { # 核心插件:处理Kubernetes域名的解析
       pods insecure
       fallthrough in-addr.arpa ip6.arpa
       ttl 30
    }
    prometheus :9153 # 暴露Prometheus指标
    forward . /etc/resolv.conf # 将非集群域名的查询转发到上游DNS
    cache 30 # 缓存
    loop # 检测DNS循环查询并终止
    reload # 允许自动重载配置(修改ConfigMap后,CoreDNS Pod会自动加载新配置)
    loadbalance # DNS负载均衡,对同一域名的多条A记录进行轮询
}

这个配置文件定义了一个监听在53端口的服务器块。其中, kubernetes 插件是灵魂所在。它告诉CoreDNS:负责解析 cluster.local 及其子域(如 svc.cluster.local )的请求。当收到一个对 my-svc.default.svc.cluster.local 的查询时,这个插件会去查询Kubernetes API,获取名为 my-svc 、位于 default 命名空间的Service的ClusterIP,并返回给客户端。

forward . /etc/resolv.conf 这一行则指明了“非Kubernetes域名”的去向。例如,Pod内查询 www.baidu.com ,由于该域名不匹配 cluster.local ,CoreDNS会将查询转发到Pod自身的 /etc/resolv.conf 中定义的上游DNS(通常是宿主机的DNS或外部公共DNS),从而实现内外网域名的统一解析。

2.3 解析流程全景图

假设一个Pod(Pod A)想要访问同一个命名空间下的服务 backend ,整个DNS解析流程如下:

  1. Pod A内的应用程序发起对 backend 的DNS查询。
  2. 查询请求被发送到本地 /etc/resolv.conf 中定义的CoreDNS Service IP( 10.96.0.10 )。
  3. 请求到达某个CoreDNS Pod。
  4. CoreDNS根据 Corefile 配置,首先检查域名。 backend 的点数少于5( ndots:5 ),且不是FQDN,因此CoreDNS会先为其拼接 search 域。
  5. 它首先尝试查询 backend.default.svc.cluster.local 。这个域名匹配 kubernetes 插件负责的 cluster.local 域。
  6. kubernetes 插件拦截该查询,向Kubernetes API Server询问:在 default 命名空间下是否存在一个名为 backend 的Service?
  7. API Server返回该Service的ClusterIP,例如 10.103.156.88
  8. CoreDNS将这个IP地址作为DNS响应返回给Pod A。
  9. Pod A的应用程序拿到IP,发起实际的TCP/UDP连接到 10.103.156.88:port ,由kube-proxy或CNI(如果使用IPVS模式或某些服务网格)完成到后端Pod的负载均衡和流量转发。

这个过程对应用完全透明,开发者无需关心后端Pod的具体位置和IP,实现了彻底的解耦。

3. 深入解析:CoreDNS如何映射Kubernetes资源

CoreDNS的 kubernetes 插件不仅仅能解析Service。通过配置,它可以处理多种Kubernetes资源,形成一套完整的集群内域名体系。

3.1 标准域名格式

  • Service : 这是最常用的形式。

    • 格式: <service-name>.<namespace-name>.svc.cluster.local
    • 解析结果:对应Service的ClusterIP。如果是Headless Service( clusterIP: None ),则返回该Service后端所有Pod的IP地址列表。
    • 示例: backend.default.svc.cluster.local -> 10.103.156.88
  • Pod : 虽然不推荐直接通过Pod IP通信,但CoreDNS也支持通过DNS直接定位Pod。

    • 格式: <pod-ip>.<namespace>.pod.cluster.local
    • 注意,这里的 <pod-ip> 需要将点( . )替换为横线( - )。例如,Pod IP 10.244.1.5 对应的域名是 10-244-1-5.default.pod.cluster.local
    • 这主要用于一些特殊场景,如StatefulSet中Pod需要有稳定的网络标识。
  • Service端口 : 可以解析SRV记录,获取服务的端口和协议信息。

    • 格式: _<port-name>._<protocol>.<service>.<ns>.svc.cluster.local
    • 这在某些需要动态发现端口的客户端中会用到。

3.2 Headless Service与StatefulSet的特别之处

Headless Service(无头服务)是一个特例。当你创建一个 clusterIP: None 的Service时,它不会被分配ClusterIP。此时,对它进行DNS查询,行为会有所不同:

  • 如果该Service没有关联任何Selector,则DNS查询会返回该Service配置的所有Endpoints(手动指定的地址列表)。
  • 如果该Service有关联Selector(最常见的是与StatefulSet配合),DNS查询会返回该Selector匹配的 所有Pod IP地址的列表

StatefulSet与Headless Service是黄金搭档。StatefulSet创建的每个Pod都有稳定的名称: <statefulset-name>-<ordinal> 。当为StatefulSet创建一个同名的Headless Service后,每个Pod会获得一个稳定的DNS域名:

  • 格式: <pod-name>.<headless-svc-name>.<namespace>.svc.cluster.local
  • 示例:对于一个名为 web 的StatefulSet和一个名为 nginx 的Headless Service,第一个Pod的域名是 web-0.nginx.default.svc.cluster.local 。这个域名直接解析到该Pod的IP,并且在该Pod的生命周期内是稳定的,即使Pod重启、重建,只要名字不变,域名解析就不变。这对于构建有状态应用集群(如ZooKeeper, Elasticsearch)至关重要,集群成员可以通过这些稳定的域名互相发现。

3.3 自定义上游与存根域(Stub Domains)

在企业环境中,我们经常需要让集群内的Pod能够解析内部的私有域名,比如 company.internal 。这可以通过CoreDNS的 forward 插件或 hosts 插件来实现,但更Kubernetes原生、更灵活的方式是配置 存根域 上游域名服务器

你可以在CoreDNS的ConfigMap中修改 Corefile ,添加针对特定域名的转发规则:

.:53 {
    ...
    kubernetes cluster.local in-addr.arpa ip6.arpa {
       ...
    }
    # 将 company.internal 域的所有查询转发到指定的内部DNS服务器
    forward company.internal 10.100.10.1 10.100.10.2
    # 将所有其他非集群域名查询转发到宿主机/etc/resolv.conf定义的上游
    forward . /etc/resolv.conf
    ...
}

这样,当Pod查询 db.company.internal 时,CoreDNS会将其转发到 10.100.10.1 10.100.10.2 ,而不是公共DNS。这种配置使得混合云或需要访问企业内网服务的场景变得非常简单。

4. 实战:CoreDNS的配置、调优与故障排查

了解了原理,我们来看看如何管理和维护CoreDNS。

4.1 扩缩容与资源限制

默认的CoreDNS Deployment通常只有2个副本。在生产环境中,这可能需要根据集群规模进行调整。你可以直接修改其Deployment的副本数:

kubectl scale deployment coredns --replicas=3 -n kube-system

同样,默认的资源请求和限制可能偏低。高负载下,CoreDNS可能因内存不足(OOM)被杀。你需要编辑其Deployment,调整资源配额。一个中型集群的参考配置如下:

# kubectl edit deployment coredns -n kube-system
resources:
  limits:
    memory: "256Mi"
    cpu: "500m"
  requests:
    memory: "128Mi"
    cpu: "100m"

注意 : CPU请求不宜设得过低,否则在节点CPU竞争激烈时,CoreDNS处理查询的延迟会显著增加,表现为应用DNS解析超时。内存限制需要监控实际使用量来设定,如果CoreDNS频繁OOM,除了增加限制,还应检查是否存在异常的高频查询或环路。

4.2 性能调优:缓存与负载均衡

  • cache 插件 : 默认配置中的 cache 30 表示缓存TTL为30秒。对于解析结果几乎不变的内部服务,可以适当增加缓存时间(如300秒),以减少对API Server的查询压力。对于外部域名,需谨慎,因为过长的缓存可能导致IP变更不及时。你可以为不同区域配置不同的缓存策略。
  • loadbalance 插件 : 当Service对应多个Endpoint(Pod IP)时,此插件会对返回的A记录进行轮询(Round Robin)。这能在DNS层面实现简单的客户端负载均衡。大多数情况下应该开启。

一个调优后的 Corefile 片段示例:

.:53 {
    ...
    kubernetes cluster.local in-addr.arpa ip6.arpa {
       pods insecure
       fallthrough in-addr.arpa ip6.arpa
       ttl 30 # 可以适当增加,比如300,减少API查询
    }
    cache {
        success 300  # 成功响应缓存5分钟
        denial 30    # NXDOMAIN响应缓存30秒
    }
    loadbalance round_robin
    ...
}

4.3 监控与日志

  • 监控 : CoreDNS默认集成了 prometheus 插件,在 9153 端口暴露指标。你可以通过Prometheus收集这些指标,关键指标包括:
    • coredns_dns_request_count_total : 总请求数,按类型、区域统计。
    • coredns_dns_request_duration_seconds : 请求延迟分布。
    • coredns_dns_response_rcode_count_total : 响应码统计(NOERROR, NXDOMAIN, SERVFAIL等),帮助发现解析失败问题。
    • coredns_panic_count_total : CoreDNS崩溃次数。
  • 日志 errors 插件会将错误日志打印到标准输出。你可以通过 kubectl logs 查看。对于更详细的调试,可以启用 log 插件,但它会产生大量日志,仅建议在排查问题时临时开启。

4.4 常见故障排查场景与命令

当遇到“服务无法解析”或“解析超时”问题时,可以按照以下链路排查:

场景一:Pod内无法解析任何域名

  1. 检查CoreDNS Pod状态
    kubectl get pods -n kube-system -l k8s-app=kube-dns
    
    确保所有Pod都是 Running READY 2/2 (一个容器是CoreDNS,另一个通常是用于节点本地缓存的 node-cache ,如果部署了的话)。
  2. 检查CoreDNS Service
    kubectl get svc kube-dns -n kube-system
    
    确认Service存在且有ClusterIP(通常是 10.96.0.10 )。
  3. 进入问题Pod,检查DNS配置
    kubectl exec -it <problem-pod> -- cat /etc/resolv.conf
    
    确认 nameserver 指向的是正确的CoreDNS Service IP。
  4. 从Pod内测试DNS查询
    kubectl exec -it <problem-pod> -- nslookup kubernetes.default
    # 或者使用 busybox 镜像
    kubectl run -it --rm --restart=Never debug --image=busybox:1.28 -- nslookup kubernetes.default
    
    如果失败,尝试直接查询CoreDNS Pod IP(绕过Service):
    # 获取一个CoreDNS Pod的IP
    COREDNS_POD_IP=$(kubectl get pods -n kube-system -l k8s-app=kube-dns -o jsonpath='{.items[0].status.podIP}')
    kubectl exec -it <problem-pod> -- nslookup kubernetes.default $COREDNS_POD_IP
    
    如果直接访问Pod IP成功,但通过Service IP失败,问题可能出在kube-proxy或网络插件上(Service网络不通)。

场景二:可以解析外部域名,但无法解析内部服务

  1. 检查CoreDNS日志
    kubectl logs -n kube-system deployment/coredns --tail=50
    
    查看是否有关于 kubernetes 插件的错误,例如连接API Server失败。
  2. 检查RBAC权限 : CoreDNS需要相应的RBAC权限来访问API Server。通常这些配置在安装时已设置好,但如果被误修改,会导致无法查询服务信息。检查ClusterRole system:coredns 的权限。
  3. 检查 Corefile 配置 : 确认 kubernetes 插件配置的域( cluster.local )是否正确,是否覆盖了你需要解析的域名。

场景三:DNS解析间歇性超时或延迟很高

  1. 检查资源限制 : 如前所述,检查CoreDNS Pod的CPU请求是否过低,是否被其他Pod挤压。
  2. 检查 ndots 配置 : 这是最常见的坑之一。假设Pod内频繁查询一个包含4个点的外部域名(如 my.app.prod.company.com )。由于 ndots:5 ,点数4<5,系统会先尝试拼接 search 域,产生一系列无用的查询(如 my.app.prod.company.com.default.svc.cluster.local ),全部失败后才会发起原始查询,导致显著延迟。
    • 解决方案A(应用侧) : 在应用的连接字符串中,使用完全合格域名(FQDN),即以点结尾,如 my.app.prod.company.com. 。这样系统会直接查询,不走 search 域。
    • 解决方案B(集群侧) : 修改Pod或Namespace级别的DNS策略。可以为Pod Spec添加 dnsConfig ,降低 ndots 值或减少 search 域。但这需要修改应用部署配置。
    spec:
      dnsConfig:
        options:
          - name: ndots
            value: "2" # 降低ndots值
      dnsPolicy: None # 必须设置为None才能使用dnsConfig
    
  3. 检查网络插件 : 某些网络插件(如Calico、Cilium)的配置可能会影响Pod与CoreDNS Service之间的网络连通性或性能。

场景四:自定义上游域名解析失败

  1. 检查 Corefile 中的 forward 规则 : 确认IP地址和端口是否正确,上游DNS服务器是否可达。
  2. 从CoreDNS Pod内测试上游
    kubectl exec -it -n kube-system <coredns-pod> -- nslookup <your-internal-domain> <upstream-dns-ip>
    
  3. 检查网络策略 : 如果集群启用了NetworkPolicy,请确认是否有策略允许CoreDNS Pod访问上游DNS服务器的53端口。

通过以上结构化的排查步骤,大部分CoreDNS相关的问题都能被定位和解决。记住,在Kubernetes中,DNS问题常常表现为网络连通性问题,掌握其工作原理和排查工具是运维人员的必备技能。

更多推荐