资讯动态

K8S Service负载均衡实战:从ClusterIP到NodePort原理与排查

发布时间:2026/10/19 14:32:17 来源:尧图企业网站定制
做K8S Service服务暴露与负载均衡实验绕不开一个躲不掉的现实Pod在网络里的身份很“脆弱”。今天换个IP明天滚动更新换一批如果你把Pod的IP直接写死在配置里那运维基本是给自己找罪受。Service这个抽象层就是来解决这件事的——它给你一个不变的访问入口把底下不断变化的Pod集合动态编排起来顺便把流量分发给多个后端副本。这篇文章我会按自己平时实验的完整路径来写先讲清楚为什么需要Service再把ClusterIP、NodePort这些类型的差异和原理拆开然后给你一套本地几分钟就能起来的实验环境最后进入实操从Deployment、Service YAML到负载均衡验证一步一步来。这篇内容不只适合刚学K8S的人也适合那些“照文档配好了但不知道为什么通”的朋友因为我会把很多关键决策背后的原因一并讲透。1. 为什么需要ServicePod“住址”会变访问必须有个稳定入口1.1 Pod IP天生不稳定直接访问Pod并不可靠在Kubernetes里Pod是调度和运行的最小单位但它的网络身份非常“随风飘”。每次新建或重建一个Pod集群IP池都会动态分配一个IP这个IP只在集群网络内有效离开集群就不可达。更麻烦的是Deployment做滚动更新、故障重启、手动扩容缩容时Pod副本会不断更换旧的IP自动回收、新的IP重新分配。如果你在别的服务里写死了某个Pod的IP只要一次重建配置就彻底过期了。这个道理很像你去一栋写字楼找人如果只记住某个员工的工位号他一旦换工位、离职或者临时出差你就找不到了。更合理的做法是记一个前台总机号码由前台帮你转接你不需要关心对方现在坐在哪一层。所以在K8S里直接通过Pod IP访问后端只适合排查问题时临时进入查看不适合作为服务间调用的稳定入口。实验中你会频繁验证这一点比如删除一个Pod再等它重建它的IP一定变了而Service的ClusterIP始终保持不变这就是稳定入口的含义。1.2 Service承担的职责稳定入口、动态后端、负载均衡Service的本质是一个抽象层它不直接跑容器也不占资源只是一组规则加一个虚拟IP。当用户创建一个Service时K8S会分配一个固定的ClusterIP并生成对应的DNS记录。ClusterIP是虚拟的不会绑定到某个具体网卡而是由每个节点上的kube-proxy组件翻译成具体转发规则。Service通过标签选择器selector来自动匹配一组Pod。你定义了selector: appdemo-web那么所有带appdemo-web标签的Pod都会自动进入这个Service的后端名单。这个名单会被记录在Endpoints对象里Pod新增、删除、灰度发布时Endpoints会跟着实时变化Service本身却完全不用改。负载均衡则是由转发机制完成的。请求到达Service后kube-proxy会把流量按规则转发到名单里的某个Pod。默认策略下流量在不同Pod之间大致均匀分布这刚好是我们实验要重点验证的内容一个入口多个后端流量被分摊。2. Service的四种类型怎么选ClusterIP、NodePort、LoadBalancer、ExternalName2.1 ClusterIP集群内部的默认选择K8S里如果创建Service时不写type默认就是ClusterIP。它分配一个集群内部可达的虚拟IP只能从集群内访问外部网络和集群外机器都直连不到。这种类型特别适合微服务之间的内部调用比如订单服务调用用户服务两边都在集群内不需要暴露到外部。实验中最常见的问题是把ClusterIP当作公网IP去访问那肯定不通。它的验证方式是用另一个临时Pod或业务Pod在集群内部去请求这个IP或DNS名字。ClusterIP还有一个特点它是虚拟的你kubectl get svc能看到ClusterIP但到节点上用ip addr看不到这个IP出现在任何网卡上因为它只存在于kube-proxy的转发规则里。很多第一次做实验的人会到处找这个IP到底在哪找不到很正常它本来就不属于任何一张真实网卡。2.2 NodePort把端口开在每个节点上外部访问最直接的入口NodePort是在ClusterIP之上叠加的一层能力。创建Service时指定type: NodePortK8S会从默认的30000-32767端口段里分配一个nodePort并在每个工作节点上都监听这个端口。此时外部请求访问任意节点的这个端口都会被kube-proxy接住再通过Service转发到后端Pod。这里有个容易被忽略的点NodePort模式下ClusterIP并不是被替代而是依然存在、依然有效。两个入口都能访问服务只是适用范围不同ClusterIP只限集群内NodePort则对集群外打开了一个明确的端口。端口的默认范围可以改但一般不建议随便动因为很多云平台安全组规则、防火墙策略都默认按这个段来配置。NodePort适合本地实验、没有负载均衡器的裸机集群以及临时需要把服务暴露出来调试的场景。生产环境用NodePort直接对用户开端口并不是最优解但作为理解K8S服务暴露的基础实验它是必练的一环。2.3 LoadBalancer和ExternalName云上入口与集群外服务映射LoadBalancer一般用在公有云环境。云服务商提供的外部负载均衡器会拿到一个公网IP流量先到公网入口再分发到各节点的NodePort或直接进入集群具体实现因云厂商而异。自建机房或本地实验环境没有云负载均衡能力所以裸金属环境通常配合MetalLB这类方案来补上这个能力。ExternalName则是完全不同的玩法它不创建ClusterIP也没有selector和Endpoints而是把Service名字直接映射成一个外部域名。比如你想让集群内的服务通过某个稳定别名去访问外部系统就可以用ExternalName。从机制上讲这更像DNS层面的别名不是流量转发。另外经常有人把Ingress和Service类型混在一起。Ingress不是Service的第五种类型它是一层七层反向代理入口通常由Ingress Controller比如Nginx Ingress实现对外提供域名、路径、TLS终结等能力再把请求转发到集群内部的Service。所以Ingress是“站在Service前面的入口”和Service本身并不冲突。我这个实验里先把Service基础打扎实之后再玩Ingress会顺畅很多。3. 本机五分钟搭一套K8S实验环境工具选型与验证3.1 选哪种工具minikube更适合做Service实验做Service实验不需要一套生产级高可用集群你需要的是一个能快速启动、能敲kubectl命令、能看kube-proxy和Endpoints变化的环境。常见选择有minikube、kind、k3s、kubeadm。我的建议是本地实验用minikube原因是它开箱即用自带完善的组件管理对Service和NodePort的语义模拟得和正规集群一致而且一条命令就能启动多节点或单节点集群。如果直接用kubeadm搭集群虽然更接近生产但前期的初始化、证书、网络插件配置会消耗大量时间实验重点反而被冲淡。kind虽然启动快但它的节点是容器模拟出来的做NodePort这类网络实验时访问体验和minikube有一点差异。所以这个实验我用minikube来演示。启动集群前确保本机已经装好Docker。启动命令如下minikube start --driverdocker --cpus2 --memory2048如果之前用过其他驱动建议先清理一下再启动minikube delete minikube start --driverdocker等待几分钟看到Done!字样kubectl就能直接连上这个集群了。3.2 验证集群状态节点、核心组件、kube-proxy集群起来后第一件事是确认节点和核心组件都健康。kubectl get nodes kubectl get pods -n kube-systemkubectl get nodes会显示一个Ready状态的节点。kubectl get pods -n kube-system能看到kube-proxy等系统组件是否处于Running状态。kube-proxy是Service转发链路里最关键的组件之一如果它没起来后面做的所有Service实验都会表现成“ClusterIP能创建但始终不通”。验证完基础状态建议再看一眼命名空间。默认情况下我们创建的对象都会进入default命名空间实验时不要搞混。命名空间也经常是Service连不通的隐藏原因之一后面排查部分还会再提到。3.3 准备一个能“报出自己名字”的测试镜像做负载均衡实验最怕的情况是后端是三份完全一样的Nginx请求打到了谁身上完全看不出来。所以我建议换一个能返回自己主机名的容器镜像这样每次请求返回的内容里都带着Pod名谁在处理流量一目了然。我实验里喜欢用hashicorp/http-echo这个轻量镜像它启动后监听一个端口把启动时传入的文本内容作为HTTP响应体。我们传入Pod名响应体里就是“来自哪台Pod”的信息。它的默认端口是5678配置YAML时targetPort要对应写5678。如果你更喜欢用Nginx做后端也没问题但验证负载均衡时得自定义返回页面或者看nginx的访问日志操作起来没那么直观。后面实验我直接用http-echo镜像大家不用另外折腾。4. 完整实验步骤从Deployment到Service再到NodePort暴露4.1 先创建后端工作负载Deployment、标签、探针一个都不能少Service要转发首先得有一组后端Pod。我们创建一个叫demo-web的Deployment副本数设为3每个Pod都带app: demo-web标签。apiVersion: apps/v1 kind: Deployment metadata: name: demo-web spec: replicas: 3 selector: matchLabels: app: demo-web template: metadata: labels: app: demo-web spec: containers: - name: http-echo image: hashicorp/http-echo args: - -textHello from $(POD_NAME) env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name ports: - containerPort: 5678 readinessProbe: httpGet: path: / port: 5678 initialDelaySeconds: 3 periodSeconds: 5保存为deploy.yaml执行kubectl apply -f deploy.yaml kubectl get pods -o wide你看到三个Running状态的Pod并且每个Pod都有自己的IP。注意观察标签等下Service的selector要和app: demo-web保持一致。这里我给容器加了readinessProbe它在Service转发中很重要只有探针成功、Pod处于Ready状态时Pod才会被纳入Endpoints列表。如果探针失败K8S会自动把Pod从Service后端名单里摘掉这在后面的故障排查里你就会明白为什么我会特意加它。4.2 编写Service配置selector、port、targetPort怎么填后端Pod就绪后创建第一个Service用ClusterIP类型。下面是完整的YAMLapiVersion: v1 kind: Service metadata: name: demo-svc spec: type: ClusterIP selector: app: demo-web ports: - port: 80 targetPort: 5678这里三个字段要特别讲清楚selector决定了Service会关联哪些Pod。它必须和后端Pod模板里的标签匹配否则Endpoints会是空的流量无处可转。port是Service对外提供访问的端口客户端访问的是这个端口。targetPort是后端Pod容器实际监听的端口这里填5678对应http-echo的默认监听端口。如果两者相同targetPort甚至可以省略但实验里明确写出来更方便理解。protocol默认是TCP如果没写就使用默认值。实验不需要UDP所以保持默认就好。执行创建kubectl apply -f svc.yaml kubectl get svc demo-svc kubectl get endpoints demo-svc理想状态下kubectl get endpoints会列出三个IP和端口每个IP对应一个后端Pod。这时Service已经具备负载均衡能力了。4.3 在集群内部验证负载均衡多副本轮流响应ClusterIP只能在集群内部访问所以验证时需要起一个临时客户端Pod。我习惯用curlimages/curl这个镜像因为它自带curl退出时自动清理Pod也很干净。kubectl run -it --rm debug --imagecurlimages/curl -- sh进入容器后向Service发起多次请求for i in $(seq 1 10); do curl -s http://demo-svc/; echo; done如果实验顺利你会看到返回内容里的Pod名在三个Pod之间轮换出现。这说明请求到达Service后被分发到了不同的后端Pod负载均衡生效了。也可以用集群内部的DNS名字访问http://demo-svc.default.svc.cluster.local。Service创建后集群DNS会自动生成这条记录集群内任意Pod都能通过服务名直接访问。实验中你可以对比一下直接访问Pod IP和访问Service的区别——Pod IP请求始终返回同一个PodService则会在多个Pod之间切换。退出临时客户端后清理一下exit4.4 把Service升级为NodePort打通外部访问ClusterIP验证通过后接下来做NodePort实验。有两种改法一种是直接kubectl edit svc demo-svc另一种是修改YAML后重新apply。我推荐用修改YAML的方式路径清晰、可回放。apiVersion: v1 kind: Service metadata: name: demo-svc spec: type: NodePort selector: app: demo-web ports: - port: 80 targetPort: 5678 nodePort: 30080这里多了一个nodePort: 30080。如果不指定K8S会自动随机分配一个30000-32767范围内的端口。手动指定便于实验记忆但要注意范围必须落在默认段内否则会被API Server拒绝。重新应用kubectl apply -f svc.yaml kubectl get svc demo-svc这次输出的PORT(S)列会显示80:30080/TCP代表ClusterIP 80端口和节点上的30080端口都可用。外部访问时先拿到minikube节点IPminikube ip然后用curl访问curl http://$(minikube ip):30080/同样能看到后端Pod名在轮换。此时你从集群外部通过节点的IP加端口就成功访问到了集群内部的Service再经由Service转发到Pod。NodePort的关键特征是请求访问的是节点端口但真正处理流量的是后端Pod中间跨了两个转发环节。5. 深入看负载均衡kube-proxy、Endpoints与externalTrafficPolicy5.1 kube-proxy的iptables模式与IPVS模式流量到底怎么分发Service的虚拟IP之所以能访问全靠kube-proxy在每个节点上写入转发规则。kube-proxy最常见的两种工作模式是iptables和IPVS。iptables模式是多数K8S发行版的默认选择。它的做法是当请求访问ClusterIP:Port时命中一条DNAT规则iptables再用statistic模块的random算法从Endpoints列表里挑一个后端做转发。这个模式实现简单、兼容性好但集群规模很大、Service数量很多时iptables规则会变得又长又多更新时有明显的性能开销。IPVS模式则使用了Linux内核自带的IP虚拟服务器能力它把Service的转发规则转换成IPVS虚拟服务器后端Pod作为真实服务器。IPVS天然支持多种调度算法比如轮询rr、最少连接lc性能远好于iptables。如果你有三台以上后端Pod用IPVS的rr算法做负载均衡实验看到的结果会更均匀。minikube可以用如下参数开启IPVS模式minikube start --driverdocker --extra-configkube-proxy.proxy-modeipvs启动后进入节点查看规则minikube ssh sudo ipvsadm -Ln在输出里你能看到K8S的Service对应一条虚拟服务器记录后端是三个真实Pod IP。这个可视化结果对理解负载均衡非常有帮助。如果当前是iptables模式也可以用sudo iptables-save | grep demo-svc来观察规则但可读性差一些。5.2 Endpoints与Readiness探针后端名单的动态维护机制Service之所以能自动适应后端变化是因为K8S内部有一整套联动机制。Service通过selector匹配Pod匹配结果写入Endpoints对象。当Pod被创建、标签变化、删除或者Pod状态不是Ready时Endpoints都会自动更新。Readiness探针在这个机制里的作用很关键。一个Pod如果通过了readinessProbeK8S就认为它“可以接收流量”它才会出现在Endpoints列表里。你可以做个实验来观察kubectl edit deployment demo-web把readinessProbe里的路径改成一个不存在的路径比如/healthz-fail。等待几秒后查看Endpointskubectl get endpoints demo-svc你会发现Endpoints里的IP数量减少了甚至变成空列表。Pod虽然还在Running但已经被从Service后端名单中摘除。这个行为在生产环境特别重要服务发版时新Pod如果还没就绪流量不会打过去崩溃时探针失败后会自动摘除避免请求打到坏Pod上。5.3 externalTrafficPolicyCluster和Local对来源IP的影响Service还有一个参数叫externalTrafficPolicy默认值是Cluster。它控制的是NodePort或LoadBalancer类型的Service收到外部流量后是否可以在节点之间二次跳转。Cluster模式下任意节点的NodePort收到请求都可以把流量转发给集群内任意节点的后端Pod。这样做的好处是负载更均衡坏处是节点转发时会做SNAT客户端源IP会被替换掉后端Pod看到的源地址不是真实客户端。Local模式下节点收到请求后只会转发给本节点上的Pod不跨节点转发。好处是保留了客户端源IP坏处是如果一个节点上没有Pod副本这个节点上的NodePort就无法正常提供服务。生产环境使用Local模式时通常要配合外部负载均衡器的健康检查让没有后端Pod的节点自动被摘掉。我在实验中会做一次对比把externalTrafficPolicy改为Local然后访问没有Pod所在节点的NodePort你会观察到连接失败访问有Pod的节点则正常。这个实验能帮你理解为什么服务暴露不只是一个“配置下就完事”的动作它和节点拓扑、来源IP、负载均衡策略都强相关。6. Service排查实录没有Endpoints怎么办、端口不通从哪查6.1 Service没有Endpoints按这个顺序查准没错这是Service实验里最经典的故障Service创建好了IP也有但访问总是不通。此时第一件事是看Endpointskubectl get endpoints demo-svc如果输出为空说明Service没有匹配到任何Pod。排查顺序我建议固定下来先用kubectl get pods --show-labels查看实际Pod的标签再对照Service里spec.selector的键值。常见错误是selector里写成app: demo但Pod标签是app: demo-web看起来很像实际上完全不匹配。第二个容易踩坑的地方是命名空间。Service只会在同命名空间内做selector匹配如果你把Service建在defaultPod却建在devEndpoints一样是空的。第三种情况是targetPort写错比如容器监听5678Service写成80虽然Endpoints里有IP但请求会连接失败。最后还可以用一条命令快速看Service的事件kubectl describe svc demo-svc这里能显示标签选择器、Endpoints列表以及最近事件基本能定位大多数问题。6.2 NodePort端口连不通范围、防火墙、kube-proxy三件套外部访问NodePort不通时先确认nodePort落没落在30000-32767区间。如果端口写小了比如8080API Server创建Service时就会直接报错。其次检查云安全组或本机防火墙是否放行了对应的端口段本地实验经常是防火墙挡住了外部访问。再看节点上端口是否真的在监听minikube ssh sudo ss -lntp | grep 30080如果监听存在说明kube-proxy规则已经生效如果监听不存在重点检查kube-proxy Pod状态kubectl get pods -n kube-system | grep kube-proxykube-proxy异常时NodePort和ClusterIP都会表现异常这是Service转发链路的主心骨。还有一类隐蔽问题如果Service的externalTrafficPolicy是Local你访问的节点上刚好没有后端Pod连接会被直接丢弃。这个我已经在上面的实验里提到过排查时千万别忽略。6.3 负载结果“偏科”一个Pod扛了大量流量这是什么原因有时候实验会发现10次请求里有7次打在同一个Pod上看起来完全不均衡。这不一定代表配置错误可能是这些原因第一客户端复用HTTP连接。curl客户端的连接池如果复用同一条长连接反复请求时TCP连接本身没断负载均衡规则只在新建连接时生效自然每次都命中同一个后端。用curl -v查看或者每次调用后主动关闭连接就能复现这种差异。第二iptables模式下的随机策略是概率性分布短连接数量少的时候个别Pod被多分配几次很正常。如果拉了100次请求还是明显偏斜才需要考虑后端Pod本身是否节点分布不均、某台节点资源被打满或者某个Pod处于Terminating状态。第三如果开了IPVS模式要看调度算法是否设置为了rr。如果容器重启过、Pod被重新创建IPVS列表更新不及时也会出现短暂偏斜。排查时建议结合以下命令来交叉验证kubectl get endpoints demo-svc kubectl get pods -o wide -l appdemo-web kubectl logs demo-web-xxxx-xxxx看请求日志落在哪些Pod上再比对这些Pod是否都处于Ready状态。6.4 修改Service类型后客户端还在访问旧地址如何确认实验里经常遇到“改了Service类型但好像没生效”的困惑。其实K8S里Service配置变更后转发规则是立即更新的。但客户端侧可能存在DNS缓存或连接复用导致看起来仍然访问的是旧入口。如果你在临时Pod里做验证建议用nslookup demo-svc或dig demo-svc确认解析到的ClusterIP是否和kubectl get svc一致。集群内部的DNS记录由CoreDNS管理默认有一定的缓存时间。如果强依赖最终一致性也可以直接删除旧Service再创建同一名字的ServiceClusterIP会重新分配DNS记录会马上更新。还有一个隐藏细节修改Service的type后ClusterIP本身不会变。这意味着集群内通过ClusterIP访问的客户端不会感知到任何变化只是对外多了一个NodePort入口。如果实验里发现“外部能访问内部反而访问失败”那基本要回到selector和Endpoints层面去查而不是怀疑Service类型改错了。写在最后的一点个人习惯做了这么多次Service相关实验我自己最深的体会是别急着把YAML堆出来就收工。每次创建Service后先花十秒钟看kubectl get endpoints确认后端名单符合预期每改一次类型或探针配置再看一遍Endpoints是否跟着联动变化。K8S的网络抽象虽然看起来就是几个字段但等你逐步验证了kube-proxy转发、IPVS调度、Readiness摘除这些机制之后再去排查生产故障就会明显从容很多。另外还有一个实用小技巧实验里如果不想每次手动进入临时Pod测试可以一条命令解决kubectl run -it --rm --imagecurlimages/curl --restartNever test-$(date %s) -- sh -c for i in $(seq 1 10); do curl -s http://demo-svc/; echo; done这条命令用完即走不会在集群里留一堆测试Pod。以后你做Ingress、Headless Service或者更复杂的流量治理实验时这套验证方法也能直接复用延长不少调试效率。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