最近在整理Kubernetes系列笔记正好写到Service这一块。前面几篇把Pod、Deployment、StatefulSet都捋了一遍到了Service这里很多朋友开始犯迷糊——明明Pod能跑起来Deployment也能正常伸缩但一到访问服务就抓瞎要么IP不通要么域名解析不到要么端口对不上。这篇文章专注把Service这个抽象层的应用管理讲透重点放在怎么选类型怎么写配置怎么排障这三个实际操作层面。如果你已经会用kubectl跑起一个Pod但对Service的理解还停留在好像是个负载均衡的阶段这篇内容会比较对路。Service这个名字直译叫服务但在K8s里它承担的角色远不止服务两个字——它本质上是Pod和外部流量之间的一个稳定抽象层。因为Pod是朝生暮死的IP地址每次重建都会变如果让客户端直接记Pod IPPod一挂整个链路就废了。Service通过标签选择器selector动态绑定一组Pod提供一个稳定的虚拟IPClusterIP和DNS名字客户端只需要记住Service的标识后端Pod怎么换都不影响访问。这套机制是整个K8s网络模型里最核心也最容易踩坑的部分。正文我会按理解定位→类型选型→配置要点→实操验证→排障实录→进阶特性这条线展开每个阶段都配上我实际维护集群时遇到过的真实场景和解决过程。1. Service在K8s工作负载里的角色定位1.1 为什么Pod不能直接对外访问很多人刚接触K8s会有个疑问Deployment已经帮我把Pod管理起来了Pod里有容器容器里有应用应用监听端口也开了为什么外部就是访问不到先说结论K8s集群里的Pod默认只在一个隔离的网络命名空间里它有自己的IP地址但这个IP地址有几个致命问题——第一Pod重建后IP会变Deployment滚动更新、节点故障转移、副本重新调度都会导致IP漂移第二Pod IP是集群内网地址外部网络包括集群外的其他机器、浏览器、客户端根本不可达第三一组相同的Pod副本各自有不同的IP客户端需要一个统一的入口来做负载均衡。这就是Service存在的根本原因。它像一个中间层代理把一组动态变化的Pod IP聚合成一个固定不变的虚拟IP。客户端只跟Service打交道Service背后怎么调度Pod那是K8s内部的事。1.2 Service与Endpoints的联动机制理解Service的关键是搞明白它和Endpoints的关系。Service本身不直接连Pod它通过标签选择器筛选出符合条件的Pod然后把Pod的IP:Port列表交给Endpoints对象维护。换句话说Service是门面Endpoints是通讯录。当你执行kubectl get endpoints时能看到每个Service对应的后端地址列表。如果Service的selector写错了或者Pod的标签没对上Endpoints列表就是空的这时候Service即使存在访问也会失败。这类问题在实际运维中出现频率极高后文排障部分我会专门讲。Service创建后K8s控制平面的Controller Manager会持续watch后端Pod的变化。Pod新增、减少、健康检查失败Endpoints列表都会随之更新。这个联动是实时的但依赖kube-apiserver的事件推送所以如果apiserver负载过高或者网络有抖动可能短暂出现Endpoints更新延迟——这也是排障时容易忽略的一个点。2. Service四种类型分别用在哪里2.1 ClusterIP默认类型集群内部访问的基石ClusterIP是Service的默认类型。创建一个不带type字段的ServiceK8s会自动分配一个集群内网IP这个IP只能在集群内部访问——包括同一Namespace的Pod、其他Namespace的Pod需要跨Namespace访问时用完整DNS名以及集群内的节点如果节点上配置了相应的路由规则。ClusterIP适合什么场景最常见的是微服务之间的内部调用。比如订单服务要调用户服务的接口直接请求用户服务Service的DNS名user-service即可K8s内置的DNS组件CoreDNS会自动解析到ClusterIP。这样服务之间的调用关系通过Service解耦不需要写死IP后端扩缩容对调用方完全透明。这里有个核心知识点ClusterIP是虚拟IP它不是真实网卡上的地址而是通过kube-proxy组件在集群每个节点上写入iptables或IPVS规则来实现的。所以ClusterIP在集群内的任何节点上都能通因为它是一套分布式规则而不是某一台机器上的IP。2.2 NodePort把服务暴露到集群外部的基础方案当集群外的客户端需要访问服务时ClusterIP就不够了。NodePort在ClusterIP的基础上在每个节点上开一个端口默认范围30000-32767外部客户端通过任意节点IP:NodePort就能访问到Service再由Service转发到后端Pod。NodePort适合什么场景我个人的经验是——测试环境、临时演示、或者企业内网没有现成负载均衡器的时候最实用。因为它实现成本极低只要一个Service定义就能搞定。但NodePort有几个明显的短板每增加一个Service就要占用一个节点端口端口数量有限32767-30000只有一个多千个客户端直接访问节点IP如果有多个节点客户端需要自己做负载均衡或者固定访问某个节点单点故障风险高如果节点IP发生变化比如机器迁移、重建外部配置的入口地址就失效了在高并发场景下跨节点转发会引入额外的网络跳数所以生产环境一般不会把NodePort作为长期方案而是配合云厂商的负载均衡器使用。但NodePort本身机制还是要吃透因为很多云厂商的LoadBalancer类型底层也是通过NodePort实现流量接管的。2.3 LoadBalancer云环境下的标准暴露方式LoadBalancer类型是ClusterIP NodePort的扩展它会调用云厂商阿里云、AWS、Azure等的负载均衡服务创建一个公网或内网的LB实例然后把流量转发到各节点的NodePort上。对使用者来说只需要声明type: LoadBalancer云厂商的Controller比如阿里云的CCM、AWS的AWS Load Balancer Controller会自动完成LB的创建和绑定最终Service会拿到一个外部IPEXTERNAL-IP。LoadBalancer解决了NodePort的入口单点问题——客户端只访问LB的IPLB会把流量分发给所有节点再由节点转发到后端Pod。但这里我要提醒一个常见的网络问题如果LB和后端节点不在同一个网络平面或者节点安全组没有放行NodePort段流量会在LB转发到节点这一步被丢弃。很多人排障半天最后发现是安全组规则没加这个坑我在第5章会再展开。2.4 ExternalName用来做服务别名和外网映射ExternalName跟前三种类型有本质区别。它不创建虚拟IP也不做转发而是直接在DNS层面返回一个CNAME记录。也就是说客户端请求Service的DNS名时CoreDNS会直接返回外部域名比如某云数据库的内网域名、某第三方API地址流量直接打到外部完全不经过K8s的流量管道。ExternalName适合什么场景我常用它来做服务对外依赖的本地化抽象。比如业务代码里要调用一个第三方支付接口支付接口的域名可能会变直接在代码里改域名不现实。这时候定义一个ExternalName类型的Service指向当前的支付域名代码里只依赖这个Service的DNS名。后续域名变了只需要修改Service的externalName字段不用重新发版。需要注意ExternalName不支持端口重定向也不支持selector它就是一个纯粹的DNS别名。而且目标域名必须是合法域名不能写IP地址。3. 核心配置细节讲透YAML里的每个字段3.1 selector标签匹配最容易出错的一环Service通过selector选择Pod这个机制看起来简单实际踩坑的人非常多。最常见的问题是selector写的标签Pod上根本没有对应标签。我在维护集群时见过一个典型场景Deployment的template里写了labels: {app: nginx, tier: frontend}但Service的selector只写了{app: nginx}——这不匹配吗其实是匹配的因为selector是部分匹配机制只要Pod上包含Service里写的所有标签键值对就匹配。真正出问题的是键名不一致。比如Deployment里写app: nginx-v1Service的selector写app: nginx或者Service里写了多个标签但其中一个键Pod上根本没有——这两种情况都会导致匹配失败Endpoints列表为空。判断匹配是否成功最快的办法是kubectl get endpoints service-name如果返回的ENDPOINTS列是空的先查selector和Pod标签。用下面命令对比kubectl get pods --show-labels kubectl get svc service-name -o yaml | grep -A3 selector只要标签一致Endpoints里就会自动填充Pod IP列表。这里要额外注意有些Pod虽然有标签但处于Terminating或Pending状态也不会被选入Endpoints。3.2 端口配置的完整写法Service的port配置有三个字段要理清port、targetPort、nodePort。很多新手分不清这三个端口的关系我拆开讲portService对外暴露的端口客户端访问ClusterIP时用的端口targetPort后端Pod上容器的端口也就是应用实际监听的端口nodePortNodePort类型下每个节点上开放的端口外部客户端访问的端口实际配置中targetPort不一定等于port。比如Pod里的nginx监听80端口Service可以定义port: 8080、targetPort: 80这样集群内其他服务访问这个Service的8080端口流量会被转到Pod的80端口。还可以用命名端口named port来规避端口号写死的问题。先在Pod定义里给容器端口起名字ports: - containerPort: 80 name: httpService的targetPort直接引用名字ports: - port: 8080 targetPort: http好处是应用升级后如果端口变了只需要改Pod定义里的containerPortService不用动。在大型微服务架构里这种间接层能减少很多维护成本。多端口Service也是高频需求。给一个Service配置多个端口时必须给每个端口起名字否则kubectl会报错ports: - name: http port: 8080 targetPort: 80 - name: metrics port: 9090 targetPort: 90903.3 externalTrafficPolicy与sessionAffinity的取舍externalTrafficPolicy有两个值Cluster和Local默认是Cluster。在Cluster模式下流量到达任意节点后都可能被转发到其他节点上的Pod。这样做的优点是负载均衡均匀但代价是源IP地址会丢失——节点做了SNAT后端应用看到的是节点IP而不是真实客户端IP。很多应用依赖客户端IP做风控、审计、限流这时候就必须改用Local模式。Local模式下每个节点只会把流量转发到运行在本节点上的Pod不跨节点转发。这样源IP能保留但可能造成负载不均——比如三个节点中只有两个有Pod那两个节点的压力就会偏大没有Pod的节点收到流量后直接丢弃如果是云LB健康检查也会对应调整。另外Local模式下如果Pod被调度走当前节点上的转发规则需要重新同步可能短暂出现不通的情况。sessionAffinity是用来做会话保持的。默认值为None即每个请求独立分发。改成ClientIP后同一个源IP的请求会一直转发到同一个后端Podspec: sessionAffinity: ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 10800会话保持适合有状态应用比如需要本地会话的Web应用。但要注意ClientIP模式下如果后端Pod数量变化哈希结果会重新计算已经建立的会话可能被重新分配。另外它基于源IP做哈希如果多个用户共享同一个出口IP比如公司NAT出口流量会集中到同一个Pod上容易造成热点。4. 实操从YAML编写到联调验证4.1 创建一套Deployment Service完整示例我直接用一次完整的实操来演示从编写YAML到最终验证通过。假设要部署一个nginx服务两个副本。第一步创建Deployment# nginx-deploy.yaml apiVersion: apps/v1 kind: Deployment metadata: name: nginx-web namespace: default spec: replicas: 2 selector: matchLabels: app: nginx-web template: metadata: labels: app: nginx-web spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80 name: http这里关键点是labels.app的键值对必须和Service的selector对应。Pod模板里我写了labels: {app: nginx-web}这个标签会跟随Pod生命周期Service就靠它锁定后端。第二步创建Service# nginx-svc.yaml apiVersion: v1 kind: Service metadata: name: nginx-web-svc namespace: default spec: type: ClusterIP selector: app: nginx-web ports: - name: http port: 8080 targetPort: http注意这里的端口设计Service对外暴露8080后端Pod的nginx实际监听80targetPort引用的是命名端口http。这样Service这一层的稳定性更高即使后端容器端口调整Service YAML基本不用改。第三步应用并验证kubectl apply -f nginx-deploy.yaml kubectl apply -f nginx-svc.yaml查看状态kubectl get pods -l appnginx-web kubectl get svc nginx-web-svc看到Service的CLUSTER-IP被分配后在集群内执行验证curl http://cluster-ip:8080如果能返回nginx的欢迎页说明Service转发链路是通的。但生产环境更推荐用DNS名访问直接kubectl run test-pod --imagebusybox --rm -it -- sh wget -qO- http://nginx-web-svc:80804.2 验证Endpoints和DNS解析是否正常Service的流量转发链路是客户端 → ServiceClusterIP→ EndpointsPod IP列表 → 容器。链路任何一环断了访问都会失败所以验证也要分层做。第一层检查Service本身kubectl get svc nginx-web-svc -o yaml重点看spec.clusterIP和spec.ports确认IP和端口符合预期。第二层检查Endpointskubectl get endpoints nginx-web-svc正常情况ENDPOINTS列应该有两个IP:Port格式类似10.244.1.2:80。如果为空直接跳到第5章的排障部分。第三层验证DNS解析。K8s集群里的Pod解析Service域名依赖CoreDNS可以用busybox验证kubectl run dns-test --imagebusybox --rm -it -- nslookup nginx-web-svc.default.svc.cluster.local如果能解析出ClusterIP说明CoreDNS的配置和Service的注册是正常的。Service的完整DNS名规则是 . .svc.cluster.local同Namespace下可以直接用短名。4.3 kube-proxy的转发模式对访问行为的影响Service的虚拟IP究竟如何生效取决于kube-proxy的运行模式。K8s目前主要有iptables和IPVS两种模式老版本的userspace模式已经基本退出历史舞台了。iptables模式是经典方案。kube-proxy watch到Service和Endpoints的变化后在每个节点上生成对应的iptables规则。数据包到达节点后通过DNAT规则把Virtual IP转换到具体的Pod IP。这种模式的缺点是规则数量跟Service/Pod数量成正比当集群规模到几千个Pod时iptables规则链会非常长性能下降明显。IPVS模式则是把规则写入内核的IPVS虚拟服务器表底层基于哈希查找性能和扩展性都远好于iptables。高版本的kubeadm部署的集群默认就是IPVS模式如果内核模块可用的话。实际运维中我遇到过IPTABLES规则刷新延迟导致的服务短暂不通。典型场景是批量创建大量Service时每个节点上的kube-proxy需要逐一写入iptables规则期间新创建的Service可能延迟几分钟才能访问。IPVS模式在这类场景下明显更稳。如果集群规模较大建议在部署时确认kube-proxy是否启用了IPVS模式kubectl get cm -n kube-system kube-proxy -o yaml | grep mode如果值是ipvs说明当前集群在IPVS模式下工作。5. 常见问题与排查技巧实录5.1 Endpoints为空Service选了但没选上Pod这是我的排障生涯中出现频率最高的问题没有之一。症状很明确Service创建成功ClusterIP正常分配但kubectl get endpoints的结果里没有任何IP。排查顺序我总结成一套固定动作确认Pod确实存在且有Ready状态——如果Deployment副本数起来了但Pod一直Pending或CrashLoopBackOff那endpoints本来就该为空对比Service selector和Pod标签——这是最大概率的根因用kubectl get pods --show-labels看Pod的标签用kubectl get svc -o yaml看selector确认Pod没有设置自定义hostname或者归属于某个StatefulSet的特殊命名规范比如StatefulSet的Pod标签如果没加也会匹配不上确认Service和Pod是否在同一个Namespace——selector是按Namespace隔离的跨Namespace的Pod不会被选中排查是不是有多个Service的selector重叠导致Pod同时被多个Service关联产生了不预期的转发规则第4点是我自己踩过最深的坑。有一次我把Service建在monitoring命名空间但Pod跑在default命名空间标签完全一样也匹配不上。当时查了很久最后用kubectl get svc -n monitoring看一眼才发现命名空间不对。5.2 能ping通ClusterIP但端口不通或超时Service的ClusterIP本质是虚拟IPping它其实是不通的除非内核开了相应的回包配置所以能通的判断标准应该是端口连通性。如果你确认curl或wget没响应按下面顺序排查第一步检查targetPort是否写对了。很多人的应用端口是8080但Pod定义里写的是80Service的targetPort写80流量打过去就是Connection refused。第二步从通知的节点上测试到Pod IP的直连。先kubectl get endpoints找到Pod IP然后在节点上curl : 。如果直连通而走Service不通说明问题出在kube-proxy规则上如果直连就不通则问题在网络插件层面比如Calico的BGP路由没建立。第三步检查kube-proxy是否正常运行。Service流量最终依赖kube-proxy维护的规则如果kube-proxy的Pod异常规则就会过期。kubectl get pods -n kube-system | grep kube-proxy第四步检查节点是否允许iptables转发流量。如果节点的net.ipv4.ip_forward0转发链路的包会被内核丢弃。这个在云主机上默认一般没问题但裸机部署时需要手动确认。5.3 外部访问NodePort不通问题出在安全组NodePort不通的排查思路完全不同因为涉及外部网络链路。外部客户端 → 节点IP:NodePort → 集群内部转发 → Pod。第一堵墙是云厂商安全组或防火墙。开NodePort之后必须在云控制台放行对应的端口段一般30000-32767否则外部流量根本无法到达节点。我见过太多人排了半天集群没问题最后发现是安全组没加。第二堵墙是节点网卡监听。kube-proxy会在节点上监听NodePort但如果有防火墙软件比如firewalld干扰或者kube-proxy没有监听在所有网卡上也会出现外部不通、集群内通的情况。第三堵墙是externalTrafficPolicyLocal模式下节点上没有Pod。Local模式会把流量限制在本节点如果你访问的这个节点上恰好没有对应的Pod副本流量会被直接丢弃。此时用Cluster模式或保证每个节点都有Pod副本都能解决。5.4 会话保持不生效或Connection Reset会话保持不生效多半是externalTrafficPolicy和sessionAffinity配合出了问题。当externalTrafficPolicyCluster时节点会把流量做二次转发即使sessionAffinityClientIP哈希的源IP在中间被SNAT成了节点IP后端看到的源IP全部相同都是节点地址会话自然就粘不到同一个Pod上。解决思路要保源IP就用Local模式。还有一种Connection Reset的情况我遇到过是因为Pod反复重建。当Service的Endpoints列表经常变化比如健康检查fail、Pod OOM重启已有连接会被重置。排查方向看Pod的restart次数和最近的事件确认应用本身是否稳定。6. 进阶特性与扩展应用6.1 Headless Service直连Pod的场景怎么用Headless Service是Service的一类特殊形态——将spec.clusterIP设为None。创建之后不分配ClusterIP不会生成负载均衡规则但DNS解析会做特殊处理每次DNS查询返回的是所有匹配Pod的IP列表。Headless Service最适合StatefulSet。StatefulSet的每个Pod有稳定的网络标识比如mysql-0.mysql-headless.default.svc.cluster.local通过Headless Service客户端可以直接解析到特定Pod的IP实现有状态应用的节点识别和主从切换。Kafka、ZooKeeper、etcd、Elasticsearch这类分布式系统都依赖这种机制。实际使用中要注意Headless Service没有负载均衡功能客户端拿到的是所有Pod IP列表需要自己决定连哪一个。如果你的应用不支持多地址发现就不要用Headless直接用普通ClusterIP更省事。6.2 多环境与多集群场景的Service命名规范建议在一个维护了多个K8s集群和大量服务的环境里Service命名和端口规划不做好后期全是坑。我个人的建议是Service命名遵循业务名-功能名的格式比如trade-svc、payment-api不要用svc1、svc2这种流水账端口规划要有全局意识内部服务统一用80、8080、443这些常用端口暴露给外部的NodePort在健康检查和安全策略上单独管理对外的Service和内部Service分开命名空间比如naming-gateway在外层命名空间业务Service在内层命名空间在Service的metadata里加上描述性label比如team、env、version方便后续用kubectl按label批量操作这些规范看起来不起眼但在遇到几十个Service找不到归属的运维场景时能帮你省下大量时间。7. 关于Service的几条实战心得Service这套抽象说到底是给Pod套了一层稳定的门牌号。我维护生产集群这几年最大的感受是——Service本身出问题的概率其实不高大多数故障都出在配置细节和网络环境这两块。配置细节上selector和targetPort是重灾区每次排查先看这两个字段能省下大半时间。网络环境上无论是裸机部署还是云环境iptables/IPVS规则、安全组、路由表这三层是Service包能不能到达Pod的关键。平时多练几遍从Pod到Service、从节点到Service、从外部到Service的逐层验证方法排障的时候手不慌。如果这篇文章里的某个问题和你的实际场景不完全一致我建议你先抓大放小——确认Pod是Running的、Service有Endpoints、节点上kube-proxy正常。三层链路通了剩下的大多数是细节问题。后面我会接着写Ingress和其他应用管理相关的实操笔记欢迎持续关注。