K8s CoreDNS 服务发现:普通Service和Headless Service到底差在哪?

AI 总结

这篇文章对比了Kubernetes中普通Service和Headless Service的服务发现机制。普通Service通过ClusterIP提供稳定虚拟IP,由kube-proxy选择后端,适合无状态服务;Headless Service无ClusterIP,DNS直接返回后端Pod IP列表,由客户端自行选择,适合StatefulSet等有状态应用。文章还介绍了CoreDNS解析链路、StatefulSet依赖Headless Service实现稳定Pod身份、Pod需Ready才进入后端、以及DNS排查步骤和ndots:5对外部域名解析的影响。

你在Pod里访问:

http://payment

为什么它能找到后端?

再看 StatefulSet:

mysql-0.mysql.database.svc.cluster.local

为什么它能精准找到某一个Pod?

这背后不是"DNS帮你查了个IP"这么简单。Kubernetes里有两套很容易混淆的服务发现方式:普通Service返回虚拟IP,Headless Service直接返回后端Pod地址。

把这两件事讲清楚,Service不通、StatefulSet互相找不到、DNS解析慢这些问题,排查就有入口了。

先看全链路:一个服务名怎么变成IP

Pod内访问服务时,请求大致走这条路径:

应用访问 payment
↓
Pod的 /etc/resolv.conf
↓
CoreDNS Service
↓
CoreDNS 查询 Kubernetes API 中的 Service / EndpointSlice
↓
返回 ClusterIP 或 PodIP

CoreDNS 是集群DNS服务。它通过监听 Kubernetes API 中的 Service、EndpointSlice 等对象变化,动态生成解析结果。

简单说:Service后端扩容、缩容、Pod变成NotReady,CoreDNS拿到新状态后,DNS答案也会跟着变。

Pod里先看DNS配置:

kubectl -n app exec payment-xxx -- cat /etc/resolv.conf

常见结果:

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

nameserver 是CoreDNS的ClusterIP。

search 是搜索域。它让你在同一个命名空间里,可以直接写短服务名:

payment

而不用每次写完整域名:

payment.app.svc.cluster.local

普通Service:DNS返回一个ClusterIP

最常见的Service长这样:

apiVersion: v1
kind: Service
metadata:
  name: payment
  namespace: app
spec:
  selector:
    app: payment
  ports:
  - port: 80
    targetPort: 8080

Kubernetes会分配一个ClusterIP,例如:

payment.app.svc.cluster.local → 10.96.128.20

应用访问的是这个固定虚拟IP,而不是某个具体Pod IP。

后续流量才由 kube-proxy 的转发规则,把请求送到可用后端:

应用
↓
Service ClusterIP
↓
kube-proxy
↓
EndpointSlice里的某个Ready Pod

这就是普通Service适合无状态服务的原因:后端Pod可以替换、扩缩容、重建,客户端始终只认一个稳定入口。

普通Service解决的是"我不关心后端是谁,只要请求能被均衡地送到可用实例"。

Headless Service:不分ClusterIP,直接告诉你后端是谁

Headless Service 的关键配置只有一行:

spec:
  clusterIP: None

完整示例:

apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: database
spec:
  clusterIP: None
  selector:
    app: mysql
  ports:
  - name: mysql
    port: 3306
    targetPort: 3306

它没有ClusterIP。

因此解析结果不再是一个虚拟IP,而是后端Pod IP列表:

mysql.database.svc.cluster.local
→ 10.244.2.15
→ 10.244.3.21
→ 10.244.1.36

请求不会先经过Service的虚拟IP转发。客户端拿到多个Pod IP后,需要自己选择连接哪个地址。

这句话要记住:

普通Service把"选后端"交给kube-proxy;Headless Service把后端地址直接交给客户端。

所以Headless Service不是普通Service的"高级版",而是用于另一类场景。

对比项普通ServiceHeadless Service
是否有ClusterIP有没有
DNS解析结果一个稳定ClusterIP多个后端Pod IP
谁负责选后端kube-proxy客户端或应用连接库
适用场景无状态API、Web服务StatefulSet、数据库、集群软件
是否天然负载均衡是不保证,取决于客户端

/ / /

StatefulSet为什么离不开Headless Service

MySQL、Kafka、ZooKeeper 这类有状态服务,不只是"需要多个Pod"。它们还需要知道自己和同伴是谁。

例如3个MySQL副本:

mysql-0
mysql-1
mysql-2

StatefulSet配合Headless Service后,每个Pod都有稳定地址:

