第29篇 k8s之Service 与 Endpoints 深入:服务发现原理

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 这样的标签。那么问题来了:

  1. 谁负责找到所有匹配标签的 Pod?

  2. Pod 的 IP 列表存储在哪里?

  3. Service 的虚拟 IP 怎么把流量转发到真实的 Pod IP?

  4. 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-serviceflask-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-dns Service)

  • redis-service.default.svc.cluster.local 是完整的 DNS 名称(<服务名>.<命名空间>.svc.cluster.local

  • 10.96.100.50redis-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 ServiceclusterIP: 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 思维 !

相关推荐
未秃头的程序猿1 小时前
别再让大模型单打独斗了!Java 多 Agent 协作实战:任务拆解+结果聚合
java·后端·ai编程
人道领域1 小时前
【LeetCode刷题日记】538.把二叉搜索树转换为累加树
java·开发语言·后端·算法·leetcode
西凉的悲伤1 小时前
Spring Boot + ShardingSphere 介绍
java·spring boot·后端·shardingsphere·分库分表
不爱编程的小陈1 小时前
Go内存模型与GC机制:高性能编程的核心
开发语言·后端·golang
日月云棠1 小时前
12 Enum —— 枚举类型的底层实现
java·后端
工位植物人1 小时前
深入理解Java中的类、抽象类、接口与枚举类
后端
用户2181697049301 小时前
Gin (二) 参数 路由分组
后端
用户925807911481 小时前
nacos服务注册源码浅析
后端
SimonKing2 小时前
Java程序员接入AI的另一种姿势:LangChain4j
java·后端·程序员