☰
K8S Service实战指南:从ClusterIP到LoadBalancer的坑与排查
2026/10/15 20:23:11 网站建设 项目流程

K8S里Service这东西,表面上看就是给Pod提供一个稳定的访问入口,真用起来才发现里面门道不少。从最简单的ClusterIP到需要对接云厂商的LoadBalancer,从写YAML到排查Endpoints为空,每一步都藏着细节。这篇总结我按自己平时排查问题的思路来写,不按官方文档的顺序背书,重点放在你真正会踩到的坑和API字段背后的逻辑上。

1. 为什么需要Service:理解了这点后面的坑都好避

刚接触K8S的人经常问一个问题:Pod有IP,我直接访问Pod不就行了,为什么非要搞个Service出来?这个问题想清楚,后面所有API字段的用途你基本都能自己推导出来。

1.1 Pod的IP天生就不靠谱

Pod的IP地址不是固定的。Deployment滚动更新时,旧的Pod被杀掉,新的Pod被创建,IP完全换了一轮。就算不滚动更新,Pod被驱逐、节点宕机、手动删除重建,IP都会变。你不可能在应用配置里写死一个Pod的IP,它今天能用明天就404了。

更麻烦的是副本扩容。一个Deployment起了三副本,三个Pod各自有独立IP,客户端要访问哪个?如果让客户端自己维护这三个IP的列表,等于把负载均衡的逻辑踢给了调用方,这在微服务架构里根本不可行。

Service正是为了解决这两个核心问题而生的:给一组动态变化的Pod提供一个固定的虚拟IP(ClusterIP),同时在这组Pod之间做负载均衡。从API设计的角度看,Service本质上是"稳定入口 + 动态后端"的抽象层。

1.2 Service在K8S网络架构中的角色

K8S集群里的网络流量路径大致是:客户端 -> Service(ClusterIP) -> kube-proxy规则 -> Pod IP。Service对象本身不干活,真正转发流量的是每个节点上的kube-proxy组件(现在也有其他实现,但主流还是这个)。

kube-proxy通过监听API Server上Service和Endpoints的变化,动态维护节点上的转发规则。你可以把Service理解成一个"声明",Endpoints是它对应的"后端列表",kube-proxy是把声明落到实处的"执行者"。这个三层分离的设计是理解Service所有行为的钥匙:Service的YAML写得不完整,kube-proxy就不知道该往哪儿转发;Endpoints没生成,Service就是一个空壳,访问它只会得到连接拒绝。

我实际排查中遇到的所有Service不通的问题,几乎都能归因到这三层中某一层出了问题。先检查Service有没有选到Pod,再看Endpoints是否生成,最后才怀疑kube-proxy,这个排查顺序能省下大量时间。

2. Service API对象拆解:核心字段逐个过

K8S所有对象都是声明式API,Service也不例外。一个完整的Service定义包含apiVersion、kind、metadata、spec几个部分,最关键的是spec。我把核心字段按重要程度排个序,每个都说说它的作用和容易出问题的细节。

2.1 apiVersion和kind不能写错

Service的apiVersion是v1,kind是Service,这是最基础的信息。有人会觉得这有什么好说的,但我见过有人把apiVersion写成apps/v1,直接把创建请求打回来。这俩字段是API Server的"路由信息",告诉它你要创建什么类型的对象,版本不对就是404。

apiVersion: v1 kind: Service metadata: name: my-service namespace: default spec: selector: app: my-app ports: - port: 80 targetPort: 8080

2.2 metadata里容易被忽略的注意事项

metadata里name是必填的,并且在namespace内必须唯一。namespace如果没写,默认就是default。这里有个实际开发中常见的坑:如果你是用kubectl create -f方式创建,YAML里没写namespace,对象会进default,但你的Deployment可能部署在别的namespace,selector选不到,Service就永远没有Endpoints。

另外,metadata里可以加annotations,有些云厂商的LoadBalancer类型Service会通过annotations来控制负载均衡器的具体行为,比如是否分配公网IP、带宽上限等。这块每个云厂商的键名都不同,用到的时候需要查对应厂商的文档。

2.3 spec.selector:决定后端Pod列表的关键

spec.selector是Service和Pod之间的"连接器"。kube-proxy和API Server的Endpoints控制器会拿你的selector和集群里所有Pod的labels做匹配,匹配上的PodIP会进入Endpoints列表。

