☰
Kubernetes DNS 链路深潜:从 resolv.conf、ndots 到 CoreDNS 与无头服务
2026/10/17 7:48:53 网站建设 项目流程

上周帮某项目组排查一个 Pod 里访问另一个服务偶发超时的问题,查到最后发现 CoreDNS 一直活着、Service 也正常,真正的坑出在“搜索域”和“ndots”上。这让我想把 Kubernetes Service DNS 这条链路从头到尾完整梳理一遍。说实话,很多人能背出 Service 的域名格式是service.namespace.svc.cluster.local,但一旦追问“这个域名是谁拼出来的”“kubelet 在里面扮演什么角色”“无头服务的 A 记录为什么直接返回 Pod IP”,就开始模糊了。这篇文章就把这些点全部串起来,从 kubelet 写入/etc/resolv.conf到 CoreDNS 返回记录,再到无头服务和 SRV 记录,一整套东西讲清楚,适合已经被各种 DNS 问题折磨过、或者准备深入理解 Kubernetes 服务发现机制的开发者阅读。

1. 从 kubelet 到 CoreDNS:一条 DNS 链路的前世今生

1.1 为什么服务发现非要靠域名

Kubernetes 里的 Pod 是有“生命”的,随时可能因为滚动更新、节点故障、HPA 扩缩容而被销毁重建,每次重建 IP 都会变。直接用 IP 访问服务,等于用手写便签记一个大象的脚印,没两天就对不上了。而 Service 提供了一个相对稳定的虚拟 IP,但 IP 本身依然可能因为 Service 重建而变化。

所以 Kubernetes 引入了“服务即域名”这套机制:把 Service 名字映射成域名,把域名解析交给集群内置的 DNS 系统。这样上层应用只需要记住order-service或者order-service.default.svc.cluster.local这种名字,IP 怎么变都无所谓,域名永远指向当前的 Service IP。这套机制在云原生架构里就是服务发现的基石。

1.2 kubelet 是第一个“DNS 配置写入者”

很多人的认知里,Kubernetes DNS 就是 CoreDNS 的事,其实链路最早的一环是 kubelet。每个 Pod 创建时,kubelet 会替这个 Pod 生成一份/etc/resolv.conf,通过容器运行时注入到 Pod 的文件系统里。这份文件决定了 Pod 内所有 DNS 查询请求最终会发给谁。

默认情况下,这份resolv.conf长这样:

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

nameserver指向的10.96.0.10就是 kube-dns Service 的 ClusterIP,也就是 CoreDNS 服务对外暴露的虚拟地址。只要 CoreDNS 的 Pod 还在,这个 IP 就不会变,Pod 里的应用只管把请求发给它就行。

search那一行是关键中的关键。它决定了你只输入一个短域名(比如order-service)时,系统会依次尝试拼上哪些后缀。默认会把“当前 Pod 所在命名空间的 Service 域”、svc.cluster.local、cluster.local都加进去。这也是同一个 Namespace 下的服务之间可以直接互访的原因——不用写全域名,短名字就能解析。

options ndots:5则是一个容易被人忽略的细节。它告诉解析器:如果待查询的域名里“点”的数量少于 5 个,就先不要当完整域名(FQDN)去查,而是依次拼接搜索域去尝试。比如查询order-service这种短名字,解析器会拿着它去拼default.svc.cluster.local、svc.cluster.local等,直到命中。这个机制极大方便了应用开发,但也是很多诡异解析问题的源头,后面专门展开讲。

1.3 CoreDNS 本身也是跑在集群里的普通 Pod

CoreDNS 不是什么独立的外部服务,它同样是跑在 Kubernetes 里的 Deployment,通常位于kube-system命名空间。它的 Pod 通过一个名为kube-dns的 Service 对外提供解析能力,Pod 内resolv.conf里的nameserver 10.96.0.10,就是这个 Service 的 ClusterIP。

