资讯详情

StatefulSet Pod域名访问原理与实战:从headless Service到外部负载均衡

📅 2026/9/11 16:03:50 | 华诺云谱 👁 阅读
StatefulSet Pod域名访问原理与实战:从headless Service到外部负载均衡
刚接手一个项目时别人问我StatefulSet的Pod能不能用域名访问我的第一反应是可以但真正落地时才发现这里面的门道比想象中多得多。Pod域名、headless Service、CoreDNS、Ingress、外部负载均衡……每一层都有一堆细节稍微有一个地方没配置对域名要么解析不出来要么解析出来的IP根本不通。这篇文章就把我在生产环境中用域名访问StatefulSet Pod的完整经验整理出来。不管你是想让集群内的其他服务通过稳定的Pod域名做服务发现还是想让集群外部的系统直接通过一个自定义域名访问数据库、消息队列、缓存这类有状态应用希望这篇文章能给你一套可以直接照着做的方案。1. StatefulSet的Pod为什么需要域名访问稳定标识是核心答案先聊一个最基础但最容易忽略的问题为什么不直接用Pod IP因为Pod IP不稳定。在Kubernetes里Deployment管理的Pod是无状态的IP变了无所谓反正前端通过Service做负载均衡后端Pod换个IP没人关心。但StatefulSet管理的Pod不一样——数据库、消息队列、NFS、ZooKeeper这类有状态应用每个Pod都有独立的数据卷和状态它们之间有主从关系、有固定的通信角色。如果今天连的是10.244.1.5明天Pod一重建变成10.244.3.8客户端还傻傻地连旧IP整个系统就断了。所以StatefulSet的设计者给出了一个核心思想每个Pod有一个稳定的主机名格式是$(statefulset名称)-$(序号)比如mongo-0、mongo-1、mongo-2配上headless Service后每个Pod会得到一个稳定的DNS名称比如mongo-0.mongo-hs.default.svc.cluster.localPod可以重建、可以漂移到别的节点IP随便变但这个DNS名称永远不变。这就是用域名访问StatefulSet Pod的本质——用稳定不变的逻辑名字替代变化无常的物理地址。1.1 有状态应用面对的三变与三不变我习惯把StatefulSet的访问问题归结为三变和三不变理解了这六点后面所有方案都能串起来。一变是Pod IP会变Pod重启、重建、节点维护导致重新调度时都会换IP二变是节点IP会变Pod漂移到另一台机器后通过节点访问的路径也要跟着变三变是负载均衡入口会变云平台上的ELB、自建的MetalLB、Nginx Ingress Controller的地址都可能在变更或迁移。对应的三不变是StatefulSet的名称不变Pod的序号不变headless Service的名称不变。只要有这三个不变Pod的域名就永远不变因为它的解析路径完全由这三者决定。所以域名访问方案的核心就是把变化的IP隐藏在不变的域名后面客户端只记域名不记IP。1.2 headless Service与StatefulSet的联动机制域名从哪来要理解Pod域名是怎么来的必须理解headless Service。普通Service有一个ClusterIP作为负载均衡入口流量进来后按概率转发给后面的Pod。headless ServiceclusterIP: None不分配ClusterIP它的作用是为每个符合选择器的Pod创建一条DNS记录。StatefulSet还比Deployment多做了一层它要求相关联的Service必须是一个headless Service并且会用$(pod名).$(service名).$(namespace).svc.cluster.local这样的格式为每个Pod生成独立的DNS名称。这里的关键是StatefulSet的Pod名字是有序的mongo-0、mongo-1……所以DNS名称天然也是有序、稳定的。打个比方普通Service像一个公司总机你拨总机号码前台帮你随机转接到某个员工headless Service像一份员工通讯录你查某个固定分机直接就能找到对应的那个人中间没有转发。域名解析链路大概是客户端 └→ 向CoreDNS发起查询 mongo-0.mongo-hs.default.svc.cluster.local └→ CoreDNS返回对应Pod的IP └→ 客户端直接连接该Pod注意没有ClusterIP做中间转发这条链路要通缺一不可headless Service存在、StatefulSet的selector匹配、CoreDNS正常工作、Pod处于Ready状态。2. 集群内先打通Pod域名的解析链路与验证要点我在实际工作中发现一个现象很多人在集群外访问时折腾半天结果发现集群内都没打通。所以建议先从集群内验证开始把链路一截一截确认好再考虑对外暴露。2.1 完整YAML示例headless Service 3副本StatefulSet下面这份配置我简化成了nginx目的是把原理讲清楚。生产上换成MongoDB、MySQL、Redis都一样原理不变。apiVersion: v1 kind: Service metadata: name: web-hs namespace: default spec: clusterIP: None publishNotReadyAddresses: true selector: app: web ports: - name: http port: 80 targetPort: 80 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: web namespace: default spec: serviceName: web-hs replicas: 3 selector: matchLabels: app: web template: metadata: labels: app: web spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80有几个配置项单独说一下。serviceName: web-hs这个字段必须跟上面的Service名字一致StatefulSet靠它找到自己对应的headless Service进而生成Pod DNS名称。如果把Service删了Domain就解析不了。publishNotReadyAddresses: true是我强烈建议加上的。默认情况下Pod没进入Ready状态时headless Service不会为它生成DNS记录。但对于数据库这类应用启动时间可能很长或者需要在Pod进入Ready之前就允许其他节点通过域名找到它来调整集群状态。加上这个字段后只要是Pod存在不管是否Ready都会发布DNS记录。2.2 验证域名的三个命令与预期输出配置文件apply上去之后先看一下Pod和Service的状态kubectl get pods -l appweb -o wide kubectl get svc web-hs预期看到CLUSTER-IP那一栏是None说明headless Service生效。然后起一个临时的busybox Pod来做DNS解析kubectl run -it --rm debug --imagebusybox:1.36 --restartNever -- sh进入容器后执行nslookup web-hs.default.svc.cluster.local nslookup web-0.web-hs.default.svc.cluster.local nslookup web-1.web-hs.default.svc.cluster.local nslookup web-2.web-hs.default.svc.cluster.local预期输出Server: 10.96.0.10 Address: 10.96.0.10:53 Name: web-0.web-hs.default.svc.cluster.local Address: 10.244.1.20 Name: web-1.web-hs.default.svc.cluster.local Address: 10.244.2.31 Name: web-2.web-hs.default.svc.cluster.local Address: 10.244.3.42三个Pod域名都能解析出各自独立的IP说明链路正常。这里注意只解析服务名web-hs.default.svc.cluster.local时会返回所有Pod的IP列表这也是服务发现的另一种用法。2.3 跨Namespace与跨集群访问的域名写法集群内的域名访问有个绕不开的细节FQDN必须带全除非客户端和目标在同一个Namespace。如果在prodNamespace下的服务想访问defaultNamespace下的web-0.web-hs.default.svc.cluster.local那必须写全名因为CoreDNS的search domain搜索域只包含Pod自身的Namespace。写成web-0.web-hs.default.svc.cluster.local就一定没问题。跨集群的场景则更复杂一些常见做法是用ExternalNameService做集群间的域名映射或者在服务网格Istio等里配置ServiceEntry或者让两个集群的CoreDNS配置互相forward。我自己最常用也最推荐的做法是ExternalName因为简单直接不引入额外组件。比如集群B要访问集群A里的mongo-0.mongo-hs.default.svc.cluster.local就在集群B里建一个同名字的ExternalName Service指向集群A的完整域名。但前提是集群B的节点能路由到集群A的Pod网段否则域名解析出来也连不上。3. 让集群外也能用域名访问三种主流方案的取舍集群内打通只是第一步大部分真实需求是我的业务系统不在K8s集群里我要通过域名访问集群里的数据库/消息队列/Web服务。这时候不能直接用Pod域名了因为集群外的DNS系统根本不知道svc.cluster.local这个域该怎么解析。需要做的事可以用一句话概括在外部域名和内部Pod域名之间架一座桥。根据应用类型有三种主流方案。3.1 HTTP类应用Ingress按域名路由到StatefulSet如果你的应用走HTTP协议比如Nginx、Web API、前端服务那Ingress就是最标准的方案。流程是外部请求先到Ingress Controller比如Nginx Ingress、TraefikController根据请求的Host头把流量转发给背后的Service。apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: web-ingress namespace: default annotations: nginx.ingress.kubernetes.io/rewrite-target: / spec: rules: - host: web.example.com http: paths: - path: / pathType: Prefix backend: service: name: web-hs port: number: 80配上Ingress后把web.example.com这个域名的DNS解析到Ingress Controller的对外IP请求就能进来了。不过这里有个容易踩的坑Ingress转发到的Service不一定要是headless Service也可以是有ClusterIP的普通Service。如果你希望Ingress把请求轮询转发给后端的三个Pod用普通Service更合适如果你希望Ingress只把请求转发给某个特定Pod比如只有web-0是主节点那backend的serviceName写web-0.web-hs是不行的Ingress不支持这种写法需要在Ingress Controller层面做自定义配置或者直接用web-hs.default.svc.cluster.local:80然后通过headless返回多个IP再配合会话保持来实现。3.2 TCP类应用外部负载均衡器直连PodMongoDB、Redis、Kafka、MySQL这些都是TCP协议标准Ingress不支持四层转发除非用Nginx Ingress的tcp-servicesConfigMap单独配置。所以TCP场景通常走下面两条路线。第一条路线是云平台负载均衡器 NodePortapiVersion: v1 kind: Service metadata: name: web-hs-external namespace: default spec: type: NodePort selector: app: web ports: - name: mongo port: 27017 targetPort: 27017 nodePort: 30017然后在云控制台上把负载均衡器的后端指向所有Worker节点的30017端口。外部访问时域名解析到负载均衡器的IP负载均衡器转发到某个节点的30017端口再由kube-proxy转发到对应Pod。第二条路线是自建环境用MetalLB分配外部IPMetalLB可以给LoadBalancer类型的Service分配一个集群外的虚拟IP。外部域名解析到这个虚拟IPMetalLB再把流量转发到StatefulSet的Pod。apiVersion: v1 kind: Service metadata: name: web-hs-lb namespace: default spec: type: LoadBalancer selector: app: web ports: - name: mongo port: 27017 targetPort: 27017对数据库这类一主多从的场景我强烈建议不要只暴露一个Service做负载均衡而是为每个Pod各建一个LoadBalancer类型的Service或者用ExternalIP直接指向每个Pod的IP。因为主从数据库的读写分离客户端需要指定连接哪个Pod主库写、从库读负载均衡随机分发会把写请求发到从库直接报错。3.3 NodePort 外部DNS映射小规模场景最快的落地方式如果你只是测试环境、或者只有几个内部系统要用NodePort是最快的方式。不需要装Ingress Controller不需要MetalLB建一个NodePort Service然后在外部DNS上做一个A记录指向某个节点IP端口用NodePort搞定。不过这种方式的缺点也很明显端口不标准、节点IP变了要手动改DNS、存在单点问题。所以我只建议临时验证用不建议长期跑生产。NodePort还有个隐藏问题如果Pod所在的节点挂了NodePort对应的端口在该节点上也就不可用了。除非你在Service上设置externalTrafficPolicy: Cluster让流量能在集群内再转发一次否则客户端访问的是哪一个节点流量就固定走那个节点的转发逻辑。4. Pod重建后域名还能用吗高可用场景下的解析行为与常见坑很多人最担心的一个问题是Pod被删掉重建之后域名还能不能解析答案是能但前提是你用对了名字。4.1 Pod重建、漂移与域名稳定性的关系StatefulSet的Pod重建后序号不变名字不变。mongo-0被删掉新Pod还是叫mongo-0新Pod可能被调度到另一台节点IP换了但是mongo-0.mongo-hs.default.svc.cluster.local这个域名在CoreDNS里会自动更新为新的IP。也就是说域名永远指向当前存在的Pod不管它漂到哪。这个特性对有状态应用特别重要。比如MongoDB三节点集群即使mongo-0所在的节点宕机运维把Pod重新调度到新节点其他节点和客户端依然可以通过mongo-0.mongo-hs.default.svc.cluster.local找到它。但有一个前提要注意StatefulSet的Pod只有当它正常重建时才会保持序号。如果你手动把Pod删了StatefulSet会自动新建一个同名Pod域名没问题如果整个StatefulSet被删除了那域名也就跟着没了因为这相当于删除了通讯录而不是某个员工换了工位。4.2 多副本场景下SRV记录做服务发现除了A记录headless Service还会为每个Pod生成SRV记录这在实际使用中非常有用。举个例子你的应用需要连接mongo这个服务但不知道有几个副本、IP是多少。可以查询SRV记录nslookup -typeSRV _http._tcp.web-hs.default.svc.cluster.localSRV记录会返回服务的端口和对应的域名。很多中间件比如ZooKeeper、Consul、MongoDB的replica set发现机制都支持通过SRV记录自动发现集群成员这就是为什么StatefulSet headless Service成为有状态应用在K8s上运行的标准组合。不过SRV记录有个细节容易被坑它使用的是端口名port name而不是端口号。如果Service里没有给端口命名SRV记录可能查询不到。所以创建Service时记得给每个端口加上name字段。4.3 一次典型的域名解析失败排查从nslookup到CoreDNS逐层定位我在群里看到过很多人问为什么我Pod的域名解析不出来这里把完整的排查链路写出来按顺序执行大部分问题都能定位到。第一步确认headless Service存在并且选择器正确kubectl get svc web-hs -o yaml kubectl get endpoints web-hsEndpoints里必须能看到三个Pod的IP。如果Endpoints为空说明selector没匹配上或者Pod没Ready没加publishNotReadyAddresses: true时。第二步确认CoreDNS运行正常kubectl get pods -n kube-system -l k8s-appkube-dns如果CoreDNS Pod处于CrashLoopBackOff先看日志kubectl logs -n kube-system -l k8s-appkube-dns --tail50第三步用busybox起临时Pod测试nslookup web-0.web-hs.default.svc.cluster.local如果提示server cant find进入CoreDNS容器手动查一下kubectl exec -n kube-system -it deploy/coredns -- sh在CoreDNS容器里执行nslookup web-0.web-hs.default.svc.cluster.local 127.0.0.1如果能解析说明CoreDNS本身正常问题在客户端Pod的DNS配置如果解析不了问题在CoreDNS的配置或者上游。第四步检查CoreDNS的配置kubectl get configmap -n kube-system coredns -o yaml看hosts插件、kubernetes插件、forward插件配置是否正常。特别是如果你改过CoreDNS的ConfigMap容易因为格式错误导致整个DNS服务挂掉。改完记得重启CoreDNS让配置生效。第五步如果上面都正常还是解析不了考虑是不是网络插件的问题。特别是用了Multus多网卡后发现Pod域名解析异常通常是因为默认路由走的是辅助网卡导致DNS请求出不去。这时候要在Pod的NetworkAttachmentDefinition里指定主网卡的默认路由或者调整策略路由。5. 一套可抄作业的完整案例MongoDB三节点用域名访问的落地过程前面讲了原理和方案这一章我用MongoDB三节点副本集做一个完整的实操案例从创建StatefulSet到用域名验证每一步都给出来。为什么选MongoDB因为它是典型的有状态应用而且MongoDB官方支持用DNS种子列表mongodbsrv://做副本集成员发现特别适合验证StatefulSet域名方案。5.1 部署前准备为什么要用publishNotReadyAddressesMongoDB副本集初始化的经典难题是三个Pod之间要互相通信但在初始化之前它们都处于容器启动但副本集未配置的状态。如果headless Service没有publishNotReadyAddresses: true这三个Pod的域名解析不出来互相之间找不到对方副本集永远初始化不了形成死锁。所以在部署有状态应用时publishNotReadyAddresses: true几乎是必须的。这个字段的意思是只要Pod存在就发布DNS记录不管Ready不Ready让Pod在初始化前也能被域名找到。5.2 操作步骤与验证结果第一步创建Namespacekubectl create namespace database第二步创建headless ServiceapiVersion: v1 kind: Service metadata: name: mongo-hs namespace: database spec: clusterIP: None publishNotReadyAddresses: true selector: app: mongo ports: - name: mongodb port: 27017 targetPort: 27017第三步创建StatefulSet这里重点注意serviceName和hostnameapiVersion: apps/v1 kind: StatefulSet metadata: name: mongo namespace: database spec: serviceName: mongo-hs replicas: 3 selector: matchLabels: app: mongo template: metadata: labels: app: mongo spec: containers: - name: mongo image: mongo:6.0 args: - --replSet - rs0 - --bind_ip_all ports: - containerPort: 27017 name: mongodb第四步等Pod起来后进入第一个Pod初始化副本集kubectl exec -it -n database mongo-0 -- mongosh --eval rs.initiate({ _id: rs0, members: [ { _id: 0, host: mongo-0.mongo-hs.database.svc.cluster.local:27017 }, { _id: 1, host: mongo-1.mongo-hs.database.svc.cluster.local:27017 }, { _id: 2, host: mongo-2.mongo-hs.database.svc.cluster.local:27017 } ] }) 注意members的host字段必须用Pod域名而不是Pod IP。因为Pod重建后IP会变但域名永远稳定MongoDB副本集成员配置写域名才能长期稳定。第五步验证副本集状态kubectl exec -it -n database mongo-0 -- mongosh --eval rs.status()预期三个节点的health都为1、stateStr为PRIMARY或SECONDARY。第六步在集群内验证域名解析kubectl run -it --rm debug --imagebusybox:1.36 --restartNever -- shnslookup mongo-0.mongo-hs.database.svc.cluster.local nslookup mongo-hs.database.svc.cluster.local预期第一个返回mongo-0的Pod IP第二个返回三个Pod的IP列表。5.3 扩展结合外部DNS与证书把域名对外暴露如果业务系统在集群外需要通过mongo.internal.example.com:27017访问MongoDB我的做法是先创建一个NodePort或LoadBalancer类型的Service把27017端口暴露出来apiVersion: v1 kind: Service metadata: name: mongo-external namespace: database spec: type: NodePort selector: app: mongo ports: - port: 27017 targetPort: 27017 nodePort: 30017在外部DNS控制台添加一条A/AAAA记录把mongo.internal.example.com解析到某个节点IP或负载均衡器IP。如果用的是云负载均衡器建议开启健康检查后端指向所有Worker节点的30017端口避免单节点故障。如果有安全要求可以用Stunnel、TLS termination或者服务网格做加密不建议直接把裸MongoDB端口暴露到公网。另外如果你的环境是自建的K8s集群可以根据实际情况选MetalLB或者Keepalived给Service分配外部IP原理一样只是不需要固定NodePort端口直接使用标准端口对客户端更友好。我在实际部署里把域名切过去后MongoDB的连接串从mongodb://10.0.0.5:27017改成了mongodb://mongo-0.mongo-hs.database.svc.cluster.local:27017,mongo-1.mongo-hs.database.svc.cluster.local:27017,mongo-2.mongo-hs.database.svc.cluster.local:27017/?replicaSetrs0后续节点发生故障重启、甚至换机器迁移应用端完全无感知这就是稳定域名带来的实际价值。这套方法不止适用于MongoDBMySQL主从、Redis Cluster、Kafka、ZooKeeper都可以用同样的思路。核心就是记住三句话headless Service提供DNS记录StatefulSet保证Pod名字不变域名解析永远指向当前Pod。三件事保证了Pod怎么折腾都行。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。