Pod IP 信不过 — Service 解决的问题
一句话总结
Pod 随时可能死亡,并以新 IP 重生。Service 会在前方提供不会消失的名称和地址, 后面的 Pod 列表则通过标签 selector 自动更新。存储也采用相同思路—— 声明的不是“哪块磁盘”,而是“需要多少具备何种属性的存储”。
为什么需要它
正如上一模块所确认的,删除 Pod 后会出现一个名称和 IP 都不同的新 Pod。 那么原先连接该 Pod 的一方该怎么办?如果每次都要重新查询列表并建立连接, 所有应用都必须成为 Kubernetes API 客户端。
Service 用间接层解决这个问题。它建立一个稳定的虚拟 IP(ClusterIP)和 DNS 名称, 并通过 selector 持续更新后方的 Pod 列表。客户端只需知道名称。
它如何运作
Kubernetes 网络模型的三个承诺
- 所有 Pod 无需 NAT 即可相互通信
- 节点 agent 可以与该节点上的所有 Pod 通信
- Pod 看到的自身 IP,与其他主体看到的该 Pod IP 相同
CNI 插件只是履行这些承诺的方式不同,承诺本身始终相同。
Service 与 endpoint
创建 Service 后,endpoint 控制器会收集符合 selector 且已就绪(Ready)的 Pod IP, 写入 EndpointSlice(过去是 Endpoints)。这里有两点很重要。
- **只有 Ready Pod 才会进入。**readinessProbe 失败后会退出流量路径,原因就在这里。
- **selector 不匹配时,endpoint 数量会变成 0。**不会产生错误。Service 本身正常,
ClusterIP 也会正常分配,只是连接后哪里都到不了。
检查
kubectl get endpointslice结果是否为空,是诊断此类故障的第一步。
作为参考,从 Kubernetes v1.33 开始,Endpoints API 已 deprecated,官方建议迁移到 EndpointSlice。 EndpointSlice 能在大规模集群中把列表拆成多个分片,也支持 dual-stack。
Service 类型
| 类型 | 暴露范围 | 使用场景 |
|---|---|---|
| ClusterIP(默认) | 集群内部 | 服务间通信 |
| NodePort | 所有节点的 30000-32767 端口 | 开发、简单的外部暴露 |
| LoadBalancer | 外部负载均衡器 | 对接云端/本地 LB |
| ExternalName | DNS CNAME | 为集群外服务赋予名称 |
Headless Service(clusterIP: None)是例外。它不创建 VIP,
而是在 DNS 查询中直接返回 Pod IP。需要像 StatefulSet 那样直接指定单个 Pod 时会使用它。
DNS 名称规则
<서비스>.<네임스페이스>.svc.cluster.local
同一命名空间内只写 <서비스> 即可。仅凭这条规则,“各环境地址不同”的问题便消失了——
只要更换命名空间,同一份 manifest 就能运行。
配置与 secret
ConfigMap 与 Secret 是分离镜像和环境的机制。注入方法有两种: 作为环境变量注入,或者挂载为卷。以卷挂载时,值改变后文件会更新; 环境变量则在进程启动时确定,不会随之更新。
关于 Secret,在 KCNA 层级必须知道一个事实:base64 是编码,不是加密。
默认配置下,Secret 实际上以明文存储在 etcd 中,只要对
kubectl get secret ... -o jsonpath 的结果执行一次 base64 -d 就能得到原文。
真正的加密必须另行启用 EncryptionConfiguration。(详情会在 KCSA 中讨论。)
存储
- PersistentVolume(PV)——实际存储。由管理员或 provisioner 创建。
- PersistentVolumeClaim(PVC)——用户的申请。“需要 1Gi 的 RWO 存储”
- StorageClass——读取申请并自动创建 PV 的规则
Pod 不直接指向 PV,而是指向 PVC。得益于这个间接层, 无论后端是 NFS 还是 block storage,Pod manifest 都不需要改变。
访问模式也是考试常见内容:ReadWriteOnce(单节点读写)、ReadOnlyMany(多节点读取)、
ReadWriteMany(多节点读写)。支持 RWX 的后端有限,因此通常会使用 NFS 等文件存储。
在实际工作中会遇到的情况
作者的家庭实验室完全没有安装 kube-proxy,而是通过 Cilium 的 eBPF 执行 Service 负载均衡。
实际检查也证明它确实工作:节点上的 iptables KUBE- 链为 0,取而代之的是 eBPF map 中直接保存
10.96.0.10:53/TCP → 10.244.0.150:53/TCP、10.96.0.1:443/TCP → 10.0.0.120:6443/TCP
这样的前端到后端映射。
这里会出现一个有趣的 bootstrap 问题。没有 kube-proxy 时,Cilium 自身连接 API server,
不能使用 kubernetes Service 的 ClusterIP(10.96.0.1)——因为创建该 VIP 的主体正是 Cilium 自己。
所以安装时必须用 k8sServiceHost=10.0.0.120、k8sServicePort=6443 直接告知真实地址。
Service 是“间接层”这句话的含义,在此处格外清晰。
同一集群通过 csi-driver-nfs 连接 NAS,以获得 RWX 存储。
L4 load balancer 使用 MetalLB,地址池为 10.0.0.200-215。Gitea 获得 .200、ArgoCD 获得 .201、
Harbor 获得 .202、Grafana 获得 .203。这套配置能直观展示 LoadBalancer 类型 Service
实际执行的动作,就是“从池中取出一个 IP 并附加上去”。
后续实验要做什么
在后续实验中建立 backend Deployment,连接 ClusterIP Service,并确认 endpoint 已被发现。 然后故意创建一个 selector 有拼写错误的 Service,复现 endpoint 数量为 0; 再亲手确认 NodePort、ConfigMap/Secret 注入、PVC 申请和 DNS 名称规则。