CoreDNS 通过监听集群 API 的变化,动态维护一份“DNS 记录表”。当你在集群里创建了一个新的 Service,CoreDNS 会自动生成对应的 A 记录,不需要人工去加。这也是云原生 DNS 系统与传统 DNS 最大的不同:记录不是手动维护的,而是声明式生成的。

CoreDNS 本身可以通过 ConfigMap 配置,核心配置文件叫Corefile,支持加载不同的插件,比如kubernetes插件专门负责集群内部域名解析,forward插件负责把集群外部的域名请求转发给上游 DNS 服务器。实际排障时,查看 CoreDNS 的日志和配置能发现很多隐蔽问题。

2. Service 即域名:记录类型与命名规则详解

2.1 Service A 记录:标准域名结构到底长什么样

一个普通的 Service 创建后,CoreDNS 会自动给它注册一条 A 记录,完整的域名结构是:

<service-name>.<namespace>.svc.<cluster-domain>

默认的cluster-domain是cluster.local。举个例子,default命名空间里有一个名叫order-service的 Service,那么它的完整域名是:

order-service.default.svc.cluster.local

在集群内部,同一个命名空间下的 Pod 直接用order-service就能访问;跨命名空间则需要写全一些,比如order-service.prod.svc.cluster.local。

这里有一点很多人会搞混:Service 的 A 记录解析出来的是 ClusterIP,而不是某个具体 Pod 的 IP。ClusterIP 是 Kubernetes 在 Service 创建时分配的虚拟 IP,它只存在于 kube-proxy 的规则里,没有对应的物理网卡。但你访问这个 IP 时,kube-proxy 会把流量转发给后端的某个真实 Pod。

除了普通 Service,还有两种特殊情况值得留意:ExternalName类型的 Service 不会分配 ClusterIP,它的 DNS 记录直接是 CNAME,指向你在externalName字段里写的那个外部域名;而ClusterIP: None的无头服务,它没有 ClusterIP,DNS 返回的是后端 Pod 的真实 IP,这个机制在下一节单独讲。

2.2 Pod 的 A 记录:连 Pod 也有域名

严格说,Kubernetes DNS 不只是给 Service 生成记录,它也会给 Pod 生成 A 记录。Pod 域名的格式是:

<pod-ip-with-dashes>.<namespace>.pod.<cluster-domain>

比如某 Pod 的 IP 是10.244.1.15,所在命名空间是dev,那它的域名就是:

10-244-1-15.dev.pod.cluster.local

这条记录的核心用途是:在某些场景下,你确实需要绕过 Service,直接访问某个 Pod 的 IP。典型就是 StatefulSet 部署的有状态应用,比如 Kafka、Elasticsearch、Cassandra,它们需要知道彼此的真实地址,而不是经过 Service 做负载均衡。

不过 Pod 的域名不是必须存在的。它取决于你是否启用了publishNotReadyAddresses,以及 Pod 是否满足 DNS 记录生成的条件。实践中更常用的还是无头服务来暴露有状态 Pod 的集合。

2.3 给端口命名:影响远比想象中大

在 Service 的 YAML 定义里,端口可以写port,也可以写name。很多人在写常规 Service 时会偷懒不写name,但如果你后续要用到 SRV 记录,端口名就是必需品。

apiVersion: v1 kind: Service metadata: name: order-service spec: selector: app: order ports: - name: http port: 80 targetPort: 8080 - name: metrics port: 9100 targetPort: 9100

这只是给端口起个名字而已,为什么值得强调?因为 SRV 记录的格式是:

_<port-name>._<protocol>.<service-name>.<namespace>.svc.<cluster-domain>

如果没有端口名,这条 SRV 记录就生成不出来。这直接影响依赖服务自动发现的场景,比如 gRPC 的负载均衡策略里经常用dns:///协议,通过 SRV 记录找到某个服务的特定端口。后面无头服务一节会更加清晰。

2.4 一个 Service DNS 解析的完整流程

