课程目录(第 7 章 / 共 33 章)
Service 与集群内 DNS
用 Service 给一组 Pod 一个稳定的访问入口,掌握 ClusterIP、NodePort、LoadBalancer 的取舍与集群内 DNS 解析。
学完这一章,你将能够
- ✓说清 Service 为什么必要,以及它如何通过 selector 选中 Pod
- ✓会创建 ClusterIP Service,并在集群内用 DNS 名访问它
- ✓区分 ClusterIP、NodePort、LoadBalancer 的适用场景与代价
Pod 的 IP 靠不住
上一章的 Deployment 让 Pod 一直有 2 个副本,但你现在还没法稳定地访问它们,因为 Pod 的 IP 有两个特性:
- 会变:Pod 重建后 IP 一定不同,你不可能把它写进配置文件或代码里。
- 有多个:2 个副本就是 2 个 IP,调用方得自己选一个、自己处理某个副本挂掉的情况。
Service 就是解决这件事的:它给一组 Pod 提供一个固定的虚拟 IP 和一个固定的 DNS 名字,调用方只认这个入口,后面的 Pod 换了几轮都不影响。
Service 不转发流量到「某个 Pod」,而是转发到「一组符合标签的 Pod」——这组 Pod 随时可以变。
Service 如何选中 Pod:selector 与 EndpointSlice
Service 选 Pod 的方式和 Deployment 一样:靠标签,也就是 spec.selector 里写的 app: web。
凡是带 app: web 标签、且与 Service 在同一个命名空间的 Pod,都会被自动加入这个 Service 的后端列表。这个列表由 Kubernetes 自动维护,Pod 一起一灭,列表立刻跟着变。
后端列表本身是一个独立资源,历史上叫 Endpoints,现在叫 EndpointSlice(Endpoints 已被弃用,由 EndpointSlice 取代;kube-proxy 实际消费的是 EndpointSlice)。你可以直接查看它:
kubectl get endpointslices -n demo一条链路串起来看:
Service(固定 IP + DNS 名,selector: app=web)
│ 自动收集
▼
EndpointSlice(当前就绪的 Pod IP 列表)
│ 转发到
▼
Pod web-xxx / web-yyyEndpointSlice 里的每个地址都带一个 ready 条件,kube-proxy 默认只把 ready 的地址作为转发目标。这是滚动更新不中断服务的关键:新 Pod 没通过就绪探针前,不会接到任何流量。
三种类型:ClusterIP、NodePort、LoadBalancer
Service 有三种常用类型,差别只在「谁能访问到它」:
| 类型 | 谁能访问 | 典型场景 | 代价 |
|---|---|---|---|
| ClusterIP | 只能在集群内访问 | 服务间调用,默认类型,最常用 | 集群外访问不到 |
| NodePort | 集群外通过「节点 IP:端口」访问 | 本地实验、临时暴露 | 端口固定且范围受限,调用方要知道节点 IP |
| LoadBalancer | 集群外通过云厂商负载均衡器访问 | 云上生产环境 | 需要云控制器或 MetalLB 之类的实现,通常要花钱 |
入门阶段的建议很简单:默认用 ClusterIP,需要临时从本机看效果就用 `port-forward`。NodePort 主要用于本地集群做验证,LoadBalancer 在 kind 上不会真的分配外部 IP(会一直显示 EXTERNAL-IP <pending>),本地实验不要指望它。
动手:创建 web 的 ClusterIP Service
先确认上一章的 web Deployment 还在:
kubectl get deploy web -n demo保存下面这份清单为 web-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: web
namespace: demo
spec:
type: ClusterIP
selector:
app: web
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP三个端口的含义必须分清:
| 字段 | 含义 |
|---|---|
| port | Service 自己暴露的端口,调用方连的是它 |
| targetPort | 转发到 Pod 容器上的哪个端口,必须与 containerPort 一致 |
| nodePort | 只有 NodePort / LoadBalancer 类型才会用到 |
应用并验证:
kubectl apply -f web-service.yaml
kubectl get svc -n demo
kubectl get svc web -n demo -o wide
kubectl get endpointslices -n demokubectl get svc 里 web 的 TYPE 是 ClusterIP,PORT(S) 是 80/TCP,CLUSTER-IP 是一个集群内部地址(具体网段取决于你的集群)。kubectl get svc web -o wide 的 SELECTOR 列应显示 app=web。kubectl get endpointslices 的 ENDPOINTS 列应列出两个 Pod IP;如果这一列是 <unset>,说明一个 Pod 都没选中,先去查标签。
在集群内验证 DNS 与访问
ClusterIP 只在集群内可达,所以要起一个临时 Pod 进去试。--rm 表示退出后自动删除:
kubectl run tmp --rm -it --image=busybox:1.36 -n demo -- sh进入容器后依次执行:
nslookup web
wget -qO- http://web
nslookup web.demo.svc.cluster.local
exitnslookup web 会返回 web 对应的 ClusterIP;wget -qO- http://web 应该打印出 nginx 的欢迎页 HTML。看到这两样,说明 Service 的 DNS 和转发都通了。
Service 的完整 DNS 名规则是:
<service>.<namespace>.svc.cluster.local也就是 web.demo.svc.cluster.local。之所以能只写 web 就解析成功,是因为 Pod 的 /etc/resolv.conf 里带了搜索域:
search demo.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10- 同一个命名空间内:写
web就够; - 跨命名空间:写
web.demo; - 最保险、最不依赖配置的写法:写完整 FQDN。
跨命名空间访问时,本章的 web 就是 web.demo.svc.cluster.local,第 8 章 Ingress 里配置后端地址时也是这个思路。
NodePort 与 port-forward
想把服务临时暴露到集群外,有两种常见做法。
做法一:NodePort。 在 Service 上把 type 改成 NodePort 并指定一个端口:
apiVersion: v1
kind: Service
metadata:
name: web-nodeport
namespace: demo
spec:
type: NodePort
selector:
app: web
ports:
- name: http
port: 80
targetPort: 80
nodePort: 30080
protocol: TCPkubectl apply -f web-nodeport.yaml
kubectl get svc web-nodeport -n demoPORT(S) 会显示 80:30080/TCP。nodePort 必须在 30000-32767 范围内(不写则自动分配)。但 kind 集群的节点本身跑在 Docker 容器里,宿主机访问不到容器内的 30080,所以需要在创建集群时做端口映射:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30080
hostPort: 30080
protocol: TCP映射之后 curl http://localhost:30080 就能看到 nginx 页面。如果集群已经建好了,重新建集群太麻烦,就直接用下面的做法。
做法二:port-forward。 它通过 API Server 建一条隧道,把本机端口转发到 Service,不管 Service 是什么类型都能用:
kubectl port-forward svc/web 8080:80 -n demo另开一个终端:
curl http://localhost:80808080:80 的含义是「本机 8080 → Service 的 80」。这条命令适合调试,不要当成生产暴露方式:它绑定在你本机、随进程退出而结束,也没有负载均衡与健康检查。
headless Service 是什么
把 spec.clusterIP 设为 None,就是一个 headless Service:它不再分配虚拟 IP,DNS 查询会直接返回所有后端 Pod 的 IP,让客户端自己挑。典型用途是给有状态应用(如 redis、数据库)做点对点连接,或配合 StatefulSet 的稳定网络标识。入门阶段知道「有这种东西、clusterIP: None 就是它」即可。
常见坑与排错
selector 不匹配,EndpointSlice 是空的
现象:kubectl get endpointslices -n demo 的 ENDPOINTS 显示 <unset>,kubectl get endpoints web -n demo 显示 none,访问 Service 超时。
原因几乎总是标签对不上:Service 写 app: web,Pod 实际带的是 app: web-v2,或者 Pod 在别的命名空间。
kubectl get pods -n demo --show-labels
kubectl get svc web -n demo -o wide对比两边的 SELECTOR 与标签即可。改 Service 的 selector 不需要重建 Pod,改完 EndpointSlice 会自动更新。
targetPort 与 containerPort 不一致
现象:EndpointSlice 里明明有 IP,但访问超时或 connection refused。
targetPort 必须指向容器真正监听的端口。nginx 监听 80,你写 targetPort: 8080 就会失败。如果 Service 只有一个端口,targetPort 省略时默认等于 port,这时如果 port 又和容器端口不同,同样会连不上。排查:
kubectl get svc web -n demo -o yaml
kubectl get deploy web -n demo -o jsonpath='{.spec.template.spec.containers[0].ports}'NodePort 在 kind 里访问不到
curl http://localhost:30080直接失败:kind 的节点在 Docker 容器内,必须用extraPortMappings把宿主机端口映射进去,或用port-forward。kubectl apply报provided port is not in the valid range:nodePort必须落在30000-32767。- 用节点 IP 访问时记得确认节点地址:
kubectl get nodes -o wide。
Service 不通的排查顺序
访问不通时不要从浏览器或 curl 的报错猜,按这条链路一层层验证,每一步都能独立得出结论:
- 先看 EndpointSlice:
kubectl get endpointslices -n demo。ENDPOINTS是<unset>,问题就在标签或命名空间,跟网络无关,跳到第 4 步。 - 再确认端口:
kubectl get svc web -n demo -o yaml看port与targetPort,和kubectl get deploy web -n demo -o jsonpath='{.spec.template.spec.containers[0].ports}'对比。targetPort必须等于容器真正监听的端口。 - 进容器验证后端本身:
kubectl run tmp --rm -it --image=busybox:1.36 -n demo -- sh,然后wget -qO- http://web。通了说明 Service 与后端都正常,问题在调用方(比如在别的命名空间用了短名)。 - 对比标签:
kubectl get pods -n demo --show-labels与kubectl get svc web -n demo -o wide的SELECTOR列,键和值必须完全一致。 - 最后看 DNS:容器里
nslookup web有没有结果。解析失败通常是命名空间写错,或 Pod 的dnsPolicy/resolv.conf被改过。
| 现象 | 原因 | 怎么确认 | 怎么办 |
|---|---|---|---|
ENDPOINTS 显示 <unset> | Service 的 selector 没匹配到任何 Pod | kubectl get pods -n demo --show-labels 对比 svc -o wide | 改 selector 或改 Pod 标签 |
有 IP 但访问超时 / connection refused | targetPort 与容器端口不一致 | 对比 svc -o yaml 与 Deployment 的 ports | 把 targetPort 改成容器真实端口 |
nslookup web 解析失败 | 跨命名空间只写了短名,或不在同一个 Namespace | 容器内 nslookup web.demo.svc.cluster.local | 写全 <service>.<namespace> 或 FQDN |
| 同一个 Pod 里能访问,别的 Pod 不行 | 有 NetworkPolicy 限制,或对方在别的命名空间 | kubectl get networkpolicy -n demo | 放开对应入站规则 |
curl localhost:30080 失败(kind) | 节点在 Docker 容器内,端口没映射出来 | kubectl get svc web-nodeport -n demo 确认端口 | 用 extraPortMappings 或改用 port-forward |
EXTERNAL-IP 一直是 <pending> | 没有云控制器或 MetalLB | kubectl describe svc <名字> -n demo 看 Events | 本地实验改用 NodePort / port-forward |
| 只有部分请求失败 | 后端某个 Pod 没就绪或已挂 | kubectl get endpointslices -n demo -o yaml 看每个地址的 ready | 修不健康的 Pod,或先摘掉它 |
动手练习:用 DNS 名访问 web
- 创建
webDeployment 与webService,确认kubectl get endpointslices -n demo里有 2 个 IP。 - 用
kubectl run tmp --rm -it --image=busybox:1.36 -n demo -- sh进入容器,执行nslookup web与wget -qO- http://web。 - 在容器内执行
nslookup web.demo.svc.cluster.local,确认完整域名也能解析。 - 故意把 Service 的 selector 改成
app: web-broken再 apply,观察 EndpointSlice 变空;然后改回来。
自测:为什么 Service 的 ClusterIP ping 不通?(点击展开答案)
因为 ClusterIP 是一个虚拟地址,它不是任何网卡上真实存在的 IP,也没有进程在它上面监听。kube-proxy 在每台节点上写转发规则(iptables 或 IPVS),只处理符合 Service 端口定义的 TCP/UDP 流量,ICMP 不在其中,所以 ping 不会有回应。但 curl、wget 这类走 TCP 的请求会被规则接住并转发到后端的 Pod。所以「ping 不通但能访问」是正常现象,不要把它当成故障。
自测:Pod 重建后 IP 变了,为什么调用方不用改任何配置?(点击展开答案)
因为调用方只认 Service 的 DNS 名和 ClusterIP,而这个入口在 Service 的整个生命周期里都不变。Pod 的变化被 EndpointSlice 吸收了:新 Pod 通过就绪探针就被加进列表,旧 Pod 被删除就自动摘掉,Service 对象本身一个字都没改。这就是「稳定入口 + 动态后端」的解耦——把「地址会变」这件事收进了 Kubernetes 内部,而不是交给每个调用方处理。
自测:selector 改错了,为什么改回来不用重建 Pod?(点击展开答案)
因为 Service 和 Pod 之间只有标签这一层弱耦合:selector 只是查询条件,不代表归属关系,Pod 并不知道自己被谁选中。selector 改回正确值后,控制器立刻重新计算 EndpointSlice,把匹配的 Pod 加回来;Pod 全程一直在跑,不需要重建。反过来说,标签写错时也不会报错——只会得到一个空的 EndpointSlice,这也是它难被发现的原因。
小结
- Pod IP 会变、副本会增减,Service 提供固定的虚拟 IP 与 DNS 名,让调用方只认入口。
- Service 用标签(selector)在同一命名空间内选中 Pod,后端列表由 EndpointSlice 维护,kube-proxy 只转发到 ready 的地址。
- ClusterIP 只能集群内访问;NodePort 通过节点端口暴露;LoadBalancer 依赖云厂商或 MetalLB,本地 kind 上通常不可用。
- Service 的 DNS 名规则是
<service>.<namespace>.svc.cluster.local,同命名空间内可以直接用短名。 port-forward适合调试,不适合生产;nodePort在 kind 里需要extraPortMappings。
练习
- 把
webService 的targetPort改成8080,用wget -qO- http://web观察报错,再改回 80。 - 建一个
NodePort类型的 Service,用kubectl get svc -n demo找到端口,再写一份带extraPortMappings的 kind 配置,说明为什么直接curl localhost:端口会失败。 - 想一想:如果后端是
redis这种有状态服务,为什么有时候会需要 headless Service,而不是 ClusterIP?
现在集群内部能互相访问了,但外部用户还是进不来,而且 Service 只有四层转发能力,无法按域名和路径分流。下一章我们用 第 8 章 Ingress 把流量从集群外引进来。
相关章节:这组 Pod 是怎么来的,见第 6 章 Deployment;标签与 selector 的基础写法见第 5 章 用 YAML 描述 Pod。