第28篇 k8s之Service:为 Pod 提供稳定的访问入口
IT策士 10余年一线大厂经验专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章助你少走弯路。在前几篇中我们通过 Deployment 管理了 Flask 应用的多个副本实现了滚动更新和自愈。但有一个问题一直没解决Pod 的 IP 是临时的。每次 Pod 被重建更新、扩容、节点故障它的 IP 都会变化。如果你用kubectl get pods -o wide观察会发现同一个 Deployment 下的 Pod IP 各不相同且随时可能改变。那么问题来了其他服务怎么找到这些 Pod在前端 Nginx 的配置里写死 IP每次 Pod 重建后手动更新配置显然不现实。Kubernetes 的解决方案是Service——一个为 Pod 提供稳定访问入口的抽象层。今天这篇我们从 Service 的核心原理讲起到三种 Service 类型的对比再到落地我们的贯穿案例把 Pod 之间的通信问题彻底解决。一、为什么需要 Service回顾第 25 篇我们创建了一个 Deployment 管理 3 个 Flask Podkubectl get pods-lappflask-counter-owide输出NAME READY STATUS IP NODE flask-deployment-8f9a0b1c2d3-abcde1/1 Running10.244.1.10 minikube flask-deployment-8f9a0b1c2d3-def341/1 Running10.244.1.11 minikube flask-deployment-8f9a0b1c2d3-ghi561/1 Running10.244.1.12 minikube如果 Redis 需要连接 Flask它该连哪个 IP三个 Pod 都可以处理请求但每个 Pod 的 IP 都不同。一个 Pod 被重建后新 Pod 的 IP 完全不同。你需要的是一组 Pod 的稳定入口和一个自动分发流量的负载均衡器。这正是 Service 的两大核心能力稳定的虚拟 IPClusterIPService 一旦创建它的 ClusterIP 在整个生命周期中保持不变除非手动删除 Service 并重新创建。无论后端 Pod 如何创建、销毁、IP 变更Client 始终通过同一个 ClusterIP 访问服务。自动负载均衡Service 通过标签选择器找到后端的 Pod将请求均匀分发给所有健康的 Pod。回想第 9 篇我们在 Docker 中通过--network-alias和 DNS 轮询实现了类似能力。但 Compose 的 DNS 只能在同一台机器上工作且没有健康检查驱动的流量摘除。K8s 的 Service 将这套机制提升到了集群级别并与 readiness probe 深度集成。二、Service 的工作原理2.1 标签选择器找到 PodService 通过selector字段指定一组标签K8s 自动找到所有匹配这些标签的 Pod将它们作为流量的后端目标。这和你之前学到的 Deployment 用matchLabels找到自己管理的 Pod 是完全一样的机制。Service(selector:appflask-counter)│ ├── Pod A(appflask-counter, IP:10.244.1.10)← 匹配 ├── Pod B(appflask-counter, IP:10.244.1.11)← 匹配 └── Pod C(appflask-counter, IP:10.244.1.12)← 匹配标签选择器是 K8s 中实现松耦合的核心机制。Service 不需要知道 Pod 叫什么名字、IP 是多少只需要知道“我要找带appflask-counter标签的 Pod”。2.2 kube-proxy 与 iptables实现负载均衡Service 的虚拟 IP 并非真实存在的网络接口而是由每个节点上的kube-proxy组件通过 iptables 规则实现的。这一点与第 8 篇学到的 Docker 端口映射本质相同——都是用 iptables 做流量转发。当你访问 Service 的 ClusterIP 时数据包被 iptables 规则捕获随机转发给后端某个 Pod 的 IP。kube-proxy 会持续监控 Service 和 Pod 的变化自动更新 iptables 规则。当一个 Pod 的 readiness probe 失败时kube-proxy 会从 iptables 规则中移除该 Pod 的条目确保流量不再发给不健康的实例。三、三种 Service 类型K8s 提供了三种主要的 Service 类型适应不同的访问场景3.1 ClusterIP默认类型ClusterIP 是默认的 Service 类型也是最常用的类型。它只分配一个集群内部可访问的虚拟 IP外部流量无法直接到达。适用场景集群内微服务之间的调用。例如Flask 应用访问 Redis、后端 API 访问数据库。apiVersion: v1 kind: Service metadata: name: redis-service spec: type: ClusterIP selector: app: redis ports: - port:6379targetPort:6379port: 6379Service 监听的端口其他服务通过服务名:6379访问targetPort: 6379后端 Pod 的容器端口流量最终到达的端口3.2 NodePortNodePort 在 ClusterIP 的基础上额外在每个节点上开放一个固定端口默认范围 30000-32767。外部客户端可以通过任意节点IP:NodePort访问服务。适用场景开发调试、临时暴露服务或没有云负载均衡器的自建集群。apiVersion: v1 kind: Service metadata: name: flask-service-nodeport spec: type: NodePort selector: app: flask-counter ports: - port:5000targetPort:5000nodePort:30080nodePort: 30080在每个节点上开放的端口。如果不指定K8s 会从 30000-32767 范围内自动分配一个3.3 LoadBalancerLoadBalancer 在 NodePort 的基础上自动向云平台申请一个外部负载均衡器如 AWS ELB、GCP Cloud LB并分配一个公网可访问的 IP。在 Minikube 中可以通过minikube tunnel模拟 LoadBalancer 行为。适用场景生产环境中需要对外暴露 HTTP/HTTPS 服务。apiVersion: v1 kind: Service metadata: name: flask-service-lb spec: type: LoadBalancer selector: app: flask-counter ports: - port:80targetPort:5000在 Minikube 环境中运行minikube tunnel后EXTERNAL-IP会从pending变为可访问的 IP 地址。四、实战为贯穿案例创建 Service现在将 Service 落地到我们的 Flask Redis 应用中。以下是完整的 Service 配置为 Redis 和 Flask 分别创建 ClusterIP 和 NodePort 类型的 Service# redis-service.yamlapiVersion: v1 kind: Service metadata: name: redis-service spec: type: ClusterIP selector: app: redis ports: - port:6379targetPort:6379---# flask-service.yamlapiVersion: v1 kind: Service metadata: name: flask-service spec: type: NodePort selector: app: flask-counter ports: - port:5000targetPort:5000nodePort:30080# 部署 Redis如果尚未部署kubectl create deployment redis--imageredis:alpine kubectl expose deployment redis--port6379--target-port6379--nameredis-service# 应用 Flask Servicekubectl apply-fflask-service.yaml4.1 验证 Service输出NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)AGE kubernetes ClusterIP10.96.0.1none443/TCP 1d redis-service ClusterIP10.96.100.50none6379/TCP 1m flask-service NodePort10.96.200.80none5000:30080/TCP 30skubernetes是 K8s API Server 的 Service由集群自动创建redis-service是 ClusterIP 类型只能在集群内部访问flask-service是 NodePort 类型可以通过节点 IP:30080 访问4.2 验证 ClusterIP 的稳定性# 删除所有 Flask Pod让 Deployment 重建它们kubectl delete pods-lappflask-counter# 观察 Pod IP 变化kubectl get pods-lappflask-counter-owide# Pod 重建后 IP 全部变化但 Service ClusterIP 不变# 再次通过 Service 访问kubectlexecdeploy/redis -- redis-cli-hflask-service-p5000# 仍能正常连接4.3 验证负载均衡在 Flask 应用中/health端点返回{status:ok}。我们可以临时修改它让每个 Pod 返回自己的主机名以直观验证负载均衡效果# 进入一个临时 Pod多次访问 flask-servicekubectl run-it--rmdebug--imagealpine --sh# 在容器内执行apkaddcurlwhiletrue;docurl-sflask-service:5000sleep0.5;done你会看到请求被轮询分发到不同的 Pod——这就是 Service 的负载均衡在起作用。4.4 验证 NodePort外部访问在 Minikube 环境中获取节点 IPminikubeip# 192.168.49.2在浏览器中访问http://192.168.49.2:30080或者使用 curlcurlhttp://192.168.49.2:30080# Hello World! I have been seen 42 times.4.5 验证 DNS 解析K8s 内置了 CoreDNS为每个 Service 自动创建 DNS 记录。格式为服务名.命名空间.svc.cluster.local。在默认命名空间下可以直接用服务名访问kubectlexecdeploy/redis -- redis-cli-hflask-service-p5000PING# 如果 Redis CLI 支持 TCP PING或使用其他方式验证kubectlexecdeploy/redis --sh-cnslookup flask-service# Name: flask-service# Address 1: 10.96.200.80 flask-service.default.svc.cluster.local这就是为什么在app.py中我们可以写REDIS_HOSTredis-service——CoreDNS 会将redis-service解析为 Redis Service 的 ClusterIP。五、Service 与 Compose 网络对比这是理解 K8s 网络的关键一步在 Compose 中DNS 轮询是服务发现的主要方式但没有与健康检查联动——即使容器 healthcheck 失败DNS 仍会返回该容器的 IP导致请求被路由到不健康的实例。K8s 的 Service 通过与 readiness probe 深度集成解决了这个问题只有 readiness probe 通过的 Pod 才会被加入 Service 的后端列表确保流量永远不会发送到未就绪的 Pod。六、命令速查表七、本篇总结Service 的本质为动态变化的 Pod 提供稳定的虚拟 IP 和 DNS 名称实现服务发现和负载均衡。三种类型ClusterIP内部通信、NodePort节点端口暴露、LoadBalancer云负载均衡器外部入口。工作原理kube-proxy 通过 iptables/IPVS 规则将 Service ClusterIP 的流量转发到后端 Pod并自动感知 Pod 的增删和健康状态变化。与 Compose 的关键差异Service 是集群级抽象与 readiness probe 联动实现健康检查驱动的流量管理DNS 解析跨节点有效。这篇让你掌握了 K8s 内部的服务发现和负载均衡。但 Service 只能提供四层TCP/UDP的负载均衡——无法基于 HTTP 路径或域名做路由。下一篇——第 29 篇Service 与 Endpoints 深入服务发现原理我们将深入 Endpoints 对象和 CoreDNS 的工作机制彻底理解 K8s 服务发现的底层原理。想了解更多还可以去各个平台搜索「IT策士」一起升级 IT 思维