现在站在 Pod 内部视角,看看访问另一个 Service 时系统做了哪些事。假设 Pod A 里有个应用去请求http://order-service:8080:

  1. 应用层拿order-service发起 DNS 查询请求。
  2. 系统解析/etc/resolv.conf,发现ndots:5,order-service的点数少于 5,所以先拼上搜索域去尝试。
  3. 依次尝试order-service.default.svc.cluster.local、order-service.svc.cluster.local、order-service.cluster.local。
  4. 第一次拼上了order-service.default.svc.cluster.local,向10.96.0.10(CoreDNS)发起查询。
  5. CoreDNS 查看到这条记录存在,返回对应的 ClusterIP。
  6. 应用拿到 IP,建立 TCP 连接,请求经过 kube-proxy 转发到后端 Pod。

这个流程看起来很流畅,但搜索域的逐个尝试机制,在某些外部域名场景下会产生多余的 DNS 查询,甚至导致解析时间变长。这正是开头提到的那个偶发超时问题的大背景。

3. 无头服务与 SRV 记录:直接摸到 Pod IP 的手段

3.1 无头服务:ClusterIP 被拿掉以后发生了什么

普通 Service 有 ClusterIP,DNS 解析返回的是这个虚拟 IP。无头服务(Headless Service)则不同,它显式把clusterIP设为None,不分配虚拟 IP,也不做负载均衡。它的作用退化为“一组 Pod 的DNS 注册表”。

创建无头服务的 YAML 长这样:

apiVersion: v1 kind: Service metadata: name: mongo-headless spec: clusterIP: None selector: app: mongo ports: - name: mongo port: 27017

此时你查询mongo-headless.default.svc.cluster.local,DNS 返回的不是一个虚拟 IP,而是这个 Service 选择器匹配到的所有 Pod 的 IP 列表。也就是说,通过无头服务,你获得的是后端每个真实 Pod 的地址,而不是一个入口。

为什么需要这种能力?以 MongoDB 副本集为例,副本集里的每个节点都需要知道其他节点的真实地址,才能完成选举和同步。如果用普通 Service,所有节点看到的是同一个 ClusterIP,无法区分彼此。无头服务则让所有节点通过 DNS 拿到全部成员的 IP,从零开始建立集群关系。

3.2 StatefulSet 场景下的“稳定域名”

无头服务和 StatefulSet 组合在一起,能发挥出更强大的能力。StatefulSet 创建的每个 Pod 都有一个稳定的网络标识,即Pod 名称。如果一个 StatefulSet 的名字叫kafka,无头服务叫kafka-headless,那么每个 Pod 的域名是:

kafka-0.kafka-headless.default.svc.cluster.local kafka-1.kafka-headless.default.svc.cluster.local kafka-2.kafka-headless.default.svc.cluster.local

这个域名不会随着 Pod 重建而改变。kafka-0 被删掉后重新创建,新 Pod 仍然叫 kafka-0,域名依然是上面那个。这样有状态应用内部的节点之间,可以用稳定的主机名互相访问,而不是用会变的 IP。

这也是分布式系统里经典的做法之一:无头服务提供“所有成员地址”,带序号的 Pod 域名提供“每个成员稳定地址”,两者结合,支撑起有状态集群的自动组网。

3.3 SRV 记录:按端口名精准定位服务实例

SRV 记录是 DNS 的一种记录类型,用来指定某服务运行在哪台主机、哪个端口上。格式是:

_<service>._<protocol>.<name>.<namespace>.svc.<cluster-domain>

在 Kubernetes 里,<protocol>取值为tcp或udp,<service>就是你给 Service 端口定义的name。

用 dig 直接看效果:

dig SRV _http._tcp.order-service.default.svc.cluster.local

返回结果大致是:

;; ANSWER SECTION: _http._tcp.order-service.default.svc.cluster.local. 5 IN SRV 0 100 8080 order-service.default.svc.cluster.local.

这条记录表达的信息很直接:要访问order-service的http端口,连到order-service.default.svc.cluster.local的8080端口就行。SRV 记录对 gRPC、etcd 这类需要自动发现对端协议和端口的系统非常有用,应用不需要硬编码端口号,靠 SRV 记录就能动态获知。

