ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

K8S Service实战指南:从ClusterIP到LoadBalancer的坑与排查

K8S Service实战指南:从ClusterIP到LoadBalancer的坑与排查 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提供一个固定的虚拟IPClusterIP同时在这组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所有对象都是声明式APIService也不例外。一个完整的Service定义包含apiVersion、kind、metadata、spec几个部分最关键的是spec。我把核心字段按重要程度排个序每个都说说它的作用和容易出问题的细节。2.1 apiVersion和kind不能写错Service的apiVersion是v1kind是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: 80802.2 metadata里容易被忽略的注意事项metadata里name是必填的并且在namespace内必须唯一。namespace如果没写默认就是default。这里有个实际开发中常见的坑如果你是用kubectl create -f方式创建YAML里没写namespace对象会进default但你的Deployment可能部署在别的namespaceselector选不到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直接导致选不到任何PodPod的labels是可以在运行中修改的一旦修改导致不再匹配selector它的IP会立刻从Endpoints里被剔除已有连接会断开。我在实际项目里踩过最深的坑是selector写对了Pod labels看起来也对但就是选不上。最后发现原因是Pod所在的namespace和Service不一致Endpoints控制器只在同一个namespace内做匹配。每个namespace是独立的隔离域Service不能跨namespace选Pod。2.4 spec.ports端口映射的完整理解ports字段是数组可以配置多个端口。每个端口有三个核心配置项port、targetPort、protocol。portService对外暴露的端口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: http2.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:80kube-proxy会在选中的Pod里挑一个做转发。注意几个细节ClusterIP有默认的IP范围通常是10.96.0.0/12具体看你的kube-apiserver启动参数。如果你在配置里用了clusterIP: None这会让Service变成headless service不分配ClusterIPDNS直接返回后端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: 30080NodePort是在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.comExternalName是一个轻量级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是是通过云负载均衡器云上生产环境的公网入口依赖云厂商的控制器或MetalLBExternalName否取决于外部目标访问集群外部已有服务只做DNS CNAME无流量转发能力选型思路就一句集群内访问用ClusterIP需要外部访问但没有云资源用NodePort云上有LB需求用LoadBalancer访问外部服务用ExternalName。4. 实操全流程从编写YAML到调试验证理论说完了实际动手的时候你得有一套标准的操作流程。一个Service从无到有到验证通过整个流程包含四步确认后端Pod running、编写Service YAML、创建Service并检查Endpoints、实际验证访问。4.1 确认后端Pod的状态和labelsService不是凭空出现的它的selector必须能匹配到Pod所以第一步是确认Deployment或者Pod已经正常运行并且labels是你预期的那一套。# 查看命名空间下所有Pod及其labels kubectl -n prod get pods --show-labels输出里会显示每Pod的labels比如appmy-app,envprod。检查labels时不要漏看HEAD部分有的Pod创建时label很多你只摘出自己需要的两三对来匹配就行。还有人可能遇到这种情况Pod状态显示Running但通过Service访问时一直超时。排查一圈发现Pod起来了但容器里的进程根本没监听targetPort。Service能配置对Pod能匹配到但转发后没用因为健康检查只到容器层面没到端口层面。做个端口连通性测试很有必要。# 进入集群内一个Pod测试后端端口是否真正可用 kubectl -n prod exec -it any-pod -- curl http://某个Pod的IP:80804.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.yamlapply是声明式的如果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就要检查云厂商控制器是否正常。第二步检查Endpointskubectl -n prod get endpoints app-svcEndpoints列表有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。排查步骤确认Service和Pod在同一namespace。确认selector的labels和Pod的labels完全匹配注意有没有拼写错误、额外的空格、key和value的格式问题。确认Pod确实处于Running状态。容器一直CrashLoopBackOff的话Pod可能被重建IP在变Selector匹配上也没用。检查Pod是否被调度到不可达的节点上。某些情况下节点网络异常Pod能跑但网络不通Endpoints存在但流量到不了。如果以上的没问题看eventkubectl -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端口访问不了。常见原因有三个节点的防火墙/安全组没放行nodePort范围。这是最常见的需要检查云安全组或iptables规则。访问的不是运行中Pod所在的节点但NodePort类型是全节点监听的理论上访问任意节点都通。如果只有特定节点不通检查那个节点的kube-proxy是否正常运行。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未RunningService 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排查的效率能提高一大截。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表