深色模式
Headless Service 与 StatefulSet DNS
摘要:本文解释
clusterIP: None的 Headless Service 与普通 Service 在 DNS 上的区别,为什么 StatefulSet 必须靠 Headless Service 获得稳定、可寻址的 per-Pod 名称,以及生产落地与排障要点。覆盖版本:Kubernetes v1.28+。
适用版本与前提
- Kubernetes:v1.28+。
- 工具:
kubectl、dig/nslookup(用临时调试 Pod)。 - 前提:理解 Service 选择器、CoreDNS(见
coredns.md)、StatefulSet 基本概念。
背景与问题
普通 Service 给你一个稳定虚拟 IP(ClusterIP),DNS 解析返回这个 IP,由 kube-proxy 做负载均衡。但有些场景你不要虚拟 IP,而是要直接、稳定地寻址每一个 Pod:
- 有状态集群(MySQL 主从、ZooKeeper、Kafka、Etcd):副本之间必须能互相按名字找到对方。
- 客户端需要自己决定连哪个副本(如分片、主节点选举)。
- 想绕过 Service 的负载均衡,做自定义客户端发现。
这正是 Headless Service 的用途:把 spec.clusterIP 设为 None,Kubernetes 不为它分配虚拟 IP,DNS 直接返回每个 Ready Pod 的 IP。
核心概念:两种 DNS 返回
| Service 类型 | DNS 记录 | 返回内容 |
|---|---|---|
| 普通 ClusterIP | A 记录 | ClusterIP(虚拟 IP) |
Headless(clusterIP: None) | A 记录(多条) | 每个 Ready Pod 的真实 IP |
| ExternalName | CNAME | 外部域名 |
关键点
Headless Service 的 DNS 反映 Pod 的 readiness:某个 Pod 未通过 readiness 探针,就会从 DNS 答案中消失——这和它从 EndpointSlice 中被剔除是同一套机制。所以“DNS 查不到 Pod”往往意味着“Pod 没 Ready”,而不是 DNS 坏了。
Headless Service 示例
yaml
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: default
spec:
clusterIP: None # 关键:Headless
selector:
app: mysql
ports:
- name: mysql
port: 3306
targetPort: 33061
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
创建后,DNS 会为 mysql.default.svc.cluster.local 返回所有 Ready Pod 的 A 记录。可用调试 Pod 验证:
bash
kubectl run dns-test --rm -it --image=nicolaka/netshoot -- bash
dig mysql.default.svc.cluster.local +short
# 输出多个 Pod IP,例如:
# 10.244.1.5
# 10.244.2.71
2
3
4
5
2
3
4
5
StatefulSet 的稳定 DNS
StatefulSet 与普通 Deployment 最大的网络差异在于:每个 Pod 有稳定的、可预测的 DNS 名称,格式为:
text
<pod-name>.<headless-service-name>.<namespace>.svc.cluster.local1
其中 <pod-name> 是 <statefulset-name>-<ordinal>(如 mysql-0、mysql-1)。即使 Pod 被重建、调度到别的节点、换了 IP,它的 DNS 名称不变。这使得 MySQL 主从、Etcd 集群等能持久地互相识别身份。
完整示例:
yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
namespace: default
spec:
serviceName: mysql # 必须指向一个 Headless Service
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
name: mysql
readinessProbe:
tcpSocket:
port: mysql
initialDelaySeconds: 10
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
clusterIP: None
selector:
app: mysql
ports:
- name: mysql
port: 3306
targetPort: 33061
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
各 Pod 的稳定名称:
mysql-0.mysql.default.svc.cluster.localmysql-1.mysql.default.svc.cluster.localmysql-2.mysql.default.svc.cluster.local
注意
spec.serviceName 必须对应一个真实存在且为 Headless 的 Service,否则 StatefulSet 的 Pod 不会获得稳定 DNS。如果漏配,Pod 名为 mysql-0 但没有对应 DNS 条目,互访会失败。
SRV 记录与端口发现
StatefulSet 还会生成 SRV 记录,把服务端口暴露为可发现项,格式大致为:
text
_<port-name>._<protocol>.<service>.<namespace>.svc.cluster.local1
例如 _mysql._tcp.mysql.default.svc.cluster.local,解析后返回各 Pod 的域名与端口。客户端可用 SRV 做“端口+主机”联合发现(常用于有状态中间件客户端库)。
生产实践
- 有状态中间件一律走 Headless + StatefulSet:不要用 Deployment 跑需要稳定身份的有状态集群。
- readiness 探针要真实:否则未就绪 Pod 仍可能被写入 DNS 或 EndpointSlice,造成连接抖动。
- Pod 反亲和:有状态集群通常配合
podAntiAffinity把副本打散到不同节点,避免单节点故障同时丢多个副本。 - 配合 NetworkPolicy:Headless Service 不限制谁能连,生产应显式用 NetworkPolicy 约束(见
networkpolicy.md)。
验证
bash
# 确认 Headless(CLUSTER-IP 为 None)
kubectl get svc mysql
# NAME TYPE CLUSTER-IP PORT(S)
# mysql ClusterIP None 3306/TCP
# 解析带序号的 Pod 名
kubectl run dns-test --rm -it --image=nicolaka/netshoot -- bash
dig mysql-0.mysql.default.svc.cluster.local +short
dig SRV _mysql._tcp.mysql.default.svc.cluster.local +short1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
回滚与清理
bash
kubectl delete statefulset mysql
kubectl delete svc mysql1
2
2
生产危险
删除 StatefulSet 默认不会删除 PVC(按设计保留数据)。若误删需要恢复,PVC 仍在,但重建 Pod 时序号与 PVC 绑定关系要确认一致,避免挂载错卷。删除 PVC 前务必备份数据。
故障排查
<pod>.svc解析失败:检查serviceName是否指向正确的 Headless Service;检查该 Service 的 selector 是否匹配 Pod 标签。- 解析到的 IP 连不上:多半是 Pod 未 Ready,DNS 已剔除它;用
kubectl get pods看 STATUS/READY。 - 跨命名空间解析:必须写全名
mysql-0.mysql.<ns>.svc.cluster.local,否则 search 域不匹配。
常见坑
- 把 Headless Service 当成“负载均衡入口”:Headless 不提供单一虚拟 IP,客户端要自己处理多个 Pod IP 的连接与重试。
- 忘记
serviceName字段。 - 用
ping验证:Pod IP 可 ping,但 Service 名解析为多个 IP,ping 单个体有意义,ping 虚拟概念无意义。
替代方案与权衡
- 普通 ClusterIP:无状态服务,不需要 per-Pod 寻址时更合适。
- 外部服务发现(如 Consul):比 K8s DNS 更强的注册/健康检查,但引入额外组件。
- Gateway API / Ingress:七层 HTTP 路由,不适合有状态 Pod 间直连。