这里的关键提醒再次出现:如果你没有给端口命名,SRV 记录就不会生成。很多人在排查 gRPC 服务发现失效问题时,第一步就该检查 Service 的端口有没有name。

3.4 无头服务的 A 记录和 SRV 记录有什么区别

对比一下会更清晰:

记录类型返回内容典型用途
无头服务 A 记录所有 Pod IP 列表客户端做自定义负载均衡、健康检查
无头服务 SRV 记录端口 + 域名客户端动态发现服务的具体端口
有头服务 A 记录ClusterIP常规服务访问入口

联系到 Cassandra 这类系统,客户端通常先通过无头服务的 A 记录拿到所有节点的 IP,再向上层应用暴露节点列表。而 gRPC 客户端则更偏好 SRV 记录,因为它是按“协议 + 端口名”去查询的,天然契合 RPC 服务的发现逻辑。

4. 搜索域 search domains:DNS 查询的“默认路径”

4.1 ndots 和搜索域的组合逻辑

很多人第一次看到/etc/resolv.conf里的options ndots:5时完全没感觉,直到遇到解析超时才回头研究它。这里我尽量用最直白的话说清楚。

ndots:5的意思是:域名中“点”的数量如果小于 5,就认为这是一个不完整的主机名,先尝试拼接/etc/resolv.conf里的每个搜索域,只有这些尝试都失败后,才把原始域名作为完整域名再查一次。这里的“点”不能凭直觉数,像order-service这样的名字,包含 1 个点,小于 5,所以会触发搜索域拼接流程。

默认的搜索域有三个:

  • default.svc.cluster.local
  • svc.cluster.local
  • cluster.local

以查询order-service为例,解析器会依次发起:

order-service.default.svc.cluster.local order-service.svc.cluster.local order-service.cluster.local order-service # 最后作为 FQDN 兜底

如果第一个就命中了,后面的请求就不会发生。但如果第一个没有这条记录,就会依次发完所有尝试,才返回最终结果。这个多步查询过程,在某些网络条件下会导致延迟显著增加。

4.2 多一个搜索域的代价在哪里

搜索域机制的负面效应,在访问外部域名时尤其明显。比如 Pod 里的应用要访问www.example.com,这个域名里有 2 个点,少于 5,解析器会先拼接搜索域去尝试:

www.example.com.default.svc.cluster.local www.example.com.svc.cluster.local www.example.com.cluster.local

前面三条在 CoreDNS 的 kubernetes 插件里都会查无此记录,产生三次 NXDOMAIN 应答,之后才会把www.example.com当成完整域名,转发给上游 DNS 拿到真正的 A 记录。

这意味着一次完整的外部域名解析,至少要经历 4 次 DNS 查询往返。如果 Pod 里的应用做了很多外部域名请求,网络状况又不好,垃圾查询请求会成倍增加,最终表现为“服务访问慢”“偶发超时”。曾经有一个生产事故,就是这个原因导致应用大量请求超时,当时排查了很久才定位到搜索域叠加的问题。

4.3 代理场景下搜索域机制被人为绕过的坑

另一个常见问题是,很多应用没有通过系统解析器去查 DNS,而是自带了解析逻辑。比如 Go 语言编写的程序,如果使用了net.DefaultResolver,通常会遵循系统的resolv.conf。但一些框架或 SDK 会直接把域名交给 gRPC 的 dns resolver,后者有时会强制把域名当作 FQDN 来解析,不做搜索域拼接,结果就是两个服务明明在同一个命名空间,用短名字却访问不通。

这类问题排查时最有效的办法,是在 Pod 里手动测试再对比应用行为。手动测试通过不代表应用里的解析代码和你一样,环境一致性差时,优先查应用自己的 DNS 解析逻辑。

4.4 怎么调整搜索域和 ndots 参数

搜索域和 ndots 默认值对大多数场景是合理的,但不排除个别业务需要调整。可以通过修改 kubelet 的启动参数来全局覆盖 Pod 的 DNS 配置,也可以手动给 Pod 指定dnsConfig。

给 Pod 单独指定 ndots 的写法:

