IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。
在第 28 篇中,我们学会了用 Service 为 Pod 提供稳定的虚拟 IP 和 DNS 名称。Service 让你不再需要关心 Pod IP 的变化------只要记住 redis-service 这个名称,就能访问后端的 Redis 实例。
但这里有一个关键问题我们没有拆解:Service 是怎么知道哪些 Pod 是"自己人"的? 当新 Pod 创建或旧 Pod 被删除时,Service 的后端列表是如何自动更新的?当 Pod 的 readiness probe 失败时,流量又是如何被摘除的?
今天这篇,我们要深入 Service 的底层机制,把 Endpoints、kube-proxy、CoreDNS 这三个服务发现的核心组件彻底搞清楚。理解这些,你才能在 Service "不工作"时快速定位到问题根源。
一、回顾:Service 解决了什么,又留下了什么?
第 28 篇的核心结论:Service 提供了一个稳定的虚拟 IP(ClusterIP),客户端通过这个 IP 或 DNS 名称访问服务,流量被自动分发到后端 Pod。
但 Service YAML 里并没有任何 Pod IP 列表。我们只写了一个 selector,指定了 app: redis 这样的标签。那么问题来了:
-
谁负责找到所有匹配标签的 Pod?
-
Pod 的 IP 列表存储在哪里?
-
Service 的虚拟 IP 怎么把流量转发到真实的 Pod IP?
-
redis-service这个名称是怎么变成 IP 地址的?
这四个问题的答案,分别对应 K8s 服务发现机制的四块拼图:Endpoints 对象、kube-proxy、iptables/IPVS 规则、CoreDNS。
二、Endpoints:Service 的"后端 Pod 清单"
2.1 什么是 Endpoints?
Endpoints 是 K8s 中一个独立的资源对象,由 Endpoints Controller (Controller Manager 的一部分)自动创建和维护。它的作用很简单:存储与 Service 的 selector 匹配的所有 Pod 的 IP 和端口。
每创建一个 Service,K8s 就会自动创建一个同名的 Endpoints 对象。你可以把它想象成 Service 的"影子"------Service 是前台接待,Endpoints 是后台维护的真实服务器列表。
2.2 动手观察 Endpoints
在第 28 篇中我们创建了 redis-service 和 flask-service。现在来看看它们背后的 Endpoints:
输出:
bash
NAME ENDPOINTS AGE
redis-service 10.244.1.5:6379 10m
flask-service 10.244.1.10:5000,10.244.1.11:5000,10.244.1.12:5000 10m
ENDPOINTS 列展示了所有匹配标签的健康 Pod 的 IP 和端口。redis-service 目前只有 1 个 Pod(单副本),flask-service 有 3 个 Pod(对应 Deployment 的 3 个副本)。
查看更详细的信息:
bash
kubectl describe endpoints redis-service
输出:
bash
Name: redis-service
Namespace: default
Labels: app=redis
Subsets:
Addresses: 10.244.1.5
NotReadyAddresses: <none>
Ports:
Name Port Protocol
---- ---- --------
<unset> 6379 TCP
Events: <none>
关键字段解读:
-
Addresses:当前健康且就绪的 Pod IP 列表。只有 readiness probe 通过的 Pod 才会出现在这里。
-
NotReadyAddresses:匹配了标签但 readiness probe 尚未通过的 Pod。这些 Pod 不会接收 Service 转发的流量。
-
Subsets:一组 IP:Port 的集合。一个 Service 可以有多个端口,每个端口对应一个 Subset。
2.3 Endpoints 的动态更新
现在来验证 Endpoints 的自动更新能力。在一个终端持续观察 Endpoints:
bash
kubectl get endpoints flask-service -w
在另一个终端删除一个 Flask Pod:
bash
kubectl delete pod flask-deployment-8f9a0b1c2d3-abcde
观察 -w 的输出:被删除 Pod 的 IP 会从 ENDPOINTS 列表中消失,而 Deployment 重建的新 Pod IP 在 readiness probe 通过后会被自动加入。
这个动态过程揭示了 Service 服务发现的完整闭环:Pod 创建 → 标签匹配 → readiness probe 通过 → Endpoints Controller 更新 Endpoints → kube-proxy 更新 iptables 规则 → 流量开始分发到新 Pod。反过来,Pod 的 readiness probe 失败 → 从 Addresses 移到 NotReadyAddresses → 流量摘除。
对比 Docker Compose:Compose 的 DNS 轮询不会检查容器是否真正健康------即使容器 healthcheck 失败,DNS 仍会返回其 IP,导致请求被路由到不可用的实例。K8s 通过 readiness probe → Endpoints 的联动机制,从根本上解决了这个问题。
三、kube-proxy:将虚拟 IP 翻译成真实 IP
3.1 kube-proxy 的三种模式
第 28 篇讲过,Service 的 ClusterIP 是虚拟的------没有任何网络接口真正持有这个 IP。流量之所以能到达后端 Pod,靠的是每个节点上运行的 kube-proxy 组件。
kube-proxy 监听 API Server 中 Service 和 Endpoints 的变化,在节点上创建网络规则,将对 ClusterIP 的访问重定向到后端 Pod 的真实 IP。它支持三种工作模式:
iptables 模式与第 8 篇学到的 Docker 端口映射原理完全一致------都是通过 iptables 的 DNAT 规则修改数据包的目标地址。
3.2 iptables 规则链示意
当你访问 redis-service 的 ClusterIP 10.96.100.50:6379 时,数据包经过的 iptables 规则链大致如下:
bash
客户端 Pod → PREROUTING → KUBE-SERVICES → KUBE-SVC-REDIS
│
└── 随机选择一条后端规则:
├── KUBE-SEP-POD1 → DNAT → 10.244.1.5:6379
├── KUBE-SEP-POD2 → DNAT → 10.244.1.6:6379
└── KUBE-SEP-POD3 → DNAT → 10.244.1.7:6379
每条 KUBE-SEP-* 规则对应一个 Endpoint(即一个 Pod IP)。kube-proxy 确保这些规则与 Endpoints 对象始终保持同步。
3.3 IPVS 模式的优势
iptables 是顺序匹配的,当 Service 和 Pod 数量达到数千个时,规则数量呈指数增长,新增连接延迟会明显升高。IPVS(IP Virtual Server)是 Linux 内核专门为负载均衡设计的模块,使用哈希表查找后端,性能几乎不受规则数量影响。生产环境中如果集群规模超过 1000 个 Service,建议启用 IPVS 模式。
四、CoreDNS:将服务名翻译成 IP
4.1 CoreDNS 在 K8s 中的角色
Endpoints 解决了"Service 的后端有哪些 Pod",kube-proxy 解决了"虚拟 IP 怎么转发到 Pod IP"。但还缺一环:客户端怎么从服务名找到 ClusterIP?
答案是 CoreDNS 。CoreDNS 是 K8s 集群的内置 DNS 服务器(从 v1.13 起取代了 kube-dns),运行在 kube-system 命名空间中。每个 Service 创建时,CoreDNS 自动为其添加一条 A 记录:
bash
<服务名>.<命名空间>.svc.cluster.local → <ClusterIP>
4.2 动手验证 DNS 解析
bash
# 查看 CoreDNS Pod
kubectl get pods -n kube-system -l k8s-app=kube-dns
# NAME READY STATUS RESTARTS AGE
# coredns-7c8b6f9d5f-abcde 1/1 Running 0 1d
# 从任意 Pod 验证 DNS 解析
kubectl run -it --rm debug --image=alpine -- sh
# 在容器内执行:
nslookup redis-service
输出:
bash
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: redis-service.default.svc.cluster.local
Address: 10.96.100.50
-
10.96.0.10是 CoreDNS 的 ClusterIP(kube-dnsService) -
redis-service.default.svc.cluster.local是完整的 DNS 名称(<服务名>.<命名空间>.svc.cluster.local) -
10.96.100.50是redis-service的 ClusterIP
你也可以直接使用短名称 redis-service,因为 Pod 的 /etc/resolv.conf 中配置了搜索域(default.svc.cluster.local),会自动补齐命名空间和集群域名。
4.3 CoreDNS 与 Docker DNS 的对比
回想第 9 篇学过的 Docker 内嵌 DNS(127.0.0.11),CoreDNS 的设计思想与之完全一致------都是通过 DNS 服务器实现服务名到 IP 的解析。关键区别在于规模:Docker DNS 只服务于单台宿主机上的容器,而 CoreDNS 服务于整个集群中所有 Pod 和 Service。在 Docker 中,DNS 记录的生命周期与容器绑定;在 K8s 中,DNS 记录的生命周期与 Service 对象绑定------即使后端 Pod 全部重建,DNS 解析结果(ClusterIP)也保持不变。
4.4 常见 DNS 故障排查
问题 1:nslookup 返回 server can't find
排查步骤:
bash
# 检查 Service 是否存在
kubectl get svc <服务名>
# 检查 CoreDNS Pod 是否正常运行
kubectl get pods -n kube-system -l k8s-app=kube-dns
# 检查 Pod 的 DNS 配置
kubectl exec <Pod名> -- cat /etc/resolv.conf
如果 CoreDNS Pod 全部 CrashLoopBackOff,整个集群的服务发现将瘫痪------这也是为什么生产环境通常部署至少 2 个 CoreDNS 副本并配置反亲和性(避免所有副本同时故障)。
问题 2:DNS 解析正确但连接超时
这说明 CoreDNS 工作正常,问题出在网络层。检查 NetworkPolicy 是否阻止了流量,或 Pod 和 Service 是否在同一个命名空间(跨命名空间访问必须用完整域名)。
五、Headless Service:直接返回 Pod IP
5.1 什么是 Headless Service?
ClusterIP 类型的 Service 返回的是虚拟 IP,客户端不感知后端 Pod。但在某些场景下(如数据库主从复制、StatefulSet 中的 Pod 需要直接通信),客户端需要绕过 ClusterIP,直接获取所有 Pod 的 IP 列表。
Headless Service 就是为这种需求设计的------将 clusterIP 设为 None,DNS 查询不再返回 ClusterIP,而是直接返回所有健康 Pod 的 IP。
5.2 动手创建 Headless Service
bash
apiVersion: v1
kind: Service
metadata:
name: redis-headless
spec:
clusterIP: None
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
bash
kubectl apply -f redis-headless.yaml
kubectl get svc redis-headless
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# redis-headless ClusterIP None <none> 6379/TCP 10s
CLUSTER-IP=None 表示这是一个 Headless Service。
5.3 验证 DNS 行为
bash
kubectl run -it --rm debug --image=alpine -- sh
nslookup redis-headless
输出会包含所有匹配 app: redis 标签的健康 Pod 的 IP 地址,而不是一个虚拟 ClusterIP。这与普通 Service 的 DNS 行为截然不同------Headless Service 的 DNS 查询直接返回 Pod IP A 记录,客户端需要自己实现负载均衡和故障转移逻辑。
5.4 Headless Service 的应用场景
-
StatefulSet :每个 Pod 有稳定的网络标识(如
redis-0.redis-headless.default.svc.cluster.local),Headless Service 为每个 Pod 提供独立的 DNS 记录 -
数据库集群:客户端需要连接特定实例(如 MySQL 主库),不能依赖随机负载均衡
-
自定义服务发现:应用需要获取所有后端实例列表,实现客户端侧负载均衡
六、服务发现的全链路拼图
现在把四块拼图组装在一起,完成一次完整的服务发现流程:
bash
1. kubectl apply -f redis-service.yaml → Service 对象写入 etcd
2. Endpoints Controller 监听 Service → 找到匹配 selector 的 Pod → 创建 Endpoints 对象
3. CoreDNS 监听 Service → 创建 DNS A 记录(redis-service → 10.96.100.50)
4. kube-proxy 监听 Service + Endpoints → 在节点上创建 iptables/IPVS 规则
5. 客户端 Pod 发起 DNS 查询(redis-service) → CoreDNS 返回 ClusterIP
6. 客户端 Pod 向 ClusterIP:6379 发起 TCP 连接 → iptables 规则截获 → DNAT 到 Pod IP
7. Pod 的 readiness probe 失败 → Endpoints 移除该 Pod IP → kube-proxy 更新规则 → 流量摘除
这就是 K8s 服务发现的完整闭环。每一个环节都有对应的控制器在持续工作------Service Controller 管理 Service 生命周期,Endpoints Controller 维护 Pod IP 列表,kube-proxy 同步转发规则,CoreDNS 提供名称解析。
七、命令速查表
八、本篇总结
-
Endpoints:Service 的"后端 Pod 清单",由 Endpoints Controller 自动维护,只包含 readiness probe 通过的 Pod IP。
-
kube-proxy:将 ClusterIP 的流量通过 iptables/IPVS 规则转发到后端 Pod,支持 iptables 和 IPVS 两种模式。
-
CoreDNS :K8s 集群级 DNS 服务器,自动为每个 Service 创建 A 记录(
<服务名>.<命名空间>.svc.cluster.local),是 Docker DNS 的集群级升级版。 -
Headless Service :
clusterIP: None,DNS 直接返回 Pod IP 而非 ClusterIP,适用于 StatefulSet 和数据库集群等需要直接通信的场景。 -
服务发现全链路:Service → Endpoints → CoreDNS → kube-proxy,四者协作完成从名称解析到流量转发的完整过程。
这篇彻底拆解了 K8s 服务发现的底层机制。但 Service 只能做四层(TCP/UDP)负载均衡------如果你需要基于 HTTP Host 或 URL 路径路由请求,就需要七层负载均衡。下一篇------第 30 篇:Ingress 基础:域名路由与 Ingress Controller,我们将解锁 K8s 的 HTTP 路由能力。
想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !