
先纠正一下这个输入里的小问题是暴露不是暴漏。经常能看到有人搜k8s 创建service 暴漏集群ip这种关键词其实想做的事情非常明确——把跑在Kubernetes集群里的服务通过Service这个资源暴露出来让集群内或集群外的客户端能稳定访问。这篇就把Service的原理、YAML写法、类型选择和排错思路一次性讲透适合刚把Pod跑起来但还没搞懂流量怎么进去的新手也适合已经用过Service但踩过坑、想补底层逻辑的同学。1. Pod IP 靠不住Service 要解决的三个具体问题很多人第一次部署K8s应用时会陷入一个误区Pod起来了、有IP了直接拿这个IP去访问不就行了在实验环境里确实能通但只要你稍微碰一下真实工作负载就会发现这条路根本走不通。1.1 Pod 地址生命周期极短客户端没法长期持有Pod是K8s里的最小调度单元但同时也是最不稳定的单元。任何一个Pod都可能因为资源不够被驱逐、因为节点要升级被重建、因为探针失败被自动重启甚至只是你手抖删了一下DeploymentPod就没了。每次重建Pod的IP都会重新分配。你可以做一个简单实验创建一个Deployment然后查看后端Pod的IP接着删除其中一个Pod让ReplicaSet重新拉一个新Pod起来再看IP——大概率已经变了。如果你的业务客户端是拿着Pod IP去写配置文件的那么每次Pod变动你都面临配置文件要改一改的风险。这在生产环境里是不可接受的。Service存在的第一个意义就是提供一个不随Pod变化而改变的稳定访问地址。1.2 多副本负载均衡不是天然能力底层网络模型并不提供聚合当你的Replicas设置成3时三个Pod各有各的IP互不相干。客户端要访问这3个Pod里的服务总不能自己在代码里写死3个地址去做轮询吧一个小小的服务扩容到5个副本客户端就要改一次缩容到2个又要改一次。如果服务背后有几十个Pod实例比如某些高并发业务手动维护这份后端地址列表简直是一场灾难。这里要理解一个关键点K8s的Service不是一个运行中的进程它更像一个流量规则。它前面的稳定IPClusterIP 端口是对外提供的一个统一入口它后面的Pod IP列表是动态维护的一份通讯录。客户端永远只需要访问这一入口至于后端实际有几台Pod、分别是什么IP、哪台挂了该剔除都是K8s自己处理的事。1.3 服务发现不能靠人肉改配置集群内需要稳定的访问名一套稍微完整的K8s集群里微服务之间的调用非常频繁。服务A要调服务B如果A的代码里写死了B的一个具体Pod IP一旦B扩容、缩容、重启A就调不通了。更合理的做法是A通过B的Service名字去访问。K8s内置的DNS机制通常是CoreDNS会把Service名字解析成对应的ClusterIP。集群内的访问方式变成了这样配置访问地址http://b-svc.namespace.svc.cluster.local:8080CoreDNS解析出ClusterIP10.96.0.5ClusterIP再通过转发规则把流量送到当前健康的Pod上这样一来后端Pod怎么变都不影响客户端。服务的地址只跟Service绑定跟Pod无关。这也是为什么几乎所有K8s应用的最佳实践里都要求跨服务调用走Service名称而不是走IP。2. Service 背后是谁在干活ClusterIP 的流量路径拆解理解了Service存在的必要性下一步就是搞清楚它到底是怎么工作的。很多人看到ClusterIP这个虚拟IP会很困惑它没有绑定在任何物理网卡上为什么能通流量从客户端出发到底发生了什么2.1 ClusterIP 是从哪来的凭空出现的网络地址当你执行这条命令创建Service时kubectl expose deployment my-app --port80 --target-port8080 --typeClusterIPK8s的API Server会从集群配置的Service CIDR网段里默认常见的是10.96.0.0/12安装时可以自己指定分配一个没有实际设备占用的IP给这个Service比如10.96.0.7。这个IP没有任何Pod持有它也没有网络设备配置它它只存在于K8s的内存数据库etcd里配合节点上的转发规则来生效。这里必须理解ClusterIP本身不处理数据包它只是一个锚点真正让流量流动的是每个节点上的kube-proxy组件。kube-proxy会持续监听API Server里Service和Endpoints的变化一旦发现有新的Service创建就会在当前节点上写入对应的流量转发规则。于是从任何节点、任何Pod访问这个ClusterIP都能命中这些规则被转发到真实的后端Pod。2.2 kube-proxy 的三种工作模式从 iptables 到 IPVSkube-proxy目前主要支持三种模式理解它们之间的差异很重要因为这会直接影响大规模集群的性能和转发方式。userspace模式最早的实现kube-proxy进程本身接收流量再转发性能差现在基本没人用了。知道有这回事即可生产环境不要选。iptables模式默认模式。kube-proxy把Service的转发规则写成一条条iptables链。每来一个数据包就按链的顺序逐条匹配规则匹配到就做DNATDestination Network Address Translation把目标地址从ClusterIP改成后端Pod IP。IPVS模式iptables规则是串行遍历的规则一多匹配开销会明显上升。IPVSIP Virtual Server使用了内核空间的哈希表来查找转发目标时间复杂度更低更适合同一Service后端Pod很多超过几十上百个副本以及集群内Service总量较大的场景。我给个小建议如果只是学习环境或小规模集群几十个Service以内iptables完全够用如果集群比较大、Service数量多可以开启IPVS模式。开启方式很简单在kube-proxy的启动参数里加--proxy-modeipvs如果用的是kubeadm方式部署直接修改kube-proxy的ConfigMap后重启kube-proxy的Pod即可。2.3 selector 与 Endpoints真正决定流量发到哪一步创建Service时我们通常会写一个selector标签选择器比如app: my-app。这个selector看起来简单但它决定了Service的联系名单。K8s的控制器会扫描集群中所有Pod凡是标签符合这个selector的Pod都会被自动加入这个Service的Endpoints列表。随后你查看Service详情时能看到类似这样的信息Endpoints: 10.244.0.5:8080,10.244.1.9:8080,10.244.2.12:8080如果selector没有匹配到任何PodService依然能创建成功但Endpoints是空的ClusterIP也是一个空壳无论你访问多少次都不会通。这个现象是很多新手踩的第一坑明明Service显示创建成功IP也有但就是访问不了——大概率是selector和后端Deployment的label对不上。3. 实操用一条 YAML 把集群内服务暴露出来原理说完了直接进入正题怎么一步步在集群里把服务暴露出来。下面以最典型的方式演示——先有一个Deployment然后创建Service暴露它。3.1 准备一个后端服务Deployment 的 YAML先说一个通用场景我们部署一个简单的Nginx应用用于测试访问效果。它的Deployment长这样apiVersion: apps/v1 kind: Deployment metadata: name: nginx-demo labels: app: nginx-demo spec: replicas: 3 selector: matchLabels: app: nginx-demo template: metadata: labels: app: nginx-demo spec: containers: - name: nginx image: nginx:1.25-alpine ports: - containerPort: 80保存为nginx-deploy.yaml执行kubectl apply -f nginx-deploy.yaml等待Pod Running后确认一下标签kubectl get pods --show-labels注意看输出里每个Pod都带上了appnginx-demo标签这个标签是后面Service匹配的依据。3.2 创建 Service 的两种方式与逐字段拆解创建Service有两种常用方式命令行快速创建和YAML文件创建。生产环境更推荐YAML方式因为可以纳入版本管理清晰可审计。先看最关键的Service YAMLapiVersion: v1 kind: Service metadata: name: nginx-svc namespace: default spec: type: ClusterIP selector: app: nginx-demo ports: - name: http port: 80 targetPort: 80我把每个字段的含义说清楚metadata.nameService的名称它也是集群内DNS名称的一部分。比如这个Service在default命名空间下别的Pod想访问它可以用nginx-svc也可以用全称nginx-svc.default.svc.cluster.local。spec.typeService类型这里指定为ClusterIP表示只在集群内暴露。spec.selector标签选择器精确指定要关联哪些Pod。这个字段是坑最多的建议反复核对Deployment里matchLabels是否与此一致。spec.ports[].portService对外提供访问的端口客户端访问ClusterIP时使用的端口。spec.ports[].targetPort流量到达Pod后转发到Pod里的哪个端口。如果你容器里的服务监听的是8080这里就填8080如果Pod的容器端口和后端端口不一致这两个字段的区别就能立刻体现出来。保存为nginx-svc.yaml并执行kubectl apply -f nginx-svc.yaml如果不想手写YAML也可以直接用命令行快速创建kubectl expose deployment nginx-demo --namenginx-svc --port80 --target-port80expose命令会根据Deployment的标签自动生成selector适合快速验证场景。3.3 验证生效的完整命令清单创建完成后请按下述顺序依次验证确保每一步都正常# 1. 查看Service是否创建记录ClusterIP kubectl get svc nginx-svc # 2. 查看Endpoints是否有真实Pod地址 kubectl get endpoints nginx-svc # 3. 在一个临时Pod内访问Service验证ClusterIP连通性 kubectl run curl-test --imagecurlimages/curl -it --rm --restartNever -- curl http://10.96.0.7/ # 4. 如果想用DNS方式访问推荐 kubectl run curl-test --imagecurlimages/curl -it --rm --restartNever -- curl http://nginx-svc.default.svc.cluster.local/正常情况下第2步输出的Endpoints应该有3个IP对应3个Nginx Pod第3、4步都能正常返回Nginx欢迎页。如果Endpoints为空直接跳到第5章排错部分去检查selector。3.4 访问效果与多份副本的负载均衡验证ClusterIP已经创建好了但我建议再多做一个验证确认流量是不是真的被均匀分发到了多个Pod。在Nginx的默认配置里响应内容不包含Pod名称不方便观察。我们可以给Nginx镜像加一个环境变量或用一个更省事的方式直接进入任意一个Pod查看Nginx访问日志然后多次访问Service看请求是不是分散到了不同的Pod# 在三个Pod里分别执行 kubectl logs -f nginx-demo-xxxx-yyyy然后用循环请求Servicefor i in $(seq 1 20); do curl http://nginx-svc.default.svc.cluster.local/; done如果三个Pod的日志里都有请求记录说明Service的负载均衡生效了。默认情况下iptables模式每次新建连接会随机选取一个后端Pod虽然做不到严格意义的均匀但在大量请求下总体是均衡的。4. 暴露到集群外NodePort 与 LoadBalancer 的选择逻辑ClusterIP只能在集群内部访问外部客户端是够不到10.96.x.x这个网段的。生产环境里我们最终都需要把服务暴露给集群外部这就是NodePort和LoadBalancer要解决的问题。4.1 NodePort 的端口范围与完整访问路径NodePort是ClusterIP的超集简单说就是Service除了分配ClusterIP还会在所有节点上开放一个固定端口默认范围是30000-32767外部客户端可以通过任意节点IP NodePort来访问。它的访问链路如下客户端 - 节点IP:NodePort - kube-proxy规则 - ClusterIP - Pod我把一个NodePort类型的Service配置写出来apiVersion: v1 kind: Service metadata: name: nginx-nodeport spec: type: NodePort selector: app: nginx-demo ports: - port: 80 targetPort: 80 nodePort: 30080执行创建后查看kubectl get svc nginx-nodeport输出里能看到TYPE ClusterIP PORT(S) NODE(S) NodePort 10.96.0.9 80:30080/TCP 现在你可以用集群里任意一台节点Master或Worker的IP加上30080端口在浏览器里打开访问。需要注意的是不是只有K8s节点本身能访问只要网络能连通到节点IP的外部机器都可以访问。关于这个模式有几个经验想分享指定nodePort字段时一定要确保端口在30000-32767区间内同时不能和已有NodePort冲突否则创建会报错。如果你不手写nodePort系统会自动分配一个随机端口。NodePort模式下Service的端口映射变成了80:30080其中80是ClusterIP上的端口30080才是节点上真正对外开放的端口。4.2 LoadBalancer云环境下的更省心方案在公有云比如阿里云、腾讯云、AWS上部署K8s时LoadBalancer类型是更常见的选择。它的原理是当Service声明为LoadBalancer时K8s Cluster API会调用云厂商的负载均衡接口自动创建一个云负载均衡实例并把所有节点的NodePort自动挂到后端。这样一来外部客户端只需要访问云负载均衡的公网IP流量会先到达云负载均衡然后分发到各个节点的NodePort再走kube-proxy转发。整个过程对使用者来说不需要关心具体是哪个节点、哪个NodePort。创建方式apiVersion: v1 kind: Service metadata: name: nginx-lb spec: type: LoadBalancer selector: app: nginx-demo ports: - port: 80 targetPort: 80在云环境执行后等待一段时间一般需要几十秒到几分钟执行kubectl get svc nginx-lb -w当EXTERNAL-IP一列从pending变成具体的公网IP时就可以通过这个IP访问了。私有机房或裸金属环境如果想用LoadBalancer需要额外部署类似MetalLB的负载均衡器方案否则EXTERNAL-IP会一直处于pending状态。4.3 ExternalName 的特殊用途把集群外服务伪装成集群内服务还有一种不常用的例外情况ExternalName类型的Service。它不分配ClusterIP也不创建任何转发规则而是把Service名称直接解析为一个外部的DNS CNAME记录。举个例子你有一个自建或者云厂商提供的MySQL数据库不在K8s集群内但想让集群内的应用统一走Service访问。可以这样定义apiVersion: v1 kind: Service metadata: name: external-mysql spec: type: ExternalName externalName: rds-internal.example.com之后集群内Pod访问external-mysql时DNS解析会直接返回rds-internal.example.com的解析结果。这种方式的优点是不需要维护IP适合对接一些域名相对固定的外部服务缺点是它只能在DNS层做转发不支持端口映射也无法做负载均衡和故障切换。如果外部服务的IP会变化最好还是自己维护一个Endpoints。4.4 一张表看清 Service 四种类型怎么选为了方便对比我把四种类型的适用场景整理如下Service类型是否分配ClusterIP集群外访问方式典型使用场景ClusterIP是不支持仅在集群内通过ClusterIP/DNS访问微服务内部调用、测试环境NodePort是节点IP 固定端口30000-32767小规模测试、自建集群临时暴露服务LoadBalancer是云负载均衡公网IP生产环境对外提供HTTP/HTTPS服务ExternalName否DNS CNAME指向外部域名将外部系统接入集群内统一服务发现选择之前先问自己一个问题客户端在集群内还是集群外如果是集群内ClusterIP就够了如果是集群外再根据自己有没有云厂商环境决定用NodePort还是LoadBalancer。5. 排错实录Service 建好后访问不通怎么办实操过程中几乎每个人都会遇到Service创建成功但访问不通的情况。这里我把最常见的问题按排查链路完整展示一遍这些是我个人在维护集群过程中总结出的高频坑。5.1 访问不通的四种经典症状先判断问题层面我把症状分成四类每一类对应不同的排查方向症状可能的问题层面Service存在但endpoints为空selector没有匹配到Podendpoints正常但集群内curl ClusterIP不通kube-proxy规则未同步、节点网络问题集群内DNS解析不到Service名CoreDNS异常或Service与Pod不在同一命名空间集群内能通集群外NodePort不通安全组/防火墙没放行NodePort先明确卡在哪一层再动手比无头苍蝇一样到处看日志高效得多。5.2 selector 不匹配是最隐蔽的坑也是最高频的错误我在第2章提过selector的问题这里展开讲。为什么说它隐蔽因为创建Service时即使selector写错K8s也不会报任何错误。Service会正常创建ClusterIP会正常分配只是Endpoints列表是空的。我见过一个典型案例Deployment里matchLabels写的app: my-appPod模板的labels也写的app: my-app但Service的selector写成了app: myapp或者app: MyApp。结果就是集群里有3个健康PodService却一个都匹配不到。排查方法很直接# 1. 查看Service的selector需要在kubeconfig里 kubectl get svc nginx-svc -o yaml | grep selector # 2. 查看Pod的标签 kubectl get pods --show-labels # 3. 两端一对比立马就能找到不匹配项还有一个容易忽略的点如果多个Service的selector过于宽泛比如都只匹配app: frontend那你创建的每一个新Service都可能把原来的Pod全部接管过去导致不同Service的Endpoints内容错乱、流量到处乱窜。所以给DevOps团队一个建议Pod标签尽量细化一点至少把app、tier、env这些维度分开用组合标签做选择避免Service之间互相抢Pod。5.3 从 ClusterIP 到后端 Pod 的完整排查链路当Endpoints正常但请求ClusterIP还是不通时就要沿着数据链路往下查了。我会按下面的顺序逐步定位第一步确认kube-proxy组件运行状态kubectl get pods -n kube-system | grep kube-proxy如果kube-proxy的Pod是Running状态但存在大量重启记录建议查看日志kubectl logs -n kube-system kube-proxy-xxxxx经常能看到类似日志node not ready、Cant use iptables internal、conntrack等错误。第二步在节点上直接验证iptables或IPVS规则是否生成如果用的是iptables模式iptables -t nat -S | grep KUBE-SVC一个ClusterIP类型的Service在NAT表中会生成一条KUBE-SVC-XXX规则指向一组流量分发链。集群里Service越多KUBE-SVC开头的规则也越多。如果完全找不到对应Service的规则大概率是kube-proxy没有感知到Service的变更可以先尝试重启kube-proxy或检查API Server连通性。第三步检查节点网络与Pod网络之间的连通性如果Service的规则存在但依然不通需要确认节点到Pod之间的网络是否正常。用一条命令从节点直接访问后端Pod IP注意替换为自己的实际IPcurl 10.244.0.5:80如果这一步不通说明问题在底层容器网络如Calico、Flannel的连通性上和Service本身已经没有关系了。常见的底层网络问题包括节点主机防火墙放行规则不一致、Calico的BGP peer失联、VXLAN隧道异常。第四步查看后端Pod是否健康容器端口是否正确监听有时候流量链路没问题但Pod本身不在工作状态。确认一下kubectl get pods -l appnginx-demo kubectl exec -it nginx-demo-xxxx-yyyy -- curl localhost:80如果容器内curl localhost都不通说明是应用本身的问题进程没起来、端口监听错误那流量无论如何都进不去。5.4 CoreDNS 导致的 Service 名解析异常在集群内应用通常通过Service名访问彼此。如果你遇到用IP能通、用Service名称不通的情况十有八九是CoreDNS出了问题。先看CoreDNS Pod是否存活kubectl get pods -n kube-system | grep coredns再看集群内的DNS配置是否正常进入任意Pod里验证nslookup nginx-svc.default.svc.cluster.local如果解析不到检查Service的name和namespace是否写对再检查CoreDNS日志经常能看到类似SERVFAIL的错误这往往指向了CoreDNS的上游转发配置异常需要查看CoreDNS的ConfigMap中forward配置。5.5 NodePort 不通时的外部因素排查如果你用的是NodePort方式集群内访问ClusterIP通、集群外访问NodePort不通最常见的原因根本不是K8s而是云平台安全组或节点防火墙没有放行NodePort端口范围。很多人会忽略这一步盯着K8s配置查半天其实问题出在基础设施层。建议先在节点本机验证curl 127.0.0.1:30080本机通、外部不通那基本就是网络层面拦住了。把30080或30000-32767加入阿里云/腾讯云安全组、或者节点的iptables INPUT链白名单即可。另一个坑是NodePort在Master节点和Worker节点上的表现没有区别但很多自建集群的Master节点开了防火墙、只允许有限的几个端口入站。如果你的Pod调度到了Worker节点而客户端只访问Master节点的IP一样会不通。稳妥做法是节点前挂一个负载均衡或直接访问所有Worker节点IP。6. 生产环境里关于 Service 的一些额外建议最后按老规矩分享一些我在生产环境踩过坑后总结出来的经验这些内容不一定写进官方文档但真的能帮你省事。6.1 提前规划 Service 命名和端口规范Service名在集群里相当于一个内部域名一旦广泛被其他服务引用改名成本很高。建议在项目启动时就定好命名规范比如Service名统一为项目名-模块名如order-service、user-serviceClusterIP的端口统一走443或80绝大多数微服务框架都能接受targetPort指向容器的真实监听端口尽量不在中间层做二次映射如果你用的是HTTP/GRPC这类协议特别建议直接使用Ingress或者网关来做外部流量分发而不是把所有服务都暴露成NodePort或LoadBalancer。Service内部访问保持ClusterIP外部流量统一从Ingress层进入这样网络策略和安全管控会简单很多。6.2 注意多端口 Service 的命名不可省略如果你的后端Pod同时需要暴露两个端口比如一个HTTP端口8080和一个监控端口9090那么Service的ports数组里每一项必须写name。K8s对多端口服务的端口命名有强制要求不写name会直接创建失败。ports: - name: http port: 80 targetPort: 8080 - name: metrics port: 9090 targetPort: 9090这个name还有一个作用它在Pod的容器环境中会生成类似service-name_PORT_80_TCP这样的环境变量方便一些老式应用读取。6.3 记住 Service 的 sessionAffinity 可以做会话保持如果你的后端应用是有状态的比如WebSocket、临时Session在Service里开启会话保持非常方便spec: sessionAffinity: ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 10800开启后来自同一个客户端IP的请求会被尽量转发到同一个Pod。但注意它只是基于源IP的简单哈希不是精细的会话管理。如果客户端IP经常变化比如移动网络这个方案就不太靠谱需要到应用层实现真正的会话同步。6.4 最后把 Service 与 Pod 的生命周期解耦我的一个深刻体会是Service的YAML一旦稳定就不要跟着应用版本频繁改动。很多团队习惯把Deployment和Service写在一个文件里每次发布都一起更新。结果偶尔字段有点出入selector写错把整个集群流量给打挂了。更稳的做法是把Service的YAML单独保存持续放行后就不再去动它只有新增服务时才创建对应的Service资源。还有一个值得养成的习惯每次创建Service后至少确认三件事——Service状态、Endpoints是否满员、一次实际访问是否通。这三个检查一旦形成肌肉记忆能帮你规避掉90%以上的服务不可用问题。Service这个组件看起来简单但它是K8s流量体系中衔接应用与基础设施的关键一环。理解它背后的实现机制会让你在排查问题时更快定位到问题层面而不是拿着工具乱试。希望这篇把原理、实操和排错串起来的文章能帮你少走一些我当年走过的弯路。