上周帮某项目组排查一个 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 本身也是跑在集群里的普通 PodCoreDNS 不是什么独立的外部服务它同样是跑在 Kubernetes 里的 Deployment通常位于kube-system命名空间。它的 Pod 通过一个名为kube-dns的 Service 对外提供解析能力Pod 内resolv.conf里的nameserver 10.96.0.10就是这个 Service 的 ClusterIP。CoreDNS 通过监听集群 API 的变化动态维护一份“DNS 记录表”。当你在集群里创建了一个新的 ServiceCoreDNS 会自动生成对应的 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的无头服务它没有 ClusterIPDNS 返回的是后端 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:5order-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.10CoreDNS发起查询。CoreDNS 查看到这条记录存在返回对应的 ClusterIP。应用拿到 IP建立 TCP 连接请求经过 kube-proxy 转发到后端 Pod。这个流程看起来很流畅但搜索域的逐个尝试机制在某些外部域名场景下会产生多余的 DNS 查询甚至导致解析时间变长。这正是开头提到的那个偶发超时问题的大背景。3. 无头服务与 SRV 记录直接摸到 Pod IP 的手段3.1 无头服务ClusterIP 被拿掉以后发生了什么普通 Service 有 ClusterIPDNS 解析返回的是这个虚拟 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.localDNS 返回的不是一个虚拟 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或udpservice就是你给 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 domainsDNS 查询的“默认路径”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 --imageregistry.example.com/dnsutils:latest --rm -it --restartNever -- 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查询无头服务的记录看看返回了几个 IPdig short mongo-headless.default.svc.cluster.local预期返回多个 IP每个 IP 对应一个后端 Pod。如果只返回了一个 IP那说明 Service 的 selector 可能没匹配到所有 Pod或者存在不可用 Pod。查询 SRVdig 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-appkube-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 等各个环节。所以排查时一定要把链路拆开来看每一段都验证一遍不要急着下结论。把服务发现当成一条完整链路去理解比背下任何一份配置模板都更有价值。