spec: containers: - name: app image: nginx dnsConfig: options: - name: ndots value: "2"

修改搜索域:

spec: dnsConfig: searches: - custom.local - default.svc.cluster.local

这里有个实操心得:除非你明确知道搜索域为什么会拖慢解析,否则不要随便把 ndots 改成 2。某些场景下改为 2 可以大幅减少外部域名查询次数,但同时会让短服务名的解析行为变得和预期不同——因为现在超过 2 个点的域名会直接作为 FQDN 查询,不再尝试搜索域。前后行为不一致反而引发新问题。建议改完先在小范围灰度验证,再逐步放开。

5. 排查与验证:从 Pod 到 ClusterIP 的一路顺手工具

5.1 没有 dig 的时候,先学会 nslookup

真的遇到线上排障,最尴尬的是 Pod 里连个像样的 DNS 调试工具都没有。很多基础镜像基于 Alpine Linux,只内置了nslookup,没有dig。不少人的第一反应是跳进 Pod 装工具,但线上环境不建议这么做,镜像里的包管理器和网络限制会浪费大量时间。更靠谱的做法是,起一个一次性的调试 Pod。

kubectl run dns-tools --image=registry.example.com/dnsutils:latest --rm -it --restart=Never -- bash

这个dnsutils镜像里集成了dig、nslookup、nslookup的更多替代品。如果没有现成的私有镜像,也可以直接用一个通用调试镜像。进入 Pod 后,先查基础信息:

cat /etc/resolv.conf

看输出的nameserver、search和options是否符合预期。这一步能快速确认 kubelet 有没有正确注入 DNS 配置。

5.2 常用解析验证命令与输出解读

基础验证:

nslookup order-service.default.svc.cluster.local

输出主要是 A 记录和权威应答的地址。查询 Pod 域名:

dig +short 10-244-1-15.default.pod.cluster.local

查询无头服务的记录,看看返回了几个 IP:

dig +short mongo-headless.default.svc.cluster.local

预期返回多个 IP,每个 IP 对应一个后端 Pod。如果只返回了一个 IP,那说明 Service 的 selector 可能没匹配到所有 Pod,或者存在不可用 Pod。

查询 SRV:

dig SRV _mongo._tcp.mongo-headless.default.svc.cluster.local

看返回的端口、权重和域名。如果报No records,先检查 Service 端口有没有配name。

5.3 CoreDNS 视角的排障方法

当 Pod 内解析正常,但应用依然报域名解析失败,问题可能出在 CoreDNS 本身。首先看 CoreDNS 的 Pod 状态:

kubectl get pods -n kube-system -l k8s-app=kube-dns

确认所有副本处于 Running 状态。接着看日志:

kubectl logs -n kube-system deployment/coredns

CoreDNS 默认日志记录量不多,但查询错误会体现出来。如果想细化日志,可以修改 Corefile 开启详细日志,也会暴露一些缓存未命中的信息。

CoreDNS 的另一个重要排障点是它是否把外部域名正确转发给了上游。默认转发插件配置如下:

forward . /etc/resolv.conf

表示集群外部的域名请求会转发给/etc/resolv.conf里配置的上游 DNS。这里的/etc/resolv.conf是 CoreDNS 所在节点或容器自身的配置,不是 Pod 的。如果节点上的 DNS 配置有问题,CoreDNS 转发外部请求就会跟着失败。

5.4 一张速查表解决大部分 DNS 问题

问题特征可能原因排查步骤
同命名空间短域名解析失败Pod 的 search 域或 ndots 配置异常进入 Podcat /etc/resolv.conf,对比默认配置
跨命名空间访问不通域名没写全尝试service.namespace.svc.cluster.local完整域名
外部域名解析慢搜索域拼接导致多余 NXDOMAIN 查询用dig +trace观察查询次数,考虑调低 ndots
无头服务返回的 IP 不全Service selector 未匹配全部 Pod检查 Pod 标签和 Service 选择器是否一致
SRV 查询无结果端口没有命名检查 Service YAML 的ports[].name字段
CoreDNS 宕机或重启资源不足或配置错误查看 CoreDNS Pod 日志、事件、资源配置

