headless service不提供负载均衡,仅通过dns返回pod ip列表,客户端需自行实现健康探测、连接调度与故障转移。
headless service 本身不暴露服务,也不做连接调度——它只是让 dns 查询直接返回 pod ip 列表,把“调度权”交还给客户端。go 应用要实现有状态连接的稳定调度,必须自己处理 dns 解析、健康探测、连接复用和故障转移。
为什么 clusterIP: None 不等于“自动负载均衡”
很多人误以为只要把 Service 设成 Headless,Go 客户端就能“自动连上健康的 MySQL 主节点”或“自动轮询 Kafka Broker”。实际不是:
-
net.LookupHost("mysql-headless.default.svc.cluster.local")返回的是所有匹配 Pod 的 IP 列表,顺序不定,且不包含端口、就绪状态、拓扑信息 - CoreDNS 默认不做健康检查,哪怕某个 Pod 已 CrashLoopBackOff,它的 IP 仍会出现在 DNS 结果里
- Go 的
net/http.DefaultTransport或sql.Open都不会主动轮询或重试多个地址;你传一个域名进去,它只解析一次、取第一个 IP(取决于 glibc 或 Go resolver 实现)
Go 客户端必须自己实现的三件事
要让 Go 应用真正利用 Headless Service 做有状态调度,得在代码里补全这三层逻辑:
-
DNS 缓存与定期刷新:不能只启动时查一次。要用
time.Ticker定期调用net.LookupHost,配合sync.Map维护可用 endpoint 列表 -
就绪探针协同:Headless Service 默认不返回未就绪 Pod,除非你显式加了
publishNotReadyAddresses: true和 annotationservice.alpha.kubernetes.io/tolerate-unready-endpoints: "true"。但即便如此,也得靠客户端发 HTTP/readyz或 TCP dial 测试端口是否真通 -
连接策略绑定实例:比如 Redis 集群客户端必须按 Gossip 协议发现拓扑,Kafka 客户端依赖
metadata.broker.list初始化后自行维护 broker 地址;硬写死"kafka-0.kafka-headless.default.svc.cluster.local:9092"就失去了弹性
StatefulSet Pod 名称 + Headless Service DNS 是稳定调度的前提
如果你的 Go 应用需要区分主从(比如只往 mysql-0 写、读走 mysql-1 和 mysql-2),关键不是 DNS 返回多少 IP,而是能不能构造出带序号的确定性域名:
- 确保 StatefulSet 的
serviceName字段指向你的 Headless Service(如mysql-headless) - 每个 Pod 的完整 DNS 名是
<pod-name>.<service-name>.<namespace>.svc.cluster.local</namespace></service-name></pod-name>,例如mysql-0.mysql-headless.default.svc.cluster.local - Go 里可硬编码规则:
if idx == 0 { addr = fmt.Sprintf("mysql-%d.mysql-headless.default.svc.cluster.local:3306", idx) },再配合sql.Open("mysql", addr) - 注意:这种写法依赖 StatefulSet 的有序性,缩容后
mysql-2可能永久消失,客户端需容忍connection refused并 fallback
容易被忽略的两个坑
一是 publishNotReadyAddresses: true 必须配在 Service 上,否则 Pod 启动中(initContainer 还没完、readinessProbe 没通过)时 DNS 查不到地址,Go 应用启动就失败;二是 CoreDNS 的 ndots:5 默认值会让短域名(如 mysql-0)先尝试追加最多 5 层后缀,导致解析慢甚至超时——建议在 Go 客户端统一用全限定域名(FQDN),别省略 .default.svc.cluster.local。