这里有几个关键点:selector里的每个label都必须出现在Pod上,是"AND"的关系;多写一个不存在的label,直接导致选不到任何Pod;Pod的labels是可以在运行中修改的,一旦修改导致不再匹配selector,它的IP会立刻从Endpoints里被剔除,已有连接会断开。

我在实际项目里踩过最深的坑是"selector写对了,Pod labels看起来也对,但就是选不上"。最后发现原因是Pod所在的namespace和Service不一致,Endpoints控制器只在同一个namespace内做匹配。每个namespace是独立的隔离域,Service不能跨namespace选Pod。

2.4 spec.ports:端口映射的完整理解

ports字段是数组,可以配置多个端口。每个端口有三个核心配置项:port、targetPort、protocol。

  • port:Service对外暴露的端口,ClusterIP上监听的端口,客户端访问的就是这个端口。
  • targetPort:流量转发到Pod时使用的端口。注意targetPort可以不填,默认等于port。如果Pod里的容器监听的是8080,而你想让Service的端口是80,就得显式写targetPort: 8080。
  • protocol:协议类型,支持TCP、UDP、SCTP,默认是TCP。UDP和TCP不能混用,同一个Service端口不能同时监听TCP和UDP,这是很多新手会犯的错误。

targetPort还有一个明细用法:可以用命名端口(named port)。如果你的Pod定义了命名端口,Service里可以直接引用名字,好处是改端口号时不用改Service的YAML。举个例子:

# Pod里的containerPort定义 spec: containers: - name: app ports: - name: http containerPort: 8080 # Service引用命名端口 spec: ports: - port: 80 targetPort: http

2.5 spec.type:一个字段决定四种工作模式

type字段是Service最核心的行为开关,支持ClusterIP、NodePort、LoadBalancer、ExternalName四种取值。默认是ClusterIP。很多人在这一步就迷糊了:不知道选哪种类型,也不知道每种类型在实际环境中到底长什么样。

我的经验是:按流量来源来判断。

  • 集群内部访问,或者用于微服务间调用,用ClusterIP就够了,这也是默认选项。
  • 需要集群外部访问,但没有现成的负载均衡器,用NodePort,在每个节点上开一个端口。
  • 部署在云平台上,需要自动创建云负载均衡器,用LoadBalancer。
  • 只是想给集群外部的服务做一个DNS别名,用ExternalName,它甚至不创建ClusterIP和Endpoints。

3. 四种Service类型怎么选:端到端配置实例

这一节把每种类型都过一遍,每个都给出完整配置和实际使用场景,能直接抄作业。

3.1 ClusterIP:默认也是用得最多的

apiVersion: v1 kind: Service metadata: name: my-app-svc namespace: prod spec: type: ClusterIP selector: app: my-app env: prod ports: - name: http port: 80 targetPort: 8080 protocol: TCP

这个Service创建后,会分配一个ClusterIP(比如10.96.88.88),在集群内部所有节点上都能通。访问http://10.96.88.88:80,kube-proxy会在选中的Pod里挑一个做转发。

注意几个细节:ClusterIP有默认的IP范围,通常是10.96.0.0/12,具体看你的kube-apiserver启动参数。如果你在配置里用了clusterIP: None,这会让Service变成headless service,不分配ClusterIP,DNS直接返回后端Pod的IP列表。headless service在需要直连Pod的场景(比如数据库主从)或需要客户端自己做负载均衡的场景下很有用。

集群内访问Service通常不走ClusterIP,而是走DNS。K8S内置的DNS服务(CoreDNS)会自动为Service生成DNS记录,格式是<service-name>.<namespace>.svc.cluster.local。同namespace下可以直接用Service名访问,跨namespace要带上namespace名再拼完整域名。这也是微服务之间互相调用的标准方式。

3.2 NodePort:外部访问的入门方案

apiVersion: v1 kind: Service metadata: name: my-app-nodeport namespace: prod spec: type: NodePort selector: app: my-app env: prod ports: - port: 80 targetPort: 8080 nodePort: 30080

NodePort是在ClusterIP基础上,在每个节点上额外开一个端口(默认范围30000-32767)。访问方式变成http://<任意节点IP>:30080,流量会从节点端口进来,转发到ClusterIP,再转发到Pod。