5.5 一个典型排查实录

有个模拟项目 X 遇到过一个问题:A 服务调用 B 服务,短域名一直通,但偶尔超时。最初怀疑是 CoreDNS 问题,但看了 metrics 之后,发现 CoreDNS 的 QPS 并不高,缓存命中率也正常。后来进 Pod 里反复测试发现,A 服务每次解析 B 的短域名时,实际上额外发起了 3 次搜索域的查询请求。由于 A 服务所在的节点 DNS 策略和沙箱环境的网络策略叠加,偶发丢包,导致其中一次尝试超过了应用的超时阈值。

最后解决方式不是改 CoreDNS,而是给 A 服务的部署配置里加了dnsConfig,把访问频率最高的那个服务域名由短域名改成完整域名。完整域名里包含 2 个点,依然触发搜索域拼接,但相比短域名少了一次无谓尝试。优化后超时问题明显缓解。这个案例提醒我,DNS 问题排查不能只守着 CoreDNS,反倒要回到 Pod 视角去理解“搜索域”和“ndots”带来的额外查询成本。

6. 最后再讲几个容易翻车的细节

6.1 kubelet 的 DNS 配置策略是逐 Pod 生效还是全局生效

kubelet 提供了--cluster-dns和--cluster-domain参数,它们决定 kubelet 往每个 Pod 的/etc/resolv.conf里写入什么内容。改这个参数会影响到节点上所有新建 Pod 的 DNS 配置。如果集群规模大、节点数量多,改动前最好先评估影响面,优先用 Pod 级别的dnsConfig去渐进调整,避免全局变更带来不可控风险。

6.2 别忽略 Pod 的 hostname 和 subdomain

除了搜索域,Pod 的hostname和subdomain字段也会影响 DNS 解析行为。如果给 Pod 设置了subdomain,且对应的 Service 是无头的,那么 CoreDNS 会额外生成一条记录,形如:

<hostname>.<subdomain>.<namespace>.svc.<cluster-domain>

这个机制在有些分布式框架里被用来替代 StatefulSet 的稳定域名。但注意,如果 Pod 没有设置subdomain字段,解析器可能默认使用 Pod 名称作为主机名,查询结果和预期会有差异。遇到 DNS 解析结果和命名对不上时,先检查这两个字段是不是被隐式设置了。

6.3 CoreDNS 的缓存在多轮查询里扮演的角色

CoreDNS 默认开启缓存插件,但缓存是针对完整查询名的。每次搜索域拼接后的查询名不同,比如order-service.default.svc.cluster.local和order-service.svc.cluster.local是两条独立缓存项,不会互相覆盖。这意味着高频应用如果总是用短域名发请求,缓存命中率可能不如预想的高,因为几条搜索域记录各查各的。对于一些内部服务互相调用的高频场景,建议在应用层使用完整域名,利用 CoreDNS 的缓存降低整体解析压力。

6.4 不要用直连 kube-dns Service IP 的方式做域名校验

有人排障时习惯直接去curl http://10.96.0.10:53或者用外部工具指向 kube-dns 的 ClusterIP 去测试。这种验证方式有意义,但要注意它绕过了 Pod 自身的搜索域逻辑。你测出来的是 CoreDNS 是否活着、记录是否存在,但无法验证应用真正经历的“搜索域拼接 + ndots”链路。最好始终从 Pod 内部出发,用真实解析流程来复现问题。

我个人反复体验到的一点是:Kubernetes DNS 的难点从来不在于“闹明白一条记录怎么生成”,而在于它和网络、容器运行时、应用解析逻辑之间层层嵌套。很多问题的表象是“域名解析慢”“服务访问超时”,根因却分散在搜索域、ndots、CoreDNS 转发、上游 DNS 等各个环节。所以排查时一定要把链路拆开来看,每一段都验证一遍,不要急着下结论。把服务发现当成一条完整链路去理解,比背下任何一份配置模板都更有价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询