Deployment 让我们有了一组会自愈、能滚动更新的 Pod。但新问题随之而来:Pod 的 IP 会随着重建而变化,副本还有好几个,客户端到底该连谁?Service 为一组 Pod 提供一个稳定的虚拟 IP 和 DNS 名称,并在它们之间做负载均衡。
学完这一课,你会掌握 ClusterIP、NodePort、LoadBalancer、ExternalName 和 Headless 五种 Service 的用途,理解 EndpointSlice 如何跟踪后端 Pod,会用集群 DNS 名称访问服务,并知道在 kind 里如何测试每一种。
为什么需要 Service
客户端 ──?──▶ Pod 10.244.1.5 (明天可能变成 10.244.2.9)
──?──▶ Pod 10.244.2.3
──?──▶ Pod 10.244.1.8 (滚动更新时被删除)直接连 Pod IP 有三个问题:IP 不固定;需要自己在多个副本间分流;需要知道哪些 Pod 当前是健康的。Service 把这三件事都解决了:
┌──────────────── Service web ────────────────┐
客户端 ──▶ web:80 ──────▶│ ClusterIP 10.96.120.15 (稳定) │
│ selector: app=web │
└──────┬──────────────┬──────────────┬────────┘
▼ ▼ ▼
Pod (Ready) Pod (Ready) Pod (Ready)ClusterIP 是虚拟 IP,没有任何网卡真正拥有它。每个节点上的 kube-proxy 监听 Service 和 EndpointSlice 的变化,把规则写进 iptables/IPVS(或由 Cilium 等 CNI 用 eBPF 实现),把发往 ClusterIP 的流量转发到某个后端 Pod。细节在阶段 3 的网络模型与 CNI里展开。
准备后端应用
我们用 traefik/whoami,它会在响应里打印处理请求的 Pod 名字,方便观察负载均衡:
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami
spec:
replicas: 3
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: traefik/whoami:v1.11
ports:
- name: http
containerPort: 80kubectl apply -f whoami-deploy.yaml
kubectl get pods -l app=whoami -o wideClusterIP:集群内访问
ClusterIP 是默认类型,只能在集群内部访问。
apiVersion: v1
kind: Service
metadata:
name: whoami
spec:
type: ClusterIP
selector:
app: whoami # 流量发给带这个标签且 Ready 的 Pod
ports:
- name: http
port: 80 # Service 自己的端口
targetPort: http # Pod 的端口,可以写数字,也可以引用容器端口名
protocol: TCPkubectl apply -f whoami-svc.yaml
kubectl get svc whoamiNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
whoami ClusterIP 10.96.120.15 <none> 80/TCP 5s起一个临时 Pod 当客户端,连续请求几次:
kubectl run client --rm -it --image=busybox:1.36 --restart=Never -- \
sh -c 'for i in 1 2 3 4 5 6; do wget -qO- http://whoami | grep Hostname; done'Hostname: whoami-7f9c6d8b5-kq2xz
Hostname: whoami-7f9c6d8b5-5tlm8
Hostname: whoami-7f9c6d8b5-kq2xz
Hostname: whoami-7f9c6d8b5-wr9vj
Hostname: whoami-7f9c6d8b5-5tlm8
Hostname: whoami-7f9c6d8b5-wr9vj请求被分散到三个 Pod。
EndpointSlice:Service 背后的地址簿
Service 控制器持续根据选择器找出就绪的 Pod,把它们的 IP 和端口写进 EndpointSlice 对象:
kubectl get endpointslices -l kubernetes.io/service-name=whoamiNAME ADDRESSTYPE PORTS ENDPOINTS AGE
whoami-8hx2c IPv4 80 10.244.1.4,10.244.2.5,10.244.1.5 2m把 Deployment 缩到 1,再看一次,端点会立即减少;未就绪的 Pod(比如就绪探针失败)也会被标记为不可用,不再接收流量。
kubectl scale deploy whoami --replicas=1
kubectl get endpointslices -l kubernetes.io/service-name=whoami
kubectl scale deploy whoami --replicas=3kubectl describe svc whoami 看到的 Endpoints: 一行是排查"Service 不通"的第一站:如果为空,通常是选择器与 Pod 标签不匹配,或者 Pod 没有就绪。
集群 DNS
集群里的 CoreDNS 会为每个 Service 创建 DNS 记录:
| 记录 | 格式 | 示例 |
|---|---|---|
| Service A/AAAA | <svc>.<ns>.svc.cluster.local | whoami.default.svc.cluster.local → ClusterIP |
| 命名端口 SRV | _<port-name>._<protocol>.<svc>.<ns>.svc.cluster.local | _http._tcp.whoami.default.svc.cluster.local |
| Headless 下的 Pod | <pod-hostname>.<svc>.<ns>.svc.cluster.local | 用于 StatefulSet,见下文 |
由于命名空间一课讲过的搜索域,同命名空间用 whoami 即可,跨命名空间用 whoami.default:
kubectl run dns --rm -it --image=busybox:1.36 --restart=Never -- nslookup whoami.defaultServer: 10.96.0.10
Address: 10.96.0.10:53
Name: whoami.default.svc.cluster.local
Address: 10.96.120.15NodePort:从节点端口访问
NodePort 在 ClusterIP 的基础上,在每个节点上打开同一个端口(默认范围 30000–32767),访问 任一节点IP:nodePort 即可到达 Service。
apiVersion: v1
kind: Service
metadata:
name: whoami-np
spec:
type: NodePort
selector:
app: whoami
ports:
- port: 80
targetPort: http
nodePort: 30080 # 不写则自动分配kubectl apply -f whoami-nodeport.yaml
kubectl get svc whoami-npNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
whoami-np NodePort 10.96.44.201 <none> 80:30080/TCP 3s在 kind 里测试
kind 的"节点"其实是 Docker 容器,它们的 IP 在 Docker 网络里。最通用的办法是进入一个节点容器去访问:
docker exec kind-control-plane curl -s localhost:30080 | grep HostnameLinux 主机上还可以直接访问节点容器 IP(kubectl get nodes -o wide 查看 INTERNAL-IP);macOS/Windows 上 Docker 运行在虚拟机里,主机访问不到这些 IP。如果想从主机浏览器直接打开,需要在创建 kind 集群时配置端口映射:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30080 # 节点上的 NodePort
hostPort: 30080 # 映射到主机端口
- role: worker
- role: worker之后就能在主机上 curl localhost:30080。日常调试更简单的方式仍是 kubectl port-forward svc/whoami 8080:80。
LoadBalancer:云上对外暴露
LoadBalancer 在 NodePort 的基础上,请求外部负载均衡器分配一个对外 IP。在云厂商集群里由云控制器(Cloud Controller Manager)创建云 LB;在裸金属集群里需要 MetalLB 之类的组件(阶段 4 生产网络讲)。
apiVersion: v1
kind: Service
metadata:
name: whoami-lb
spec:
type: LoadBalancer
selector:
app: whoami
ports:
- port: 80
targetPort: http在没有 LB 实现的 kind 里,EXTERNAL-IP 会一直是 <pending>:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
whoami-lb LoadBalancer 10.96.87.33 <pending> 80:31547/TCP 10skind 官方提供了 cloud-provider-kind,在主机上运行它后,会为 LoadBalancer Service 分配 IP:
go install sigs.k8s.io/cloud-provider-kind@latest # 也可下载发布的二进制
sudo cloud-provider-kind # 保持运行
kubectl get svc whoami-lb # EXTERNAL-IP 变为一个 172.18.x.x 地址(macOS 上需要按项目 README 的说明额外处理网络访问,以其官方文档为准。)
ExternalName:给外部服务起别名
ExternalName 不选择 Pod,也没有 ClusterIP,只是在 DNS 里返回一条 CNAME 记录:
apiVersion: v1
kind: Service
metadata:
name: database
spec:
type: ExternalName
externalName: db.example.com集群内的应用统一连 database,外部数据库迁移或切换时只需改这一个对象。注意它是纯 DNS 层的转发:不做端口映射,HTTP Host 头和 TLS 证书校验看到的仍是 database,可能导致对方拒绝。
Headless Service:直接拿到 Pod IP
把 clusterIP 设置为 None,就得到一个 Headless Service。它不分配虚拟 IP,不经过 kube-proxy,DNS 查询直接返回所有就绪 Pod 的 IP:
apiVersion: v1
kind: Service
metadata:
name: whoami-headless
spec:
clusterIP: None
selector:
app: whoami
ports:
- port: 80
targetPort: httpkubectl apply -f whoami-headless.yaml
kubectl run dns --rm -it --image=busybox:1.36 --restart=Never -- nslookup whoami-headlessName: whoami-headless.default.svc.cluster.local
Address: 10.244.1.4
Name: whoami-headless.default.svc.cluster.local
Address: 10.244.2.5
Name: whoami-headless.default.svc.cluster.local
Address: 10.244.1.5适用于客户端要自己选择后端的场景:数据库集群的客户端、gRPC 客户端负载均衡,以及 StatefulSet 为每个 Pod 提供 pod-0.svc 这样的稳定域名(见 StatefulSet)。
五种类型对比
| 类型 | ClusterIP | 访问范围 | 典型用途 |
|---|---|---|---|
ClusterIP | 有 | 集群内 | 服务间调用(最常用) |
NodePort | 有 | 节点IP:端口 | 测试、配合外部 LB |
LoadBalancer | 有 | 外部 LB IP | 云上对外暴露 TCP/UDP 服务 |
ExternalName | 无 | DNS CNAME | 引用集群外服务 |
| Headless | 无(None) | DNS 返回 Pod IP | 有状态服务、客户端负载均衡 |
会话保持:sessionAffinity
默认情况下每个连接独立选择后端。如果应用把会话存在本地内存里,可以让同一客户端 IP 的请求总是落到同一个 Pod:
spec:
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 3600 # 默认 10800(3 小时)kubectl patch svc whoami -p '{"spec":{"sessionAffinity":"ClientIP"}}'
kubectl run client --rm -it --image=busybox:1.36 --restart=Never -- \
sh -c 'for i in 1 2 3 4; do wget -qO- http://whoami | grep Hostname; done'这次 4 次请求的 Hostname 全部相同。
清理:
kubectl delete svc whoami whoami-np whoami-lb whoami-headless database --ignore-not-found
kubectl delete deploy whoami动手练习
- 部署
whoami和 ClusterIP Service,用临时 Pod 连续请求 10 次,统计每个 Pod 各处理了多少次。 - 故意把 Service 的 selector 改成
app: whoam(拼错),观察kubectl describe svc中的 Endpoints 和客户端报错,再改回来。 - 在 kind 节点容器里用
curl访问 NodePort Service;如果你使用 Linux,再从主机直接访问节点 IP。 - 创建 Headless Service,对比它和普通 Service 的
nslookup结果。 - 在另一个命名空间里起一个客户端 Pod,分别用
whoami、whoami.default、whoami.default.svc.cluster.local访问,看哪些能通。
自测
port、targetPort、nodePort 分别是什么?
port 是 Service 的 ClusterIP 上监听的端口;targetPort 是转发到 Pod 上的端口(可以是端口名);nodePort 是 NodePort/LoadBalancer 类型在每个节点上开放的端口。
Service 创建了,但访问一直超时,第一步查什么?
查 kubectl describe svc <name> 或 EndpointSlice 是否有端点。没有端点说明选择器和 Pod 标签不匹配、或者 Pod 没有 Ready;有端点再检查 targetPort 是否对应容器真正监听的端口。
为什么 ping 一个 ClusterIP 通常不通,但 curl 可以?
ClusterIP 是虚拟 IP,kube-proxy 只为 Service 定义的协议和端口配置转发规则,没有网卡响应 ICMP。所以 ping 不通不代表 Service 有问题。
Headless Service 与普通 ClusterIP Service 的 DNS 结果有什么不同?
普通 Service 的 DNS 返回一个 ClusterIP,由 kube-proxy 负载均衡;Headless Service(clusterIP: None)的 DNS 直接返回所有就绪 Pod 的 IP,由客户端自行选择。