无头服务(Headless Service)必须显式设置clusterIP: "None",才能绕过kube-proxy、让CoreDNS返回全部Pod的A记录,配合客户端负载均衡解决gRPC长连接导致的流量倾斜问题。

gRPC 或其他长连接场景下,Kubernetes 默认 ClusterIP Service 无法均匀分发请求——连接一旦建立就“黏”在某个 Pod 上,扩容后大量 Pod 闲置。必须用 Headless Service 配合客户端主动负载均衡,否则负载永远不均。
Headless Service 的 YAML 必须设 clusterIP: None
这不是可选项,是强制开关。Kubernetes 只有看到这个字段显式设为 None,才会跳过分配虚拟 IP、绕过 kube-proxy,并让 CoreDNS 返回所有匹配 Pod 的 A 记录。
-
clusterIP:字段必须存在且值为字符串"None"(注意引号),写成null或留空都无效 - Service 仍需带
selector,否则 DNS 不会返回任何 Pod IP - 不要加
type: ClusterIP—— 它是默认值,显式声明反而容易让人忽略clusterIP: None这个关键点 - 端口定义和普通 Service 一致,
port和targetPort对齐即可
Go 客户端必须用 net.LookupIP 查全量 Pod IP
net.LookupHost 在 Headless Service 场景下大概率只返回一个 IP 或直接报 no such host,因为它不保证解析出全部 A 记录;而 net.LookupIP 才是正确入口,且必须传 "ip" 作为网络类型参数。
- 域名必须是完整 FQDN,例如
"my-svc.default.svc.cluster.local",短名如"my-svc"会被系统 resolver 当作外部域名处理 - 别设
net.Resolver{PreferGo: true}—— 它不读/etc/resolv.conf中的search域,导致 FQDN 补全失败 - 查到的
[]net.IP是无序的,且可能混着 IPv4/IPv6,生产中建议过滤出 IPv4 并去重 - 每次建连前都应重新查一次,但别高频轮询:加 10–30 秒 TTL 缓存,避免压垮 CoreDNS
gRPC 客户端要启用 round_robin 并用 dns:/// 前缀
光靠 DNS 解析出 IP 不够,gRPC 默认仍会复用单个连接。必须显式开启客户端负载均衡器,并用特殊 scheme 让它感知 DNS 变更。
- 目标地址必须以
dns:///开头,例如"dns:///my-svc.default.svc.cluster.local:9090"(注意三个斜杠) - 必须传
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin": {}}]}`) - 不能用
grpc.WithInsecure()简化连接——它会跳过 resolver 初始化,dns:///就失效 - 如果服务端启用了 TLS,务必配
grpc.WithTransportCredentials(credentials.NewTLS(...)),否则连接拒绝
DNS 解析结果不可信,得自己做轻量健康检查
CoreDNS 返回的 IP 列表是最终一致性快照,Pod 可能已终止但记录尚未过期。直接轮询列表里的 IP,很容易把请求发到已死的实例上。
- 每次选 IP 前,先用
net.DialTimeout("tcp", ip+":9090", 300*time.Millisecond)快速探活 - 跳过连接失败的 IP,只在存活列表里轮询或随机取一个
- 避免在每次 RPC 调用前都做 TCP 探活——开销大;建议每 5–10 秒刷新一次存活列表并缓存
- 别依赖
readinessProbe完全兜底:它只影响 Endpoint 更新,DNS 缓存延迟仍可能导致短暂误发
最易被忽略的一点:Headless Service 的 DNS 解析行为高度依赖运行环境。代码若不在集群内 Pod 中执行(比如本地调试用 kubectl port-forward),my-svc.default.svc.cluster.local 根本无法解析——此时必须换用 localhost 或宿主机 IP,逻辑分支要隔离清楚。


