nodePort字段可以手动指定,也可以不写,让K8S自动分配一个空闲端口。手动指定的好处是方便配置防火墙白名单和负载均衡器监听规则,坏处是有可能被占用(创建时API Server会检查)。

使用NodePort有几个实际注意点。节点IP得能从外部访问到,如果你有多套环境共用机器,要规划好端口,别冲突。另外nodePort默认范围是可以调的,在kube-apiserver启动参数里改--service-node-port-range,但一般建议保持标准范围,避免外部设备不识别高位端口。

NodePort适合什么场景?本地开发、小规模集群、不想额外引入负载均衡器的场景。生产环境你要是直接用NodePort暴露到公网,我见过不少,但更常见的做法是NodePort配合外部的负载均衡器(比如Nginx、HAProxy)使用,外部LB转发到多个节点的NodePort。

3.3 LoadBalancer:云环境标准姿势

apiVersion: v1 kind: Service metadata: name: my-app-lb namespace: prod annotations: # 云厂商自定义的注解,用于配置LB的具体参数 # 具体键名因云厂商而异 spec: type: LoadBalancer selector: app: my-app env: prod ports: - port: 80 targetPort: 8080

在云平台上创建LoadBalancer类型的Service,云厂商的控制器会自动调用云API创建一个负载均衡实例,并且自动配置好监听器和后端转发规则。你的应用等于直接拥有了一个可以从公网访问的入口。

注意LoadBalancer其实是NodePort的超集,它内部仍然会分配nodePort,云LB会绑定到节点的NodePort上,而不是直接绑定Pod。这意味着每个负载均衡Service额外的nodePort也会被分配,只是在HostPort层面占用。如果你的集群是自建的(比如裸金属部署的K8S),没有云厂商的控制器,LoadBalancer类型创建完会一直卡在Pending状态,需要安装MetalLB这类实现。

LoadBalancer的一个重要限制是每个Service都会创建一个独立负载均衡实例,成本和时延都得考虑。多个服务更经济的方式是用Ingress,一个入口转发到多个Service,那是另一个话题,这里不展开。

3.4 ExternalName:特殊但偶尔很管用

apiVersion: v1 kind: Service metadata: name: external-db namespace: prod spec: type: ExternalName externalName: db.example.com

ExternalName是一个轻量级Service,它不创建ClusterIP,也没有Endpoints,只是在CoreDNS里配置了一条CNAME记录。当你在集群内访问external-db.prod.svc.cluster.local时,DNS解析会跳到db.example.com。

它可以用来把外部服务的域名"包装"成K8S内部的Service名,这样应用配置里不需要硬编码外部域名,改起来方便。也可以用来对接一些不在集群内的旧系统,或者做服务迁移时的兼容层。需要注意ExternalName只支持DNS解析,如果你要访问的外部服务需要走HTTP并有路径路由的需求,ExternalName可能不够用,需要自己再包一层代理。

3.5 四种类型的选择对照

我用一张表总结了下四种类型的关键差异,方便你选型时不纠结。

类型是否分配ClusterIP是否支持集群外访问典型场景注意事项
ClusterIP是否集群内微服务间调用默认类型,集群外不可直达
NodePort是是(通过节点IP+端口)简单的外部访问测试、小规模服务端口范围30000-32767,受节点IP可达性影响
LoadBalancer是是(通过云负载均衡器)云上生产环境的公网入口依赖云厂商的控制器或MetalLB
ExternalName否取决于外部目标访问集群外部已有服务只做DNS CNAME,无流量转发能力

选型思路就一句:集群内访问用ClusterIP,需要外部访问但没有云资源用NodePort,云上有LB需求用LoadBalancer,访问外部服务用ExternalName。

4. 实操全流程:从编写YAML到调试验证

理论说完了,实际动手的时候你得有一套标准的操作流程。一个Service从无到有到验证通过,整个流程包含四步:确认后端Pod running、编写Service YAML、创建Service并检查Endpoints、实际验证访问。

4.1 确认后端Pod的状态和labels

Service不是凭空出现的,它的selector必须能匹配到Pod,所以第一步是确认Deployment或者Pod已经正常运行,并且labels是你预期的那一套。

# 查看命名空间下所有Pod及其labels kubectl -n prod get pods --show-labels

