上周帮某项目组排查一个 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:5nameserver指向的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:
- 应用层拿
order-service发起 DNS 查询请求。 - 系统解析
/etc/resolv.conf,发现ndots:5,order-service的点数少于 5,所以先拼上搜索域去尝试。 - 依次尝试
order-service.default.svc.cluster.local、order-service.svc.cluster.local、order-service.cluster.local。 - 第一次拼上了
order-service.default.svc.cluster.local,向10.96.0.10(CoreDNS)发起查询。 - CoreDNS 查看到这条记录存在,返回对应的 ClusterIP。
- 应用拿到 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.localsvc.cluster.localcluster.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/corednsCoreDNS 默认日志记录量不多,但查询错误会体现出来。如果想细化日志,可以修改 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 等各个环节。所以排查时一定要把链路拆开来看,每一段都验证一遍,不要急着下结论。把服务发现当成一条完整链路去理解,比背下任何一份配置模板都更有价值。