Kubernetes里的Service资源,可能是所有k8s使用者“听得最多、理解最浅”的一个对象。我见过不少同事,Deployment写得滚瓜烂熟,Pod一跑就高呼“服务起来了”,结果联调时碰到Service就两眼一抹黑,端口不通、DNS解析失败、负载不均衡,各种问题冒出来,折腾半天才发现是对Service的理解出了偏差。这篇文章就以多年生产运维的视角,把k8s里的Service资源从设计原理到实际排障,一次性讲透,包括它的类型选型、底层转发机制、常见配置误区和线上故障排查套路。
适合几类人看:正在学k8s的新手、准备考CKA或者面试的兄弟、以及已经在用k8s但经常被Service问题折磨的运维和开发。读完你会得到一套可以直接复用的YAML模板,以及一套高成功率的排查顺序。
1. 为什么Kubernetes绕不开Service资源
1.1 Pod的“短命”特性决定了访问入口必须稳定
先从一个最基本的事实说起:在k8s集群里,Pod是“用完即丢”的。无论是Deployment滚动更新、节点故障导致Pod重建,还是HPA自动扩缩容,每个Pod在被创建时都会重新分配IP地址,这个IP只在Pod存活期间有效。
这一点和传统虚拟机时代非常不一样。以前你给一台服务器配个固定IP,服务部署上去,只要机器不宕机,其他业务直接通过IP访问就可以。但k8s里Pod频繁调度、重建、迁移,如果让调用方直接硬编码Pod IP,等于把稳定性建立在沙滩上:只要一次重建,所有调用方的配置都要跟着改,这在生产环境里是完全不可接受的。
Service恰恰就是为这个痛点设计的。它通过标签选择器动态追踪一组Pod,对外提供一个不会变的虚拟IP和DNS名字。后端Pod怎么变、IP怎么换,调用方完全无感知。理解这个前提,后面所有配置细节才有落脚点。
1.2 Service的资源定位:解耦与发现
从资源定位来看,Service至少承担了三层职责:
第一层,稳定的访问入口。Service的ClusterIP在整个生命周期内保持不变,除非你手动删掉重建。服务间互相调用时,使用这个稳定的VIP或DNS名称,能彻底摆脱Pod IP变化带来的冲击。
第二层,负载均衡。当一个Service背后挂着多个Pod副本时,Service会根据转发规则把流量分摊到不同Pod上。默认情况下,iptables模式是随机选后端,IPVS模式可以用轮询等多种算法,这部分后面单开一节讲。
第三层,服务发现。k8s集群内置的CoreDNS会自动为Service生成DNS记录。比如你在default命名空间创建了一个名为order-svc的Service,其他Pod内部直接访问order-svc.default.svc.cluster.local就能找到它;同命名空间下甚至可以简写成order-svc。这个能力让微服务架构下的动态服务发现变得极其简单。
这三层职责加起来,Service就成了一切内部流量的“交通枢纽”。
1.3 Service与Deployment的关系:很多人在这里理解偏了
新手最常犯的一个错误,是把Deployment和Service当成绑定关系,认为“一个Service必须对应一个Deployment”。
实际上Service根本不管理Pod生命周期,它只管“路由”。Deployment负责创建和维持Pod副本数,Service负责把稳定入口绑定到符合标签条件的Pod上。两者是解耦的,靠标签选择器建立关联。
而且一个Service可以对应多个Deployment的Pod,只要标签匹配就行。比如你有多个后端服务都带app=backend这个标签,一个Service就能把流量分发到它们上面。反过来说,一个Deployment也可以被多个Service同时选中,比如一个Service暴露内部接口,另一个Service用NodePort暴露到集群外部,两者互不影响。
提示:排查Service问题时,第一件事永远是确认标签选择器是否真的匹配到了目标Pod,而不是先怀疑Deployment。
搞清这点,后面所有操作才不会跑偏。
2. Service的类型选择:从ClusterIP到ExternalName
2.1 ClusterIP:集群内部访问的首选
ClusterIP是Service的默认类型。创建后k8s会从服务网段里分配一个虚拟IP,这个IP只能从集群内部访问。
配置示例:
apiVersion: v1 kind: Service metadata: name: core-api-svc namespace: production spec: type: ClusterIP selector: app: core-api ports: - name: http protocol: TCP port: 80 targetPort: 8080这里有个特别容易搞混的点:port指的是Service对外暴露的端口,也就是其他服务访问core-api-svc时使用的端口;targetPort才是Pod内容器实际监听的端口。这种设计是为了让Service层和Pod层各自独立演进——即使内部容器端口从8080改成9090,只需要改targetPort,调用方完全不用调整。
ClusterIP最适合的场景是微服务之间的内部调用、控制面组件通信、以及不需要暴露给外部用户的业务。绝大多数内部服务都应该优先选用ClusterIP,而不是一上来就NodePort。
2.2 NodePort:没有云负载均衡时的外部入口
当外部流量需要进入集群,而你又没有云厂商的LoadBalancer时,NodePort是最直接的方案。
它的原理很简单:Service会在集群的每个节点上开一个指定端口,任何节点的该端口都能访问到这个Service。
apiVersion: v1 kind: Service metadata: name: web-public namespace: production spec: type: NodePort selector: app: web ports: - port: 80 targetPort: 80 nodePort: 30080注意几个约束:nodePort的合法范围默认是30000-32767,如果我不写nodePort字段,k8s会在范围内自动分配一个端口。生产环境里我习惯显式指定,方便配合防火墙规则和安全组策略。
NodePort的短板也很明显:端口需要人工管理,30000-32767区间有上限;访问入口变成了每个节点,如果某个节点故障,指向该节点的访问就会失败;而且外部流量进来后,多了一层额外的NAT转发,存在延迟和源IP丢失的问题。
所以我的建议是:测试环境、临时演示、小型私有化部署可以用NodePort;大规模生产环境,有条件还是上LoadBalancer或者Ingress Controller。
2.3 LoadBalancer:云环境下的“一键”接入
LoadBalancer类型可以理解为NodePort的升级版。它在创建Service的同时,会调用云厂商提供的负载均衡服务,自动为你分配一个公网IP或域名,并将流量转发到Service。
apiVersion: v1 kind: Service metadata: name: app-lb spec: type: LoadBalancer selector: app: app ports: - port: 80 targetPort: 8080各个公有云平台的实现大多是:云控制器管理器(CCM)监听LoadBalancer类型的Service,自动创建对应负载均衡实例,并将后端的节点端口加入监听列表。使用者不需要关注底层细节。
这里有个容易忽略的点:LoadBalancer本质上是建立在NodePort之上的。也就是说就算你配置的是LoadBalancer类型,集群节点上依然会占用nodePort端口。云平台的负载均衡器也是将流量转发到节点的这个端口,再进入Service转发链路。这解释了为什么有些用户创建了LoadBalancer后,用kubectl get svc会发现端口列表里冒出一个30000多段的端口。
LoadBalancer的成本相对较高,尤其是多环境多服务都独立创建时,公网LB数量会非常可观。实际生产里,我更推荐内部服务用ClusterIP,对外暴露的统一入口交给Ingress Controller,必要时再为个别特殊业务单独创建LoadBalancer。
2.4 ExternalName与Headless Service:两种特殊形态
ExternalName和Headless Service虽然不算常用,但踩坑时遇到了也不能不懂。
ExternalName类型的Service不带选择器,也不会生成ClusterIP和任何后端转发。它只在DNS层面做CNAME映射。比如你的应用需要访问集群外部某个数据库,又不想把外部地址硬编码到代码里:
apiVersion: v1 kind: Service metadata: name: external-db namespace: production spec: type: ExternalName externalName: db.example.com这样应用只需要访问external-db.production.svc.cluster.local,就会被DNS解析到db.example.com。好处是后续数据库迁移、域名变更时,应用代码和配置都不用动,只改Service定义即可。
Headless Service则是在Service的spec.clusterIP字段显式写None,k8s不会分配ClusterIP,也不会做统一的负载均衡。它的意义在于让DNS查询直接返回所有后端Pod的真实IP地址。这在StatefulSet场景中特别有用:有状态应用需要识别每一个Pod的独立身份,客户端希望直接连接指定Pod,而不是经过负载均衡。
apiVersion: v1 kind: Service metadata: name: mongo-svc spec: clusterIP: None selector: app: mongo ports: - port: 27017 targetPort: 27017配合StatefulSet,每个Pod都能拿到带序号的稳定DNS名,比如mongo-0.mongo-svc.default.svc.cluster.local。数据库集群节点间通信、主从同步常常依赖这个机制。
提示:Headless Service适用于自己实现负载均衡、需要知道真实后端IP的场景。把无状态应用也设置成Headless,通常等于放弃了内置负载均衡能力,收益不大。
3. Service背后的三大支撑机制
3.1 Endpoints与EndpointSlice:后端列表怎么来的
Service本身并不保存Pod地址,它维护的是一个叫作Endpoints的对象。当Service有匹配的Pod时,k8s控制面会自动创建同名Endpoints,里面记录所有符合条件的Pod IP和端口。
kubectl get endpoints core-api-svc -n production kubectl describe endpoints core-api-svc -n production查看后你会看到类似这样的输出:一组IP列表,每个IP对应一个Pod地址和targetPort。只要Pod的标签发生变化、Pod被重建,Endpoints都会自动同步更新。这个同步过程一般发生在秒级以内。
在新版本k8s里,更底层的机制是EndpointSlice。它是对Endpoints的拆分和扩展,每个Slice最多承载100个后端地址。当Service后端Pod数量巨大时,通过多个Slice并行更新,能显著降低控制面和数据面的压力。这也是为什么在大型集群中,kube-proxy可以从EndpointSlice读取后端列表做转发。
理解Endpoints的意义在于排障:如果你发现Service的Endpoints是空的,说明标签选择器没有匹配到任何Pod,这是所有Service网络故障里最常见的原因之一。
3.2 kube-proxy的转发模式:从iptables到IPVS
Service底层流量转发依赖每个节点上运行的kube-proxy组件。kube-proxy有三种模式,分别是userspace、iptables和IPVS。
userspace模式是最早的实现,所有流量都要经过kube-proxy进程转发,性能很差,现在基本只有老资料里才会提到,生产环境已经看不到了。
iptables模式是当前默认模式。kube-proxy会为每个Service和每个Endpoints生成一组iptables规则,当请求到达节点时,由内核iptables匹配规则并随机选择一个后端Pod做DNAT转发。优点是可靠、兼容性强、不依赖额外内核模块;缺点是当集群Service数量巨大时,iptables规则会膨胀到几万条,规则更新变成了全量刷新,耗时明显,链路匹配也会因链路过长而增加延迟。
IPVS模式则是基于内核的IPVS模块,它把转发规则维护在一张哈希表里,查询效率远高于iptables线性匹配,同时支持更多负载均衡算法。配置kube-proxy的mode为ipvs需要保证节点内核加载了相关模块,比如ip_vs、ip_vs_rr、ip_vs_wrr、nf_conntrack等。
从我的实践来看,超过几百个Service规模的集群,建议直接切IPVS。我曾经在一个约300个Service的测试集群里做过对比,iptables模式下新增Service后规则同步偶尔会产生秒级的延迟,切换IPVS后几乎感受不到规则变更的开销,负载均衡也更稳定。
3.3 负载均衡策略与会话保持
很多人以为k8s默认的Service负载均衡是“轮询每个Pod”,其实不完全对。
iptables模式下,kube-proxy是利用iptables的statistic模块做随机选择,权重基本都是均等的,但它是概率均等,不是严格轮询。也就是说短时间内的多次请求很可能连续命中同一个Pod,只有在请求量足够大的时候才会呈现整体均衡。IPVS模式下则可以配置调度算法,如轮询(rr)、加权轮询(wrr)、最少连接(lc)、源地址哈希(sh)等,灵活性高很多。
除了后端调度算法,Service还有一个会话保持的配置项sessionAffinity。默认是None,每个请求独立选择后端;如果设置为ClientIP,同一个来源IP的请求会被固定转发到同一个后端Pod。
spec: sessionAffinity: ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 10800这个特性应对无状态服务意义不大,但在有状态场景下非常有用。比如你的应用在同一会话里需要多次访问同一节点的本地缓存,如果每次请求都跳到不同Pod,缓存命中率会急剧下降。
注意:依赖Ingress和Service两层负载均衡时,客户端IP可能已经变化,
sessionAffinity: ClientIP未必能生效。需要结合前置负载均衡器的真实客户端IP透传能力一起设计。
4. 从YAML到流量:一次完整的Service创建过程
4.1 核心字段逐个拆解
写Service YAML看似简单,真正把每个字段的含义讲明白,能避免一半配置事故。
先看apiVersion和kind,Service属于核心v1资源,所以固定是apiVersion: v1、kind: Service。这个不需要多想。
metadata.name是Service在命名空间里的唯一标识,也是DNS记录的第一部分,命名最好直接体现业务含义,比如payment-svc、user-svc。不要用test1、svc2这种,指代性太差了。
metadata.namespace指定命名空间,不写就是default。生产环境务必规划好命名空间,Service的访问DNS会带上namespace,比如payment-svc.pt在pt命名空间下对应payment-svc.pt.svc.cluster.local。
spec.type是服务类型,默认ClusterIP。另外还有前面讲到的NodePort、LoadBalancer、ExternalName。
spec.selector用来筛选后端Pod。它的匹配规则是等值匹配,多个标签之间是AND关系,也就是说Pod必须同时满足所有标签条件才会被选中。
spec.ports是端口映射列表,支持一个Service暴露多个端口,比如同时暴露80端口做HTTP、3306端口做MySQL内部协议。多端口时必须给每个端口起名字,后续在Istio等场景里会经常用到端口名。
字段port是Service对外端口,targetPort是Pod内实际端口。targetPort可以写成数字,比如8080,也可以写成Pod中定义的端口名,例如targetPort: http-port,这样容器端口调整时,Service配置不用跟着改。
4.2 一个可以直接抄作业的生产示例
下面我给出一个实际部署中用到的完整示例,包含Deployment和Service两部分,读者可以直接复制修改使用。
apiVersion: apps/v1 kind: Deployment metadata: name: order-api namespace: business spec: replicas: 3 selector: matchLabels: app: order-api template: metadata: labels: app: order-api spec: containers: - name: order-api image: registry.internal/order-api:v2.3.1 ports: - name: http-port containerPort: 8080 readinessProbe: httpGet: path: /healthz port: http-port initialDelaySeconds: 5 periodSeconds: 10 --- apiVersion: v1 kind: Service metadata: name: order-api-svc namespace: business spec: type: ClusterIP selector: app: order-api ports: - name: http protocol: TCP port: 80 targetPort: http-port这个配置有几点讲究:
第一,Service的selector只写了app: order-api一个标签,和Deployment模板中的Pod标签完全对应。如果中间还加了version等标签,务必保证所有Pod都写了对应标签。
第二,targetPort直接引用了容器端口名http-port,这样不依赖具体数字。真实场景里读接口文档时,端口是多少就写多少,但用命名引用更健壮。
第三,preparation的readinessProbe虽然不属于Service范畴,但它对Service是否能把流量转发到Pod有直接影响。从k8s 1.14开始,Service Endpoints只包含就绪状态为Ready的Pod。如果Pod一直不Ready,Service有后端也不会转发流量。这个坑特别隐蔽。
第四,命名空间统一为business,访问地址简化后就是order-api-svc,集群内跨命名空间访问时需要写成order-api-svc.business.svc.cluster.local。
4.3 创建后的验证与调试路径
应用完YAML后,不要直接走人,按下面几步做一次快速体检:
kubectl apply -f order-api.yaml kubectl get svc order-api-svc -n business kubectl get endpoints order-api-svc -n business kubectl get pod -n business -l app=order-api如果一切正常,get svc会看到一个ClusterIP和一个80端口;get endpoints会列出三个Pod IP和8080端口。Pod列表也应该显示三个Ready状态的Pod。
接下来在集群内任意Pod里做连通性测试:
kubectl run curl-test --rm -it --image=curlimages/curl -- sh curl http://order-api-svc.business.svc.cluster.local/healthz返回200说明整个链路通。如果访问超时或拒绝,再进入下一步的排查阶段。另外,还可以用kubectl describe svc order-api-svc -n business查看事件和具体字段,排查是否有匹配异常。
5. 生产环境里的Service故障排查实录
5.1 连接不上时,按这个顺序排查
线上遇到Service访问失败,最忌讳的是东点一下西试一下。我自己总结了一套固定排查顺序,基本能覆盖八成问题,从下往上层层递进。
第一步,确认Pod状态。执行kubectl get pods -n <namespace> -o wide,先保证有Pod处于Running且Ready状态。如果Pod本身重启循环或不Ready,Service链路必然异常。
第二步,检查Endpoints。执行kubectl get endpoints <svc-name> -n <namespace>,看Endpoints里有没有后端IP。如果显示<none>,说明selector没匹配到Pod,或者Pod还没Ready。这一步能分离出一半以上的问题。
第三步,验证DNS解析。进入一个临时Pod,执行nslookup order-api-svc.business.svc.cluster.local,确认能解析到Service的ClusterIP。解析失败则检查CoreDNS状态、Service所属命名空间是否正确。
第四步,检查Service本身。kubectl describe svc <svc-name>可以查看selector、端口映射、类型等所有细节,顺便看看Service没有报什么事件。
第五步,检查节点网络转发。登录到集群节点,针对ClusterIP和端口做一次curl或nc测试,确认数据能不能到达节点层。能到节点但进不了容器,问题大概率在kube-proxy或iptables/IPVS规则层。
第六步,检查kube-proxy状态。kubectl get pod -n kube-system -l k8s-app=kube-proxy,查看是否有异常重启或者日志报错。
这个顺序我用了很多年,能稳定定位大多数Service网络故障。
5.2 高频踩坑点一览
第一个高频坑是selector标签不匹配。我在一个项目里遇过,Deployment里Pod标签明明写的run=web,Service声明的selector写的app=web,Endpoints永远是空的。这种问题特别容易发生在手写YAML或者不同人员各写一半的情况下。
第二个坑是targetPort写错。曾经有同事把Service的targetPort写成80,但容器实际监听的是8080,结果curl Service端口直接Connection Refused。所以创建Service后一定要回头看容器端口定义,两个端口别想当然。
第三个坑是Pod没Ready。很多开发只盯着Pod Running就以为万事大吉,结果Service不把没通过readinessProbe的Pod加入Endpoints,流量全部异常。这里记得先看Endpoints里实际地址数量。
第四个坑是externalTrafficPolicy设置为Local后,流量只会被转发到“存在Pod副本的节点”。如果某个节点上没有Pod,这个节点上的NodePort访问就会失败。尤其在做负载均衡测试时,会间歇性不通。
第五个坑是用NodePort时防火墙没放行。云厂商安全组、自建机房iptables默认可能只放行常用端口,30000-32767段很容易被遗落,表现为集群内访问正常、集群外访问超时。
5.3 故障排错速查表
| 症状 | 可能原因 | 首选排查命令 |
|---|---|---|
| Service访问超时 | Endpoints为空或Pod未Ready | kubectl get endpoints |
| Connection refused | targetPort与容器监听端口不一致 | kubectl describe svc |
| DNS解析不到名称 | Service所在命名空间/名称写错,或CoreDNS异常 | nslookup <svc>.<namespace>.svc.cluster.local |
| 只有部分节点可访问 | externalTrafficPolicy: Local导致Pod分布不均 | kubectl get pod -o wide |
| 集群外访问超时 | 防火墙/安全组未放行nodePort段 | curl <nodeIP>:<nodePort> |
| 扩容后仍然流量不均 | iptables随机模式短时概率不均 | 切换IPVS并重测 |
| 同一客户端反复切换后端 | sessionAffinity未配置或前级LB改变源IP | 检查Service sessionAffinity字段 |
这张表适合贴在本地笔记里,遇到问题先查表,再对着5.1的顺序逐层定位。
6. 生产架构里的Service最佳实践
6.1 命名、标签与命名空间规范
Service资源在运维中会积累很多,命名规范直接决定后续查问题的效率。我建议所有Service的命名链路统一采用“业务模块+功能后缀+srv”的格式,比如order-srv、user-srv、auth-api-srv,并严格与Deployment的app标签保持一致。
命名空间按环境隔离,比如dev、stage、prod。同环境下的Service之间默认可以互通,不同环境的Service通过命名空间隔开,避免开发环境把生产服务误调了。
标签规划上,尽量用app表示业务名,用tier表示层级或者version表示版本。Service selector里通常只写稳定的业务标签,版本标签不要写进selector,否则滚动更新时新旧Pod标签变化,会造成服务短暂不可用。
6.2 监控Service健康状态的三个黄金指标
第一个是Endpoint数量。监控项里必须有kube_endpoint_address_available这类指标,只要它持续为0,服务就是不健康的,应立即告警。
第二个是请求成功率。借助Metrics Server或者Prometheus采集Pod业务指标,配合Service做聚合,看5xx比例是否异常。这个指标比单纯检查Pod是否Running真实得多。
第三个是连接失败率。在集群节点层采集conntrack相关指标,或者通过Ingress统一入口观察TCP握手失败数,能及早发现kube-proxy规则异常、节点网络故障等问题。
我见过最典型的事故就是某个Service的Endpoints因为标签漂移变成空,Curator任务反复重试把数据库连接池打爆,直到业务方反馈才被发现。如果Endpoint数量指标配了告警,完全可以在业务受损前提前介入。
6.3 运维视角的几条个人体会
先说IPVS模式。这是我做过的最值的集群级调整之一。几百个Service规模的集群,iptables规则同步偶尔会出现秒级延迟,切到IPVS后不仅查询快,还支持rr等调度算法,规则更新也从全量刷新变成了哈希表增量更新。只要内核模块加载好,稳定性足够可靠。
再说externalTrafficPolicy。如果外部流量要进到集群,尽量使用externalTrafficPolicy: Cluster的默认模式,它会做一层转发,但保留源IP会更可靠,应在LoadBalancer或Ingress层处理,不要为了保留源IP在Service层牺牲负载均衡的均匀性。
最后一条建议:每创建一个Service,都要同步建立对应的健康检查机制。不是建完就结束了,而是要让它的状态在你的监控大屏上“肉眼可见”。只有监控到位,Service才算真正被纳入了生产运营体系。
最后分享一个实用小技巧:排查Service问题时,几乎总能通过三个命令快速缩小范围——kubectl get endpoints看后端、kubectl describe svc看配置、kubectl logs看应用日志。把这三个命令的查询条件提前写成shell别名,能省下大量敲命令的时间。我到现在依然保持着这个习惯,它帮我解决过无数次莫名其妙的“Service连不上”问题。