输出里会显示每Pod的labels,比如app=my-app,env=prod。检查labels时不要漏看HEAD部分,有的Pod创建时label很多,你只摘出自己需要的两三对来匹配就行。

还有人可能遇到这种情况:Pod状态显示Running,但通过Service访问时一直超时。排查一圈发现Pod起来了,但容器里的进程根本没监听targetPort。Service能配置对,Pod能匹配到,但转发后没用,因为健康检查只到容器层面,没到端口层面。做个端口连通性测试很有必要。

# 进入集群内一个Pod,测试后端端口是否真正可用 kubectl -n prod exec -it any-pod -- curl http://<某个Pod的IP>:8080

4.2 编写Service YAML的通用模板

我习惯用模板来避免因为笔误出问题。一份最通用的Service YAML长这样:

apiVersion: v1 kind: Service metadata: name: app-svc namespace: prod labels: app: app-svc spec: type: ClusterIP selector: app: my-app env: prod ports: - name: http port: 80 # Service暴露的端口 protocol: TCP targetPort: 8080 # 必须与容器实际监听端口一致 # 如果需要外部访问,按需调整type和对应配置

保存为svc.yaml后,执行:

kubectl apply -f svc.yaml

apply是声明式的,如果Service已存在则更新,如果不存在则创建。用kubectl create也可以,但它要求目标对象当前不存在,在CI/CD脚本里我个人更推荐apply。

4.3 创建后的验证三部曲

创建完Service不能直接走人,至少做三件事验证。

第一步看Service基本信息:

kubectl -n prod get svc app-svc -o wide

重点关注EXTERNAL-IP列和PORT(S)列。ClusterIP类型的EXTERNAL-IP是None,这正常。LoadBalancer类型如果EXTERNAL-IP一直Pending,就要检查云厂商控制器是否正常。

第二步检查Endpoints:

kubectl -n prod get endpoints app-svc

Endpoints列表有IP说明Selector正确选了Pod。如果显示<none>,说明后端没匹配上,优先检查namespace和labels。这里我要强调一下:Endpoints对象是Service的"实际后端状态",它是由Endpoints控制器根据Selector自动生成的,不要手动去改Endpoints(虽然有API,但改了会被控制器回填)。

第三步做实际的连通性测试。在集群内任意节点或Pod里测试:

# 从集群内Pod访问Service kubectl -n prod exec -it any-pod -- curl http://app-svc.prod.svc.cluster.local

如果用的是NodePort,在节点上直接测:

curl http://<nodeIP>:30080

三步走完,Service通了基本就是真通了。

4.4 会话保持配置的细节

Stateful应用需要同一客户端的请求都落到同一Pod上,这就是会话保持。Service层面通过spec.sessionAffinity字段控制,支持None和ClientIP两种。

spec: sessionAffinity: ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 10800

配置成ClientIP后,kube-proxy会根据客户端来源IP做哈希,同一个IP的请求会尽量转发到同一个Pod。timeoutSeconds控制会话保持的超时时间,默认10800秒,即3小时。

这里有个明显的副作用需要意识到:如果你在集群外部前面还有一层负载均衡器做NAT或者代理,所有客户端IP都变成LB的IP,那会话保持就会失效,所有流量都哈希到同一个Pod上,完全丧失负载均衡能力。解决方案是用Ingress层的会话保持或应用层Session,而不要依赖Service的ClientIP。

5. 常见问题与排查技巧实录

Service问题排查是我日常工作中遇到频率最高的。把所有遇到过的坑整理了一下,按从"最常见"到"最隐蔽"排个序。

5.1 Endpoints为空,最经典的问题

症状:Service创建了,访问不通,kubectl get endpoints显示<none>。

排查步骤:

  1. 确认Service和Pod在同一namespace。
  2. 确认selector的labels和Pod的labels完全匹配,注意有没有拼写错误、额外的空格、key和value的格式问题。
  3. 确认Pod确实处于Running状态。容器一直CrashLoopBackOff的话,Pod可能被重建,IP在变,Selector匹配上也没用。
  4. 检查Pod是否被调度到不可达的节点上。某些情况下节点网络异常,Pod能跑但网络不通,Endpoints存在但流量到不了。
  5. 如果以上的没问题,看event:
kubectl -n prod describe svc app-svc

大部分问题出在第二步,我遇到80%的Endpoints为空都是labels写错或者namespace不一致。

