第 5 章:搞懂 K8s Service:从 ClusterIP 到负载均衡
上一章讲了 StatefulSet。本章讲 Service:Pod 的 IP 是临时的,Service 负责为 Pod 提供一个稳定的访问入口。
5.1 为什么需要 Service
Pod 的 IP 不是固定的。重建一次 IP 就变一次,这带来几个问题:
- 前端调用后端时,没法写死 Pod IP
- 后端有多个副本时,需要自己做负载均衡
- Pod 扩缩容或滚动更新时,调用方要重新发现后端
Service 就是来解决这些问题的。它给一组 Pod 提供:
- 稳定的虚拟 IP(ClusterIP)
- 负载均衡
- 基于标签的服务发现
markdown
Service: web (ClusterIP 10.96.12.34)
│
┌─────────┼─────────┐
▼ ▼ ▼
Pod IP 1 Pod IP 2 Pod IP 3
10.244.1.2 10.244.2.5 10.244.1.9
调用方只需要访问 http://web:80,流量会被自动转发到后端的某个 Pod。完整的流量链路如下:

5.2 Service 的四种类型

K8s 内置了四种 Service 类型,覆盖从内网访问到公网暴露的不同场景。
5.2.1 ClusterIP(默认)
只在集群内部暴露一个虚拟 IP,外部无法直接访问。这是最常用、最安全的类型。
yaml
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: ClusterIP
selector:
app: web
ports:
- port: 80
targetPort: 8080
| 字段 | 含义 |
|---|---|
port |
Service 本身暴露的端口 |
targetPort |
后端 Pod 容器里的端口 |
selector |
选择哪些 Pod 作为后端 |
集群内其他 Pod 可以通过 http://web:80 访问,请求会被转发到某个 Pod 的 8080 端口。
5.2.2 NodePort
在集群每个节点上打开一个固定端口,外部可以通过 节点IP:端口 访问。NodePort 范围默认是 30000-32767。
yaml
apiVersion: v1
kind: Service
metadata:
name: web-nodeport
spec:
type: NodePort
selector:
app: web
ports:
- port: 80
targetPort: 8080
nodePort: 30080
在我们的 3 节点集群中,创建后可以通过任意节点访问:
bash
curl http://192.168.0.201:30080
curl http://192.168.0.202:30080
curl http://192.168.0.203:30080
注意:NodePort 只是入门调试时使用,生产环境通常不会直接暴露端口,而是配合 Ingress 使用。
5.2.3 LoadBalancer
由云厂商的负载均衡器(如阿里云 SLB、AWS ELB)分配一个公网 IP,自动把流量转发到 NodePort。K8s 本身不实现 LoadBalancer,需要云环境支持。
yaml
apiVersion: v1
kind: Service
metadata:
name: web-lb
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 80
targetPort: 8080
在裸金属环境或私有云中,通常用 MetalLB 来模拟 LoadBalancer 行为。
5.2.4 ExternalName
把 Service 映射到一个外部域名,而不是 Pod。它不做负载均衡,只是做一个 DNS 别名。
yaml
apiVersion: v1
kind: Service
metadata:
name: db-external
spec:
type: ExternalName
externalName: mysql.corp.example.com
集群内访问 db-external 会解析为 mysql.corp.example.com,常用于临时对接外部数据库。
5.3 Endpoints 和 EndpointSlice
Service 本身并不直接保存 Pod IP 列表。它通过 selector 找到匹配的 Pod,然后由控制器生成 Endpoints 对象。
bash
$ kubectl get endpoints web
NAME ENDPOINTS AGE
web 10.244.1.2:8080,10.244.2.5:8080,10.244.1.9:8080 1h
Endpoints 就是后端真实 Pod 的 IP 列表。当 Pod 变化时,Endpoints 会自动更新。
大规模集群中,一个 Service 可能有成百上千个后端。Endpoints 对象太大,更新效率低。因此 K8s 引入了 EndpointSlice,把后端分成多个切片。
bash
$ kubectl get endpointslices
NAME ADDRESSTYPE PORTS ENDPOINTS
web-abcde IPv4 8080 10.244.1.2,10.244.2.5,10.244.1.9
日常运维中,我们更常看 kubectl get endpoints,但 K8s 内部实际使用 EndpointSlice。
5.4 kube-proxy 与 iptables/ipvs
Service 的虚拟 IP 是怎么工作的?这背后是 kube-proxy 在维护转发规则。
kube-proxy 运行在每个节点上,监听 Service 和 EndpointSlice 变化,然后在本机配置转发规则。它支持三种模式:
| 模式 | 原理 | 适用场景 |
|---|---|---|
| userspace | 早期模式,性能差,已淘汰 | 不推荐 |
| iptables | 默认模式,通过 iptables 规则转发 | 中小规模 |
| ipvs | 内核态负载均衡,性能更好 | 大规模集群 |
iptables 模式下,访问 10.96.12.34:80 会命中一条 DNAT 规则,被随机转发到某个后端 Pod:
bash
$ sudo iptables -t nat -L KUBE-SERVICES -n | grep web
KUBE-SVC-XXXXX tcp -- 0.0.0.0/0 10.96.12.34 tcp dpt:80
ipvs 模式则会在内核里维护一个虚拟服务器列表,转发效率更高,支持的调度算法也更多(rr、wrr、lc 等)。
要查看当前 kube-proxy 模式:
bash
$ kubectl get configmap kube-proxy -n kube-system -o yaml | grep mode
mode: "iptables"
5.5 CoreDNS 与集群内 DNS
K8s 集群内有内置 DNS 服务,默认由 CoreDNS 提供。每个 Service 都会自动生成一个 DNS 记录:
xml
<Service 名>.<namespace>.svc.cluster.local
同一个命名空间内,可以直接用 Service 名访问:
bash
curl http://web
跨命名空间需要加上命名空间:
bash
curl http://web.dev
# 或完整域名
curl http://web.dev.svc.cluster.local
Headless Service(即 clusterIP: None)的 DNS 解析方式不同,它会直接返回后端 Pod 的 IP 列表,而不是返回一个虚拟 IP。
验证 DNS 是否正常工作:
bash
$ kubectl run -it --rm debug --image=busybox:1.28 --restart=Never -- nslookup web
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: web
Address 1: 10.96.12.34
5.6 selector 与 label 不匹配排错
这是 Service 最常见的故障之一。创建了 Service,但访问时返回 Connection refused 或 no endpoints。
常见原因:
- 标签不匹配 :Service 的
selector和 Pod 的labels不一致 - 端口写错 :
targetPort和容器实际监听端口不一致 - Pod 没 Ready:只有 Ready 的 Pod 才会加入 Endpoints
排查步骤:
bash
# 查看 Service 的 selector
$ kubectl get svc web -o yaml | grep selector -A 5
# 查看 Pod 的 labels
$ kubectl get pod -l app=web --show-labels
# 查看 Endpoints 是否为空
$ kubectl get endpoints web
NAME ENDPOINTS AGE
web <none> 5m
如果 Endpoints 是空的,说明 selector 没匹配到 Pod,或者 Pod 都没 Ready。
5.7 实战:创建 Service 并验证
假设我们有一个 Deployment 运行了 3 个 Nginx 副本:
yaml
# web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
给它配一个 ClusterIP Service:
yaml
# web-service.yaml
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web
ports:
- port: 80
targetPort: 80
应用并验证:
bash
$ kubectl apply -f web-deployment.yaml -f web-service.yaml
$ kubectl get svc web
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web ClusterIP 10.96.12.34 <none> 80/TCP 1m
$ kubectl get endpoints web
NAME ENDPOINTS AGE
web 10.244.1.2:80,10.244.2.5:80,10.244.1.9:80 1m
在集群内 curl 测试:
bash
$ kubectl run -it --rm test --image=nginx:alpine --restart=Never -- curl http://web
多次执行,流量会轮询到不同的后端 Pod。
5.8 NodePort 实战与访问
把 Service 类型改成 NodePort,并指定端口 30080:
yaml
apiVersion: v1
kind: Service
metadata:
name: web-nodeport
spec:
type: NodePort
selector:
app: web
ports:
- port: 80
targetPort: 80
nodePort: 30080
创建后,在任意节点都能访问:
bash
$ curl http://192.168.0.201:30080
$ curl http://192.168.0.202:30080
$ curl http://192.168.0.203:30080
注意:如果集群有防火墙或安全组,需要放行 30080 端口。
5.9 本章小结
- Service 为 Pod 提供稳定的访问入口和负载均衡
- 四种类型:ClusterIP、NodePort、LoadBalancer、ExternalName
- Endpoints 和 EndpointSlice 保存后端 Pod 的 IP 列表
- kube-proxy 通过 iptables 或 ipvs 实现转发
- CoreDNS 提供集群内 DNS 解析,Service 名可直接访问
- 排错核心:看
selector是否匹配、targetPort是否对、Endpoints 是否非空
5.10 课后练习
- 创建 3 副本 Nginx Deployment,并给它添加 ClusterIP Service,验证 Endpoints 是否包含 3 个 Pod IP。
- 把 Service 改成 NodePort,指定 30080,然后从集群外通过
192.168.0.201:30080访问。 - 故意把 Service 的
selector写成app: webxxx,观察 Endpoints 是否变空,并理解原因。 - 用
nslookup测试 Service 的 DNS 解析,对比同一命名空间和跨命名空间的域名格式。