课程目录(第 7 章 / 共 33 章)
课程/最小工作单元

Service 与集群内 DNS

7 章 / 共 33·18 分钟·入门ServiceDNSkubectl

用 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)。你可以直接查看它:

bash
kubectl get endpointslices -n demo

一条链路串起来看:

text
Service(固定 IP + DNS 名,selector: app=web)
   │  自动收集

EndpointSlice(当前就绪的 Pod IP 列表)
   │  转发到

Pod web-xxx / web-yyy

EndpointSlice 里的每个地址都带一个 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 还在:

bash
kubectl get deploy web -n demo

保存下面这份清单为 web-service.yaml

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

三个端口的含义必须分清:

字段含义
portService 自己暴露的端口,调用方连的是它
targetPort转发到 Pod 容器上的哪个端口,必须与 containerPort 一致
nodePort只有 NodePort / LoadBalancer 类型才会用到

应用并验证:

bash
kubectl apply -f web-service.yaml
kubectl get svc -n demo
kubectl get svc web -n demo -o wide
kubectl get endpointslices -n demo

kubectl get svcwebTYPEClusterIPPORT(S)80/TCPCLUSTER-IP 是一个集群内部地址(具体网段取决于你的集群)。kubectl get svc web -o wideSELECTOR 列应显示 app=webkubectl get endpointslicesENDPOINTS 列应列出两个 Pod IP;如果这一列是 <unset>,说明一个 Pod 都没选中,先去查标签。

在集群内验证 DNS 与访问

ClusterIP 只在集群内可达,所以要起一个临时 Pod 进去试。--rm 表示退出后自动删除:

bash
kubectl run tmp --rm -it --image=busybox:1.36 -n demo -- sh

进入容器后依次执行:

bash
nslookup web
wget -qO- http://web
nslookup web.demo.svc.cluster.local
exit

nslookup web 会返回 web 对应的 ClusterIP;wget -qO- http://web 应该打印出 nginx 的欢迎页 HTML。看到这两样,说明 Service 的 DNS 和转发都通了。

Service 的完整 DNS 名规则是:

text
<service>.<namespace>.svc.cluster.local

也就是 web.demo.svc.cluster.local。之所以能只写 web 就解析成功,是因为 Pod 的 /etc/resolv.conf 里带了搜索域:

text
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 并指定一个端口:

yaml
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: TCP
bash
kubectl apply -f web-nodeport.yaml
kubectl get svc web-nodeport -n demo

PORT(S) 会显示 80:30080/TCPnodePort 必须在 30000-32767 范围内(不写则自动分配)。但 kind 集群的节点本身跑在 Docker 容器里,宿主机访问不到容器内的 30080,所以需要在创建集群时做端口映射:

yaml
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 是什么类型都能用:

bash
kubectl port-forward svc/web 8080:80 -n demo

另开一个终端:

bash
curl http://localhost:8080

8080: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 demoENDPOINTS 显示 <unset>kubectl get endpoints web -n demo 显示 none,访问 Service 超时。

原因几乎总是标签对不上:Service 写 app: web,Pod 实际带的是 app: web-v2,或者 Pod 在别的命名空间。

bash
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 又和容器端口不同,同样会连不上。排查:

bash
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 applyprovided port is not in the valid rangenodePort 必须落在 30000-32767
  • 用节点 IP 访问时记得确认节点地址:kubectl get nodes -o wide

Service 不通的排查顺序

访问不通时不要从浏览器或 curl 的报错猜,按这条链路一层层验证,每一步都能独立得出结论:

  1. 先看 EndpointSlicekubectl get endpointslices -n demoENDPOINTS<unset>,问题就在标签或命名空间,跟网络无关,跳到第 4 步。
  2. 再确认端口kubectl get svc web -n demo -o yamlporttargetPort,和 kubectl get deploy web -n demo -o jsonpath='{.spec.template.spec.containers[0].ports}' 对比。targetPort 必须等于容器真正监听的端口。
  3. 进容器验证后端本身kubectl run tmp --rm -it --image=busybox:1.36 -n demo -- sh,然后 wget -qO- http://web。通了说明 Service 与后端都正常,问题在调用方(比如在别的命名空间用了短名)。
  4. 对比标签kubectl get pods -n demo --show-labelskubectl get svc web -n demo -o wideSELECTOR 列,键和值必须完全一致。
  5. 最后看 DNS:容器里 nslookup web 有没有结果。解析失败通常是命名空间写错,或 Pod 的 dnsPolicy / resolv.conf 被改过。
现象原因怎么确认怎么办
ENDPOINTS 显示 <unset>Service 的 selector 没匹配到任何 Podkubectl get pods -n demo --show-labels 对比 svc -o wide改 selector 或改 Pod 标签
有 IP 但访问超时 / connection refusedtargetPort 与容器端口不一致对比 svc -o yaml 与 Deployment 的 portstargetPort 改成容器真实端口
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>没有云控制器或 MetalLBkubectl describe svc <名字> -n demo 看 Events本地实验改用 NodePort / port-forward
只有部分请求失败后端某个 Pod 没就绪或已挂kubectl get endpointslices -n demo -o yaml 看每个地址的 ready修不健康的 Pod,或先摘掉它

动手练习:用 DNS 名访问 web

  1. 创建 web Deployment 与 web Service,确认 kubectl get endpointslices -n demo 里有 2 个 IP。
  2. kubectl run tmp --rm -it --image=busybox:1.36 -n demo -- sh 进入容器,执行 nslookup webwget -qO- http://web
  3. 在容器内执行 nslookup web.demo.svc.cluster.local,确认完整域名也能解析。
  4. 故意把 Service 的 selector 改成 app: web-broken 再 apply,观察 EndpointSlice 变空;然后改回来。
自测:为什么 Service 的 ClusterIP ping 不通?(点击展开答案)

因为 ClusterIP 是一个虚拟地址,它不是任何网卡上真实存在的 IP,也没有进程在它上面监听。kube-proxy 在每台节点上写转发规则(iptables 或 IPVS),只处理符合 Service 端口定义的 TCP/UDP 流量,ICMP 不在其中,所以 ping 不会有回应。但 curlwget 这类走 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

练习

  1. web Service 的 targetPort 改成 8080,用 wget -qO- http://web 观察报错,再改回 80。
  2. 建一个 NodePort 类型的 Service,用 kubectl get svc -n demo 找到端口,再写一份带 extraPortMappings 的 kind 配置,说明为什么直接 curl localhost:端口 会失败。
  3. 想一想:如果后端是 redis 这种有状态服务,为什么有时候会需要 headless Service,而不是 ClusterIP?

现在集群内部能互相访问了,但外部用户还是进不来,而且 Service 只有四层转发能力,无法按域名和路径分流。下一章我们用 第 8 章 Ingress 把流量从集群外引进来。

相关章节:这组 Pod 是怎么来的,见第 6 章 Deployment;标签与 selector 的基础写法见第 5 章 用 YAML 描述 Pod