5.2 端口配置错误导致的“可以解析但无法连接”

症状:DNS解析能拿到Service名,但请求超时或connection refused。

这种情况先确认targetPort。我见过有人把targetPort写成8080,但容器真正监听的是5000,流量转过去直接被拒绝。用kubectl -n prod get endpoints app-svc -o yaml查看Endpoints的port字段,确认和容器实际监听端口一致。

还有一个容易忽略的点:多容器的Pod。一个Pod里有多个容器时,targetPort如果不指定容器名,kube-proxy会把流量都转发到Pod的第一个容器端口,如果你期望转到第二个容器(比如sidecar容器),就得使用命名端口,并在Pod定义里明确命名不同容器端口。

5.3 NodePort类型端口冲突或不可达

症状:NodePort类型Service创建成功,但外部节点IP+端口访问不了。

常见原因有三个:

  1. 节点的防火墙/安全组没放行nodePort范围。这是最常见的,需要检查云安全组或iptables规则。
  2. 访问的不是运行中Pod所在的节点,但NodePort类型是全节点监听的,理论上访问任意节点都通。如果只有特定节点不通,检查那个节点的kube-proxy是否正常运行。
  3. nodePort端口范围被修改过,而客户端访问的是默认范围。确认服务实际分配的端口,用kubectl get svc看PORT(S)列,最保险。

5.4 会话保持失效的分布不均问题

症状:配置了sessionAffinity: ClientIP后,流量还是分散到不同的Pod。

这个我前面提过,如果上游有代理做NAT,源IP被隐藏了,所有客户端看起来都是一个IP,哈希就全指向同一个Pod,这会导致某个Pod负载过高。可以从入口加X-Forwarded-For并让Service层识别,但实际更常用的方案是在应用层实现会话保持。

5.5 LoadBalancer一直Pending

症状:LoadBalancer类型Service创建后,EXTERNAL-IP一直显示Pending。

自建集群没有云厂商的负载均衡控制器,或者云厂商controller-manager异常,都会导致这个问题。看cloud-provider组件日志能定位。如果是裸金属集群,可以考虑部署MetalLB来提供load balancer的实现,它通过BGP或Layer2模式把NodePort或Pod IP广播给外部网络。

5.6 UDP端口超时和丢包

面向UDP的Service排查起来比TCP麻烦得多,因为UDP没有握手和连接状态,丢包了你很难定位。kube-proxy的负载均衡是基于conntrack的,UDP同样会被跟踪,但某些场景下conntrack表项老化过快,导致UDP包频繁被分到不同后端,造成服务异常。

如果业务是UDP且对后端一致性有要求,建议对UDP Service关闭会话保持相关配置,或者用headless service让客户端直接连后端做应用层处理。

5.7 问题速查表

症状优先排查项次要排查项
Endpoints为空selector与labels不匹配namespace不一致、Pod未Running
Service DNS可以解析但连接超时targetPort是否正确Pod容器端口是否真正监听
NodePort外部访问不通安全组/防火墙放行kube-proxy状态、端口范围
LoadBalancer一直Pending云厂商控制器状态自建集群缺少LB实现
会话保持不生效上游代理是否做NAT超时时间配置
UDP服务偶发失败conntrack表状态后端Pod是否有UDP监听

6. 一些写在最后的实操体会

Service看起来简单,但它连接了K8S的多个核心组件:API Server存储声明状态,Endpoints控制器维护后端列表,kube-proxy把规则下发到每个节点,DNS服务提供域名解析。任何一个环节出问题,表现都是"Service不通",但定位路径完全不同。

我的个人习惯是,任何Service变更都遵循"先确认后端Pod,再确认selector,再确认端口,最后看访问"的顺序。遇到"通一下不通一下"的诡异问题,优先检查网络层(kube-proxy日志、节点网络插件状态),而不是反复盯着YAML文件看。YAML写错会立刻暴露,网络问题才是AM隐性的。

最后分享一个小技巧。调试Service时,别只依赖kubectl get svc,多看看kubectl get endpoints和kubectl describe svc。Endpoints是后端真实状态的"照妖镜",它有没有IP,一眼就能看出来selector是否匹配成功。比跑到节点上抓包快得多。掌握这个习惯后,你会发现Service排查的效率能提高一大截。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询