前几周有个同事跑过来找我,说集群里有个Pod一直在报域名解析失败,公网域名比如baidu.com能通,但公司内网的一个GitLab地址怎么都解析不了。我第一反应不是去看网络,而是去查CoreDNS的配置——这种问题十有八九不是链路断了,是CoreDNS不知道该把这串域名交给谁处理。
是的,你没看错。K8s集群里的DNS解析,默认全部由集群内的CoreDNS负责。它既管集群内部服务的名字(比如service名、Pod名),也管集群外部的域名解析。而“外部域名”到底交给哪台DNS服务器来解,完全取决于我们给CoreDNS配置的上游DNS服务器是什么。配置得当,Pod内外域名通吃;配置不当,轻则解析超时,重则集群里一堆服务莫名其妙互访失败。
这篇文章就是要把“给CoreDNS配置上游DNS服务器”这件事讲透,从原理到实操,再到排错和一些进阶玩法,把我这些年踩过的坑一次说完。
1. CoreDNS在K8s里到底扮演什么角色
1.1 先搞清楚Pod是怎么“问路”的
在K8s集群里,每个Pod的/etc/resolv.conf并不是管理员手动指定的,而是由每个节点上的kubelet自动生成并写入。这里面最关键的一行是nameserver,它指向的是kube-dns这个Service的ClusterIP。
这里有个容易混淆的点:Service名是kube-dns,但真正干活的Pod是CoreDNS。kube-dns这个名称是历史遗留,早期K8s用的DNS插件叫kube-dns,后来被CoreDNS取代,但Service名一直保留着。所以你在集群里看到kubectl get svc -n kube-system里有个叫kube-dns的Service,它背后对应的Pod就是CoreDNS。
当你在一个Pod里执行nslookup my-service.default.svc.cluster.local时,流程是这样的:
- Pod读取自己的
/etc/resolv.conf,拿到nameserver(也就是kube-dns的ClusterIP,通常默认是10.96.0.10)。 - 请求到达CoreDNS。
- CoreDNS收到查询域名后,先看是不是
cluster.local后缀(集群内部域名),是的话直接查K8s API返回Service或Pod的IP。 - 不是集群内部域名,就按配置走转发逻辑,交给上游DNS服务器处理。
这个“交给上游”的动作,就是本文的核心所在。默认情况下,CoreDNS的forward插件会把查询转发到/etc/resolv.conf里指定的DNS服务器,而这个文件是容器镜像里带的,通常指向宿主机所在的DNS或公共DNS。具体值取决于你的集群安装方式,很多场景下它并不符合你的实际网络需求。
1.2 默认配置能解决什么、不能解决什么
默认配置下,CoreDNS能顺利完成的事包括:
- 解析集群内Service名,比如
nginx.default.svc.cluster.local; - 解析跨命名空间的Service名,比如
redis.middleware.svc.cluster.local; - 解析带
cluster.local后缀的Pod反向查询(in-addr.arpa部分); - 解析一些简单的公网域名,比如
www.baidu.com,前提是CoreDNS容器里的/etc/resolv.conf指向的DNS能递归解析公网域名。
但有三类常见需求,默认配置是搞不定的:
第一类,公司内网域名。很多企业有自建的DNS服务器,专门解析内部系统,比如gitlab.corp.local、harbor.registry.internal。这类域名在公网DNS上根本不存在记录,CoreDNS默认转发给公共DNS肯定返回NXDOMAIN。
第二类,跨VPC或专线互联的私有域名。比如你有一个数据库服务部署在另一套IDC,走专线和K8s集群互通,但域名需要由IDC侧DNS解析。这个地址只对IDC网络可见,公共DNS同样不认。
第三类,合规和审计要求。部分企业的安全策略要求所有DNS查询必须经过指定的内网DNS网关,不允许Pod直接向公共DNS发起解析。这个时候也必须把上游DNS改成公司指定的服务器。
所以,给CoreDNS配置正确的上游DNS服务器,本质上是让集群内DNS解析适配你的外部网络环境,而不是让CoreDNS真的“只靠自己解决所有解析”。
2. 动手前想清楚:你要上游帮你解析哪些域名
2.1 三种常见需求对照
不少人在配CoreDNS的时候有个误区:不加思考地填几个DNS IP就完事。实际上,你得先明确一个问题——你希望上游DNS帮你解析哪些域名?是全部域名,还是只有一部分特定域名?
我帮你把常见需求归成三类,可以直接对照你的场景来选:
| 需求类型 | 典型现象 | 上游配置建议 |
|---|---|---|
| 仅需要解析公网域名 | Pod访问外网应用,内网域名不需要 | 保持默认,确认CoreDNS容器能访问公网DNS即可 |
| 需要全局解析内网+公网域名 | 公司自建DNS既能解析内网域名,也能递归公网域名 | 把上游指向内网DNS,比如forward . 10.10.0.2 |
| 内网域名和公网域名分别走不同DNS | 内网域名只能由内网DNS解,公网域名不走内网DNS(或内网DNS不支持公网递归) | 用多个zone区分,比如forward corp.local 10.10.0.2,forward . 223.5.5.5 |
大多数中大型企业属于第三种。因为内网DNS服务器通常只负责内部域名,并不开放公网递归,或者出于安全策略,不允许把公网查询流量也送进内网DNS。
2.2 关键概念:Corefile里那个.代表什么
CoreDNS的核心配置在一个叫Corefile的文件里,它类似于Nginx的nginx.conf,通过区块来定义域名和插件逻辑。我们最常见到的一行配置是这样:
forward . 10.10.0.2这里的.不是单纯的“句中句号”,它代表根域,也就是“所有未匹配的域名”。CoreDNS的匹配逻辑是从最长后缀开始匹配查询域名,如果没有更具体的zone匹配,就走.这个根域配置。
举个例子,假设你在Corefile里同时定义了:
corp.local:53 { forward . 10.10.0.2 } .:53 { forward . 223.5.5.5 }当你查询gitlab.corp.local时,CoreDNS发现corp.local这个zone存在,所以走第一个区块,把请求发给10.10.0.2。当你查询www.baidu.com时,没有匹配到corp.local,于是走.区块,把请求发给223.5.5.5。
理解了这一点,你就能玩出很多花样。但要注意,多个zone的配置区块之间不要互相覆盖,否则会出现“配了等于没配”的情况。比如你写了一个corp.local区块,却在里面配了forward . 10.10.0.2,又在.:53区块里也把上游指向公共DNS,那corp.local域名的解析确实走了内网DNS,但如果你内网DNS解析不了其他内网域名(比如office.corp.local),一样会失败。正确做法是把你所有内网域名后缀都在zone里表达清楚。
2.3 一个容易被忽略的问题:上游DNS服务器IP从哪来
在动手配置之前,先确认你要填进去的DNS服务器IP是否真的可连通。很多人配完之后发现不起作用,结果一查,上游DNS的IP写错了,或者是防火墙阻断了53端口的UDP流量。
我建议用下面这个顺序逐层确认:
- 在集群节点上执行
nslookup gitlab.corp.local 10.10.0.2,确认节点本身能通过这台DNS解析目标域名; - 在节点上执行
ping 10.10.0.2,确认网络能通; - 如果节点能通,但进入CoreDNS Pod后不通,检查节点安全组、Calico/Cilium网络策略是否限制了Pod网段访问该IP的53端口。
第3点特别容易踩坑。K8s集群里Pod访问外部IP时,流量要经过节点SNAT,如果安全组只放行了节点IP访问DNS端口,但没放行Pod网段(虽然SNAT后会变成节点IP,但有些防火墙会按源连接追踪的原始信息判断),也可能失败。反正,填上游IP之前先做通联测试,能省后面一大半排错时间。
3. 实操:给CoreDNS配上真正好用的上游
3.1 先摸清当前CoreDNS配置
动手之前,先看看CoreDNS现在是啥状态。两条命令搞定:
kubectl get cm coredns -n kube-system -o yaml你会看到类似这样的输出:
apiVersion: v1 data: Corefile: | .: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 cache 30 loop reload loadbalance } kind: ConfigMap metadata: name: coredns namespace: kube-system再确认一下CoreDNS Pod当前用的上游是什么:
kubectl -n kube-system get pod -l k8s-app=kube-dns kubectl -n kube-system exec <coredns-pod-name> -- cat /etc/resolv.conf默认情况下,forward . /etc/resolv.conf表示CoreDNS容器使用自身/etc/resolv.conf里的nameserver作为上游。而这个文件的内容,通常继承自节点的DNS配置,或者是集群安装工具写入的。
在这个阶段,你把/etc/resolv.conf里的内容记录下来,后面无论是改还是排错,都能作为对比基线。
3.2 修改Corefile的两种姿势
现在到了正式动手改配置的环节。有两种常见姿势,我分别说下优劣。
第一种,直接用kubectl edit修改ConfigMap:
kubectl edit cm coredns -n kube-system这个方式简单直接,但有个隐患:如果你对YAML格式不熟,改错一个缩进,整个ConfigMap会出问题,如果又保存了,会导致CoreDNS重启失败。我见过不少同事在这个环节把集群DNS搞挂过。
第二种,先导出为文件,编辑后用kubectl apply提交:
kubectl get cm coredns -n kube-system -o yaml > coredns-cm.yaml vim coredns-cm.yaml kubectl apply -f coredns-cm.yaml个人推荐第二种。因为它有文件留底,万一改出问题,还能用文件快速回滚。另外,不要把resourceVersion和uid等元数据保留在apply文件里,导出来后先把这些字段清理掉,参考下面的精简结构:
apiVersion: v1 kind: ConfigMap metadata: name: coredns namespace: kube-system data: Corefile: | .: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 . 10.10.0.2 223.5.5.5 { policy random health_check 5s max_concurrent 1000 } cache 30 loop reload loadbalance }改完后,直接apply。但这里有个很重要的知识点:CoreDNS默认不会因为ConfigMap变化就自动重载配置。虽然上面的Corefile里有reload插件,但它实现的是周期性地检查配置文件变化,默认30秒一次,而且它依赖CoreDNS进程直接读取文件内容。在K8s里,ConfigMap更新后,如果CoreDNS容器没有以subPath方式挂载配置文件,reload插件不一定能可靠感知变更。为了确保生效,最稳妥的办法是重启CoreDNS:
kubectl -n kube-system rollout restart deployment/corednsrollout restart会触发滚动更新,旧Pod退出、新Pod启动,新Pod启动时会重新读取ConfigMap里的最新配置。
3.3 配置参数详解:这些数字别随便填
好,现在我们把上面Corefile里的forward块单独拎出来看:
forward . 10.10.0.2 223.5.5.5 { policy random health_check 5s max_concurrent 1000 }这里的每个参数都是可以调的,我分别说明一下。
域名匹配区(.):表示所有未匹配的查询都转发到后面的服务器列表。如果你只想让特定域名走某个上游,把.换成具体后缀即可,比如forward corp.local 10.10.0.2。
上游DNS列表(10.10.0.2 223.5.5.5):支持填多个IP。CoreDNS会按选定的策略在它们之间分配查询流量。注意,这里填的IP必须能直接访问,不能填需要递归才能找到的域名地址。
policy random:随机选择上游。可选值有random、round_robin、sequential。我一般推荐random,能有效避免固定使用第一台DNS导致高负载堆积。如果你的上游有多台且性能不均衡,可以用round_robin轮询。
health_check 5s:每隔5秒对上游做一次健康检查。具体检查方式是发送一个.的DNS查询(根域查询),如果上游返回响应(哪怕SERVFAIL)都算健康,只有超时或无响应才标记为故障。被标记为故障的上游会被自动摘除,直到恢复健康。这个参数的值不宜太大,否则故障转移不及时;也不宜太小,否则健康检查流量会干扰上游。5秒是比较均衡的选择。
max_concurrent 1000:限制同时转发给上游的最大并发查询数。超过这个数后,额外的查询会直接被丢弃并返回SERVFAIL。这其实是一个“熔断保护”机制,防止上游高负载打爆CoreDNS整个进程。如果你集群规模很大,比如每秒DNS QPS有几千,可以把1000调大,比如2000或5000,但也要配合上游的实际处理能力,否则你只是把队列问题向后推移。
这里还得提一下时间参数:CoreDNS默认的转发超时是10秒,也就是说上游10秒不响应,CoreDNS会返回超时失败。如果你在业务里碰到偶尔的DNS超时,可以把超时设置加长。但更推荐的做法是增加健康检查,让有问题的上游被提前摘除,而不是拉长等待时间。
3.4 常见坑:配置明明改了,Pod却还是解析失败
配置改完、重启完,最让人崩溃的是什么?Pod里一测,还是老样子。我来盘点几个我实际遇到过的原因。
坑一:nodelocaldns插件遮蔽了上游配置。
很多K8s集群会安装nodelocaldns组件(也叫NodeLocal DNSCache),它会在每个节点上运行一个本地DNS缓存,并把Pod的DNS请求拦截到本机。这种情况下,真正和上游DNS打交道的是nodelocaldns,而不是CoreDNS。
也就是说,你改了CoreDNS的上游,但如果nodelocaldns自己还缓存着旧结果,或者它的上游配置没有跟着变,Pod端还是拿到旧结果。改这类集群的配置,要同时查看kubectl get cm -n kube-system node-local-dns之类的配置。
鉴别方法很简单:看Pod的/etc/resolv.conf里nameserver指向的是不是169.254.20.10或者节点IP。如果是,说明有nodelocaldns拦截。排错时一定要把这个组件考虑进来,否则你会被“改配置没效果”这个假象折磨很久。
坑二:改的是ConfigMap,但CoreDNS副本数不对。
有些集群把CoreDNS设置成单个副本部署,你在滚动重启的瞬间,旧Pod停了,新Pod还没就绪,会导致短暂DNS中断。更糟的情况是,如果你用的镜像有拉取问题,新Pod一直Pending,集群DNS就废了。
所以重启前,先看下副本数:
kubectl -n kube-system get deploy coredns如果是单副本,建议先临时扩成两副本再重启:
kubectl -n kube-system scale deploy coredns --replicas=2 kubectl -n kube-system rollout restart deploy/coredns kubectl -n kube-system scale deploy coredns --replicas=<原副本数>虽然CoreDNS本身很轻量,但“DNS服务重启期间断解析”这个窗口期,放在生产环境完全是不可接受的。
坑三:上游DNS本身就不支持递归解析公网域名。
前面说了,内网DNS服务器不一定支持公网递归。如果你把上游全部改成内网DNS,公网域名解析就会变慢甚至失败。CoreDNS会返回SERVFAIL。这类问题在配置前就该通过需求分析(第2节)规避掉,但如果已经改了,建议立即在Pod里测试一下公网域名:
kubectl run -it --rm dns-test --image=busybox --restart=Never -- nslookup www.baidu.com如果返回SERVFAIL或超时,多半就是上游不支持公网递归。解决方法是加一个能解析公网域名的备用上游,比如forward . 10.10.0.2 223.5.5.5,让CoreDNS自己实现故障转移。
4. 验证配置是否生效和问题排查
4.1 在Pod内验证解析结果
配置改了、部署重启了,现在要验证。最老土也最直观的方法是起一个临时Pod进去测试:
kubectl run -it --rm test-dns --image=busybox:1.36 --restart=Never -- sh进去后依次跑:
nslookup kubernetes.default.svc.cluster.local nslookup gitlab.corp.local nslookup www.baidu.com这三条分别验证集群内部域名、内网上游域名、公网域名。三者都通,说明配置基本没问题。
有些人习惯用dig命令,但busybox镜像里没有dig,需要额外装bind-tools,比较麻烦。nslookup在busybox里就有,够用了。
另外一个更精确的验证方法,是直接在CoreDNS Pod里测试其对上游的解析能力:
kubectl -n kube-system exec <coredns-pod-name> -- nslookup gitlab.corp.local 10.10.0.2这条命令的意思是:绕过CoreDNS自身的转发逻辑,直接让CoreDNS容器请求10.10.0.2这台DNS。如果这里能解析,说明网络通、上游DNS工作正常;如果这里不行,那问题出在CoreDNS配置之前——要么是上游IP写错了,要么是网络策略拦截。
4.2 常见问题速查表
遇到问题别慌,先对照下面这个表定位方向:
| 现象 | 可能原因 | 解决方式 |
|---|---|---|
| 集群内Service名解析失败 | CoreDNS的kubernetes插件配置异常或API Server连接异常 | 查看CoreDNS日志,检查kubectl get ep -n kube-system kube-dns是否正常 |
| 内网域名解析到公网IP | 内网域名前缀没匹配到corp.local zone,走了默认上游 | 检查Corefile里zone配置是否覆盖了所有内网后缀 |
| 公网域名解析超时 | 上游DNS不支持公网递归,或上游IP不可达 | 修改forward,加入公网DNS作为备选,比如forward . 10.10.0.2 223.5.5.5 |
| 部分Pod解析正常、部分Pod超时 | nodelocaldns缓存或Pod网络策略差异 | 检查是否有nodelocaldns,检查Pod之间的网络策略 |
| CoreDNS Pod日志大量i/o timeout | CoreDNS到上游DNS的链路不稳定 | 在节点上测试到上游的连通性,检查安全组和MTU |
| 改了配置但长时间不生效 | 没重启CoreDNS,或reload插件不可靠 | 执行kubectl rollout restart deployment/coredns -n kube-system |
4.3 排错链路三段法
我给团队培训的时候,习惯把CoreDNS排错拆成三段,遇到问题按顺序排查,效率很高。
第一段:Pod到CoreDNS。
在问题Pod里执行:
cat /etc/resolv.conf nslookup kubernetes.default.svc.cluster.local如果/etc/resolv.conf里的nameserver不是kube-dns的ClusterIP,可能是这个Pod用了dnsPolicy: Default或dnsPolicy: None。如果nameserver正确但解析失败,可能是CoreDNS本身挂了,或者网络组件(Calico/Cilium的kube-proxy规则)有问题。这一段的关键是确认“请求有没有到CoreDNS”。
第二段:CoreDNS到上游DNS。
在CoreDNS Pod里直接请求上游:
kubectl -n kube-system exec <coredns-pod-name> -- nslookup gitlab.corp.local 10.10.0.2如果失败,看返回的是connection timed out还是SERVFAIL。超时是网络不通,SERVFAIL是上游已经响应但拒绝权威解析。这两个方向完全不同:超时查网络和安全组,SERVFAIL查上游DNS配置和授权情况。
第三段:上游DNS到最终权威源。
如果上游正常,但解析结果不对,需要在上游DNS服务器本身上测试它解析目标域名的情况:
nslookup gitlab.corp.local 127.0.0.1上游DNS返回的不对,有可能是它自身配置了错误记录,也可能是它向上层权威DNS查询时失败。这一步通常需要和网络/基础架构团队协同处理。
三段定位法不做多余猜测,大多数DNS问题都能在半小时内定位到具体环节。
5. 进阶:多上游、分场景配置,做一个稳的集群DNS
5.1 分支解析:内网域名和公网域名各走各路
如果你的内网DNS不支持公网递归,又想保留集群访问公网域名的能力,那就得用多zone方案。看这个Corefile示例:
corp.local:53 { errors forward . 10.10.0.2 } office.example.com:53 { errors forward . 10.10.20.5 } .: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 cache 30 loop reload loadbalance }这个配置的巧妙之处在于:corp.local和office.example.com这两个内网zone各自指定了不同的内网DNS,其他域名走默认的.:53区块,使用公共DNS递归。看起来简单,但在实际生产中非常实用。
需要注意一个细节:当一个内网域名既匹配corp.local,又匹配更长的corp.local子域(比如ops.corp.local)时,CoreDNS会按照最长后缀优先的规则,选择最具体的zone。所以如果你有复杂的内网域名层级,zone要写全,避免走了错误的默认分支。
5.2 缓存和DNSSEC:别让上游处理太多重复请求
CoreDNS本身有cache插件,默认配置里一般写cache 30,意思是正解析结果缓存30秒。这个值考虑的是“业务域名变更后能较快感知”和“降低上游压力”之间的平衡。如果你在调整上游DNS配置后,发现DNS查询QPS很高、上游压力大,可以适当调大,比如cache 120。但注意,如果你业务里有域名频繁变更的场景,缓存时间太长会导致Pod一直拿到旧IP,这比解析慢更讨厌。
另外,默认配置里通常不会开启DNSSEC。DNSSEC能防止DNS劫持,但要求上游DNS支持,且会增加解析延迟和故障复杂度。在内网环境里,很多DNS服务器根本不支持DNSSEC,强行开启会导致解析失败。我的建议是:先确认上游支持情况再决定要不要开,默认保持关闭即可。
还有一个小技巧:如果内网域名解析后的TTL非常短(比如1秒),而上游又没有做本地缓存,CoreDNS的cache会让这1秒的TTL变成30秒,实际效果是业务拿到的解析结果TTL被放大。这个在排查“为什么改了DNS记录,Pod半天不生效”时很有用。
5.3 从可观测性角度看集群DNS健康
配置完上游DNS,不是“能用”就行,还得“看得见”。强烈建议把CoreDNS的监控指标接入Prometheus,在Grafana里配个面板。CoreDNS会在9153端口暴露metrics,前提是Corefile里配置了prometheus :9153。
几个核心指标可以重点盯:
coredns_dns_request_count_total:总请求量,按zone和type区分,能看出哪些域名查询最多;coredns_forward_request_duration_seconds:转发到上游的耗时,如果这个值长期处于高位,说明上游响应慢;coredns_forward_healthcheck_failure_count_total:上游健康检查失败次数,如果持续增长,说明上游不稳定;coredns_dns_response_rcode_count_total:按返回码区分,重点关注SERVFAIL和NXDOMAIN占比。
为什么要强调可观测?因为DNS是“故障放大器”。上游慢100毫秒,对单次请求来说还好,但一个Pod里如果有多个域名查询,并发一上去,应用整体请求就会被拉垮。而这种问题在故障发生时,业务方大概率报的是“接口变慢”,不容易联想到DNS。有了指标和数据,排查效率会高很多。
我遇到过最典型的案例:某个内网DNS服务器升级后,开启了递归限速,CoreDNS的forward请求超时率飙升,但Pod内看起来只是“偶尔慢”。当时就是通过Grafana面板看到coredns_forward_request_duration_seconds从原来的10ms涨到800ms,才定位到根因。
结尾
配置CoreDNS上游DNS这件事,看着简单,真到线上环境里细节还挺多的。我个人这些年比较大的体会是:改Corefile之前,一定要先理清自己网络环境里有哪些域名、该走哪台DNS,不要想当然地填几个IP上去。特别是多网络互通、混合云部署的场景,一个错误的上游配置会引发连锁故障。
最后再分享一个小习惯:每次改完CoreDNS配置,我都会顺手建一条变更记录,写明改了哪个zone、上游从什么改成什么、原因是什么。CoreDNS配置在K8s里就是个ConfigMap,改起来太容易了,但也因为太容易,很多人改完就忘了。等到三个月后集群DNS出问题,谁能记得当时为什么配了一个奇怪的上游IP?记录留好,排查时能省下不少时间。