mysql-0.mysql.database.svc.cluster.local
mysql-1.mysql.database.svc.cluster.local
mysql-2.mysql.database.svc.cluster.local

即使mysql-1因为节点故障被重建,只要它仍然叫mysql-1,使用对应PVC,它的逻辑身份依旧没变。

这对数据库复制、Broker选主、成员发现非常重要。

一个典型StatefulSet片段:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
  namespace: database
spec:
  serviceName: mysql
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql

serviceName: mysql 指向的就是Headless Service。

Headless Service提供稳定发现地址;StatefulSet提供稳定Pod序号和PVC对应关系。两者要配合看。

DNS记录不只A记录,命名端口还能用SRV记录

普通场景用A记录就够了:服务名解析出IP。

但有状态集群有时还需要同时发现"主机名和端口"。这时可以使用SRV记录。

Service端口必须命名:

ports:
- name: mysql
  port: 3306
  targetPort: 3306

查询格式:

_mysql._tcp.mysql.database.svc.cluster.local

对Headless Service,SRV记录会返回各个后端Pod的主机名和端口。对需要成员发现的应用很有用。

一个容易踩的坑:Pod Ready和DNS结果有关

Service不是只要selector匹配就一定把Pod放进后端。

通常情况下,Pod必须Ready,才会成为可用Endpoint。也就是说:

Pod Running
≠ 一定进入Service后端

如果readinessProbe失败,Pod可能仍在运行,但不会接收普通Service的新流量,也通常不会作为Headless Service的可用后端返回。

检查顺序:

kubectl -n app get pod -l app=payment
kubectl -n app get svc payment
kubectl -n app get endpointslice \
-l kubernetes.io/service-name=payment

StatefulSet有特殊需求时,可能需要:

publishNotReadyAddresses: true

它允许Headless Service发布未Ready Pod地址,常用于集群成员启动阶段互相发现。

但不要把它当默认配置。未Ready代表应用还未准备接流量;提前发布地址,需要应用自己能正确处理启动中的成员。

DNS不通,按这4层排查

1. 先确认Pod到底在问谁

kubectl -n app exec payment-xxx -- cat /etc/resolv.conf

确认nameserver是不是集群DNS地址,search域是否符合预期。

2. 再查CoreDNS自己是否健康

kubectl -n kube-system get svc kube-dns
kubectl -n kube-system get pod -l k8s-app=kube-dns -o wide
kubectl -n kube-system logs deploy/coredns --since=10m

CoreDNS Pod Running不代表它一定能回答Kubernetes记录。还要看日志中是否有连接API Server、同步对象或插件报错。

3. 核对Service与后端

kubectl -n app get svc payment -o yaml
kubectl -n app get endpointslice \
-l kubernetes.io/service-name=payment -o yaml

常见问题:

Service selector写错
Pod label不匹配
Pod没有Ready
端口名或targetPort不一致

4. 最后再查网络和应用

DNS解析到IP,不代表网络和端口一定通。

kubectl -n app exec payment-xxx -- nslookup payment
kubectl -n app exec payment-xxx -- nslookup mysql.database.svc.cluster.local

拿到IP后,再确认Pod网络、NetworkPolicy、目标端口和应用监听状态。

ndots:5 为什么会让外部域名变慢

Pod的resolv.conf常见:

options ndots:5

意思是域名中点号少于5个时,DNS客户端可能先按search域补全查询。

例如应用访问:

api.example.com

它可能先尝试:

api.example.com.app.svc.cluster.local
api.example.com.svc.cluster.local
api.example.com.cluster.local
api.example.com

如果外部DNS慢、查询失败重试多,应用会感觉"访问外部接口很慢"。

处理时不要直接全局改ndots。先确认应用是否使用完整域名、DNS请求量是否异常、CoreDNS转发是否正常,再决定是否针对应用做DNS配置优化。

【CoreDNS与Headless Service速记】

 CoreDNS根据Service和EndpointSlice动态回答集群DNS
 普通Service:服务名 → ClusterIP → kube-proxy选择后端
 Headless Service:服务名 → 后端Pod IP列表
 Headless Service适合StatefulSet和成员发现
 StatefulSet稳定身份:pod-序号 + Headless Service + 独立PVC
 Pod Running不等于在Service后端,重点看Ready和EndpointSlice
 DNS不通按:resolv.conf → CoreDNS → Service/EndpointSlice → 网络/端口 排查
 外部域名慢,注意ndots:5带来的search域查询

参考官方文档

  • Kubernetes官方文档:Services与Pods的DNS记录
  • CoreDNS官方文档:Kubernetes插件

评论 (0)