K8s CoreDNS 服务发现:普通Service和Headless Service到底差在哪?
这篇文章对比了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的"高级版",而是用于另一类场景。
| 对比项 | 普通Service | Headless 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)