前置回顾
第 02 篇:Pod 有自己的 IP,但 Pod 是短暂的--重建后 IP 就变了
第 03 篇:Deployment 管理一组 Pod,但客户端怎么知道这组 Pod 的 IP 分别是什么?更别提 Pod 随时可能被滚动更新替换
本篇的 Service 就是解决"如何用一个稳定入口访问一组随时在变的 Pod"这个问题.
Pod IP 靠不住
先看一个具体场景:前端 Pod 需要访问后端 API(3 个副本),如果让前端直接用 Pod IP 去连,会遇到三个问题.
问题 1:Pod IP 不稳定
Pod 是短命的.滚动更新,扩缩容,节点故障重建--任何一种情况都会导致旧 Pod 被删除,新 Pod 被创建,IP 随之改变:
|
问题 2:没有负载均衡
3 个后端副本,前端只知道其中一个 IP → 所有请求打到同一个 Pod → 单点压力,另外两个副本闲着.水平扩展的意义为零.
问题 3:应用不该关心网络拓扑
每个 Node 分到一段 Pod 子网(如 Node-1 分到 10.244.1.0/24,Node-2 分到 10.244.2.0/24).不同 Node 上的 Pod 之间能通信,是因为集群安装时选择的 CNI 插件(如 Calico/Flannel/Cilium)在底层铺设了跨节点路由(VXLAN 隧道,BGP 路由,或 eBPF 转发).
也就是说:Pod IP 在网络层确实是全集群可达的--CNI 已经解决了"通不通"的问题.但应用代码不应该直接用 Pod IP,因为前两个问题(IP 不稳定,无负载均衡)依然存在.网络连通性是基础设施的事,服务发现和流量分发是更上一层的问题.
CNI是什么
CNI(Container Network Interface)是 K8s 的网络插件标准. 你装集群时必须选一个 CNI 实现(Calico / Flannel / Cilium 等), 它负责:
- 给每个 Pod 分配 IP
- 让不同 Node 上的 Pod 能互相通信 -- 具体手段因插件而异: - Flannel: 用 VXLAN 隧道把包封装后跨节点转发 - Calico: 用 BGP 路由协议让各 Node 知道"10.244.2.x 走 Node-2" - Cilium: 用 eBPF 在内核层做转发
所以技术上, Pod C(10.244.2.3)和 Pod A(10.244.1.5)之间 curl 10.244.1.5 是通的
Service 的核心价值就一句话:给一组 Pod 分配一个稳定的虚拟 IP + DNS 名称,并自动做负载均衡.
Service 的基础模型
一个最基本的 Service:
|
提交后 K8s 做了三件事:
|
selector 必须匹配 Pod 的 labels,不是 Deployment 的
Service 的 selector 直接匹配 Pod 的 metadata.labels.它完全不知道 Deployment 的存在--只要 Pod 带着 app: backend 标签且处于 Ready 状态,就会被纳入这个 Service 的后端池.这意味着你可以用一个 Service 对接来自不同 Deployment 的 Pod(只要标签一致).
port 与 targetPort 的关系
port: 80 是 Service 自己对外暴露的端口(调用方连这个),targetPort: 8080 是流量最终转发到的 Pod 端口.两者可以不同--Service 相当于做了一次端口映射.同一个 Service 的所有后端 Pod 必须在同一端口监听(它们本就是同一 Deployment 的同质副本).
如果一个 Pod 需要暴露多个端口,在 ports 数组中写多条即可:
|
如果担心 Pod 端口未来会变,targetPort 还支持引用 Pod 中定义的端口名称(而非写死数字):
|
四种 Service 类型
Service 通过 spec.type 决定暴露范围.四种类型是逐层递进的包含关系:
|
ClusterIP - 集群内部访问(默认)
不指定 type 时的默认类型.分配一个仅集群内可达的虚拟 IP.
|
|
| 特性 | 说明 |
|---|---|
| 可访问范围 | 仅集群内部(Pod 之间) |
| 分配 IP | 从 Service CIDR 分配虚拟 IP |
| 适用场景 | 内部微服务通信(数据库,缓存,RPC 服务) |
NodePort - 在每个节点开一个固定端口
NodePort 在 ClusterIP 基础上,额外在每个节点 的某个端口上监听.外部流量可以通过 <任意节点IP>:<NodePort> 访问 Service.
|
|
| 特性 | 说明 |
|---|---|
| 可访问范围 | 集群外部(通过节点 IP + NodePort) |
| 端口范围 | 30000-32767(可通过 API Server 参数修改) |
| 适用场景 | 开发测试环境,没有云 LB 的裸机集群,端口已知的内部工具 |
| 局限 | 端口号不友好(30000+),需要暴露节点 IP,没有自动故障摘除节点 |
生产环境慎用 NodePort
NodePort 的问题:客户端需要知道节点 IP(节点挂了怎么办?),端口号不是 80/443(用户体验差),且每个 Service 占一个全局端口(资源紧张).
生产通常用 LoadBalancer 或 Ingress,NodePort 更适合开发调试.
LoadBalancer - 自动创建云负载均衡器
LoadBalancer 在 NodePort 基础上,额外请求云平台(AWS/GCP/Azure/腾讯云)创建一个外部负载均衡器,并分配一个公网 IP.
|
|
|
| 特性 | 说明 |
|---|---|
| 可访问范围 | 公网(通过云 LB 的外部 IP) |
| 前提条件 | 集群运行在支持 LoadBalancer 的云平台,或安装了 MetalLB 等实现 |
| 适用场景 | 需要对外暴露的生产服务 |
| 局限 | 每个 Service 独占一个 LB(成本高);裸机集群无原生支持 |
一个 LB 只服务一个 Service?
是的--LoadBalancer 类型的 Service 每个都会创建一个独立的云 LB 实例.如果你有 20 个微服务,就是 20 个 LB,成本很高.
这正是 Ingress 存在的原因:一个 Ingress Controller(本身是一个 LoadBalancer Service)承载所有七层路由规则,下一篇详解.
ExternalName - DNS 别名
ExternalName 完全不同于前三种--它不代理流量,只做 DNS 层面的 CNAME 重定向.
|
|
| 特性 | 说明 |
|---|---|
| 流量代理 | 不代理,纯 DNS CNAME |
| 适用场景 | 集群内应用访问外部服务(RDS,SaaS API),保留一层抽象方便迁移 |
| 局限 | 不支持端口映射,不经过 kube-proxy,不能用 IP 只能用域名 |
四种类型对比
| 类型 | ClusterIP | 外部 IP | 流量路径 | 典型场景 |
|---|---|---|---|---|
| ClusterIP | 有 | 无 | Pod → ClusterIP → kube-proxy → 后端 Pod | 内部微服务 |
| NodePort | 有 | 节点 IP | 外部 → NodeIP:Port → kube-proxy → Pod | 开发调试 |
| LoadBalancer | 有 | 云 LB IP | 外部 → 云LB → NodePort → kube-proxy → Pod | 生产对外服务 |
| ExternalName | 无 | 无 | DNS CNAME 重定向 | 对接外部服务 |
kube-proxy - Service 背后的流量引擎
Service 本身只是一个 API 对象(存在 etcd 里的一段 YAML).真正让流量"从 ClusterIP 到达 Pod"的是 kube-proxy--它运行在每个节点上,watch Service 和 Endpoints 的变化,然后在节点的网络栈中写转发规则.
三种代理模式
| 模式 | 实现方式 | 状态 | 特点 |
|---|---|---|---|
| userspace | kube-proxy 自己监听端口做转发 | 已废弃 | 流量经过用户态,性能极差 |
| iptables | 写 iptables DNAT 规则 | 默认 | 纯内核转发,性能好;规则多时更新慢 |
| IPVS | 利用 Linux IPVS(IP Virtual Server) | 推荐(大规模) | 哈希表查找 O(1),支持多种负载均衡算法 |
iptables 模式详解
iptables 模式是当前默认.理解它的工作方式,就理解了 Service 流量转发的本质.
|
几个关键事实:
- ClusterIP 是虚拟的:没有任何网卡绑定这个 IP,它只存在于 iptables 规则中.ping ClusterIP 不通(因为没人回复 ICMP),但 TCP 连接能通(被 DNAT 转走了).
- 负载均衡是概率性的:iptables 用
-m statistic --mode random --probability实现,不是轮询(Round Robin).3 个后端的概率分别是 1/3,1/2(剩余的一半),1(最后一个兜底). - 规则数量 = O(Service × Endpoint):每个 Service 的每个后端 Pod 对应一条规则.1000 个 Service,每个 10 个 Pod → 10000+ 条 iptables 规则.更新时需要全量刷新 iptables,大规模集群会出现延迟.
IPVS 模式
IPVS(IP Virtual Server)是 Linux 内核的四层负载均衡模块,专为高性能转发设计.
|
|
什么时候需要从 iptables 切换到 IPVS?
经验法则:Service 数量超过 1000,或 Endpoint 总数超过 5000 时,iptables 的规则刷新延迟会变得明显(秒级).此时切换 IPVS 可以显著改善性能.中小规模集群用 iptables 完全够用.
Endpoints 与 EndpointSlice
Service 怎么知道后端有哪些 Pod?答案是 Endpoints(旧)和 EndpointSlice(新)对象.
Endpoints 对象
创建 Service 后,Endpoints Controller 自动创建一个同名的 Endpoints 对象,列出所有匹配 selector 且处于 Ready 状态的 Pod IP + Port:
|
|
EndpointSlice - 大规模集群的优化
Endpoints 的问题:一个对象存所有后端地址.如果一个 Service 有 5000 个 Pod,这个 Endpoints 对象就有 5000 条记录.每次任何一个 Pod 变动,整个对象都要更新并推送给所有节点的 kube-proxy.
|
|
Endpoints 还是 EndpointSlice?
Kubernetes 1.21+ 默认使用 EndpointSlice.旧的 Endpoints 对象仍然存在(向后兼容),但 kube-proxy 优先读取 EndpointSlice.
你不需要选择用哪个--集群自动管理.只有当你排查 Service 后端不对时,使用 kubectl get endpoints 或 kubectl get endpointslice 确认后端列表.
无 selector 的 Service - 手动管理后端
如果 Service 不写 selector,K8s 不会自动创建 Endpoints.你可以手动创建,让 Service 指向集群外部的 IP(如裸机数据库,遗留系统).
|
|
Headless Service 深入
第 03 篇已介绍 Headless Service 与 StatefulSet 的配合.这里从 Service Discovery 的角度补充更完整的视图.
工作机制
|
|
典型使用场景
| 场景 | 原因 |
|---|---|
| StatefulSet 配套 | 需要稳定的逐 Pod DNS(pod-0.svc),用于主从复制,分片等 |
| 客户端负载均衡 | gRPC 长连接场景--kube-proxy 只在连接建立时选后端,长连接会一直打到同一个 Pod.客户端拿到全部 IP 自己做轮询 |
| 服务网格(Service Mesh) | Envoy sidecar 需要知道所有后端 IP 来做智能路由,不需要 kube-proxy 的简单随机分发 |
gRPC + 普通 Service = 负载均衡失效
gRPC 基于 HTTP/2 长连接.
普通 Service(kube-proxy)只在 TCP 连接建立时做一次负载均衡选择.之后这条连接上的所有 RPC 请求都发给同一个 Pod: 即使后端有 10 个 Pod,流量也可能全压在一个上.
解法:使用 Headless Service + 客户端负载均衡(如 gRPC 内置的 round_robin),或引入服务网格.
客户端负载均衡不是牺牲长连接
常见误解:
- Q: 用 Headless + 客户端 round_robin 是不是把长连接变成了短连接?
- A: 并不是.客户端拿到所有 Pod IP 后,会同时建立 N 条长连接(每个后端一条),每条连接都保持不断开.HTTP/2 多路复用允许一条连接承载多个并发 stream,gRPC 的 round_robin 做的是:每个新 RPC 请求在 N 条持久连接中轮着选一条发出去.
|
本质是"1 条长连接 → N 条长连接 + 请求级分发",TCP 握手复用和 HTTP/2 头部压缩的优势完全保留.
服务发现机制
前面讲的 kube-proxy + iptables 解决的是 报文到了 ClusterIP 之后怎么转发到 Pod .
但还有一个前置问题:应用代码怎么知道该连哪个 ClusterIP?你不可能在代码里硬编码 10.96.0.100, 这个 IP 是 Service 创建时随机分配的,换个集群就变了.
服务发现解决的就是这一步:把 Service 名称翻译为可连接的地址.名称 → IP 的翻译完成后,后续的流量转发才交给 kube-proxy.
|
K8s 提供两种服务发现机制:
DNS 发现(推荐)
集群内的 CoreDNS 为每个 Service 自动注册 DNS 记录:
|
|
DNS 搜索域
Pod 的 /etc/resolv.conf 里配置了一组搜索域(search domain),使得你在代码里只写短名就能解析到完整的集群内 DNS 记录:
|
当你写 backend-svc 时,DNS 解析器会按 search 列表依次拼接尝试:
backend-svc.default.svc.cluster.local→ 命中,返回 ClusterIP- (如果第 1 步没命中才继续)
backend-svc.svc.cluster.local backend-svc.cluster.local
跨 namespace 访问时,短名找不到,需要至少写到 backend-svc.other-ns(会被拼为 backend-svc.other-ns.svc.cluster.local).
环境变量发现(了解即可)
K8s 在 Pod 启动时,把同 namespace 中已存在的 Service 信息注入为环境变量:
|
环境变量发现的局限:
- 顺序依赖:Service 必须在 Pod 之前创建,否则环境变量不会注入
- 不会动态更新:Service 的 ClusterIP 变了(极少发生),已运行的 Pod 看到的还是旧值
- 命名冲突:Service 名中的
-被转为_后可能和其他变量冲突
结论:始终使用 DNS 发现.环境变量只在极少数无法使用 DNS 的遗留场景下使用.
Session Affinity(会话亲和性) - 不推荐用于无状态服务
默认情况下,Service 每次连接随机选后端 Pod.如果需要同一客户端的请求始终打到同一个 Pod(如基于内存的 session),可以配置 Session Affinity:
|
Session Affinity 不是银弹
依赖 Session Affinity 说明应用有状态: 这和 K8s 推崇的无状态设计相矛盾.
更好的做法是把 session 存到 Redis/数据库,让应用真正无状态.
Session Affinity 适用于:遗留应用暂时无法改造,或性能敏感的缓存场景(如本地内存缓存).
Service 问题排查清单
Service 不通是 K8s 最常见的问题之一.按这个顺序排查:
|
|
小结
快速回顾
- Service 的核心价值:给一组 Pod 提供稳定的虚拟 IP + DNS + 负载均衡,解耦调用方与后端 Pod 的生命周期
- ClusterIP:默认类型,仅集群内访问;适合内部微服务通信
- NodePort:在每个节点开端口(30000-32767),适合开发调试;生产不推荐
- LoadBalancer:自动创建云 LB + 公网 IP,生产对外服务首选;每个 Service 占一个 LB(成本高)
- ExternalName:纯 DNS CNAME,不代理流量;用于对接外部服务保留一层抽象
- kube-proxy:iptables(默认,规则链式匹配)vs IPVS(哈希表 O(1),大规模推荐);是 Service 流量转发的实际执行者
- Endpoints/EndpointSlice:记录 Service 当前后端地址列表;EndpointSlice 拆小对象,解决大规模更新放大问题
- Headless Service:clusterIP: None,DNS 直接返回 Pod IP 列表;StatefulSet 配套,gRPC 客户端 LB,Service Mesh 必备
- 服务发现:始终用 DNS(Service 名即域名);环境变量方式有顺序依赖和不更新的问题
动手练习
- 创建一个 Deployment(3 副本,nginx)+ ClusterIP Service.用
kubectl exec进入另一个 Pod,反复curl http://<svc-name>,观察返回的 Pod hostname 是否均匀分布. - 将 Service 改为 NodePort,从宿主机用
curl localhost:<nodePort>验证外部访问是否成功. - 创建一个 Headless Service(clusterIP: None),用
nslookup <svc-name>观察 DNS 返回的是 Pod IP 列表而非 ClusterIP. - 手动将一个 Pod 的 readinessProbe 改为必定失败(如探测一个不存在的端口),观察 Endpoints 列表中该 Pod IP 是否被自动移除.
- 创建一个无 selector 的 Service + 手动 Endpoints,指向集群外部 IP(如你笔记本的 IP),验证集群内 Pod 能否通过 Service 名访问到外部服务.
- 配置
sessionAffinity: ClientIP,反复 curl 同一个 Service,验证请求是否始终落到同一个 Pod.移除配置后再试,观察差异.