流量路径图#
graph TD
Client[客户端] -->|域名解析| DNS[DNS / CoreDNS]
DNS -->|返回 IP| Client
Client -->|HTTP/HTTPS| Ingress[Ingress Controller
可选]
Ingress -->|路由规则转发| Service[Service]
Client -->|NodePort/LoadBalancer
直接访问| Service
Service -->|查询就绪 Pod 列表| EP[Endpoint / EndpointSlice]
EP -->|写入转发规则| KubeProxy[kube-proxy
iptables / IPVS]
KubeProxy -->|负载均衡转发| Pod[Pod]
各组件的职责与作用#
从流量路径图,分析各组件的职责与作用。
DNS#
Kubernetes 集群内使用 CoreDNS 作为 DNS 服务器,负责将 Service 名称解析为可访问的 IP 地址。
核心作用#
- 服务发现:Pod 通过 Service 名(如
my-service)即可访问目标服务,无需硬编码 IP。 - 域名补全:自动根据 Pod 所在 Namespace 补全 FQDN,简化调用方式。
- 动态更新:CoreDNS 通过 Watch kube-apiserver 实时感知 Service/Endpoint 变化,DNS 记录自动保持最新。
工作流程#
- Pod 发起请求:应用代码访问
my-service.default.svc.cluster.local(或简写my-service)。 - 查询本地 DNS 配置:Pod 的
/etc/resolv.conf中配置了nameserver(指向 CoreDNS 的 ClusterIP,通常是10.96.0.10)和search域(如default.svc.cluster.local svc.cluster.local cluster.local)。 - 补全域名:如果应用只写了短名(如
my-service),kubelet 会依据search域依次尝试补全:my-service.default.svc.cluster.localmy-service.svc.cluster.localmy-service.cluster.local
- CoreDNS 解析:CoreDNS 收到查询后,查找其内存中缓存的 Service 记录:
- ClusterIP 类型 Service:返回 Service 的 ClusterIP(A 记录)。
- Headless Service(
clusterIP: None):直接返回后端所有就绪 Pod 的 IP 列表(多条 A 记录)。
- 返回结果:Pod 拿到 IP 后发起 TCP/UDP 连接。
graph TD
A[Pod 发起域名请求] -->|读取 /etc/resolv.conf| B[获取 nameserver 和 search 域]
B -->|短名补全| C{域名是否为 FQDN?}
C -->|是| D[直接查询 CoreDNS]
C -->|否| E[按 search 域依次补全尝试]
E --> D
D --> F{Service 类型?}
F -->|ClusterIP| G[返回 Service ClusterIP]
F -->|Headless| H[返回所有就绪 Pod IP]
G --> I[Pod 发起 TCP/UDP 连接]
H --> I
注意:CoreDNS 通过 Watch kube-apiserver 中的 Service 和 Endpoint 资源来实时更新 DNS 记录,无需手动配置。
Ingress(可选)#
Ingress 是 Kubernetes 中用于管理集群外部 HTTP/HTTPS 流量进入集群的资源对象。它本身只是一组路由规则的声明,真正执行流量转发的是 Ingress Controller(如 Nginx Ingress Controller、Traefik 等)。
核心作用#
- 七层路由:根据域名(Host)和路径(Path)将请求路由到不同的 Service。
- TLS 终止:在入口处统一处理 HTTPS 证书,后端 Service 只需处理 HTTP。
- 统一入口:多个 Service 共用一个外部 IP/域名,通过路由规则区分,避免每个服务都暴露一个 LoadBalancer。
工作流程#
- 用户创建 Ingress 资源,定义路由规则(如
api.example.com/v1 → service-a)。 - Ingress Controller 监听 kube-apiserver,感知到 Ingress 规则变化。
- Ingress Controller 将规则转换为自身的配置(如 Nginx 的
nginx.conf),并 reload 生效。 - 外部流量到达 Ingress Controller 后,根据 Host 和 Path 匹配规则,转发到对应的 Service ClusterIP。
graph TD
A[外部请求到达 Ingress Controller] --> B{匹配 Host 域名}
B -->|api.example.com| C{匹配 Path 路径}
B -->|web.example.com| D{匹配 Path 路径}
B -->|未匹配| E[返回 404 或默认后端]
C -->|/v1| F[转发到 service-a]
C -->|/v2| G[转发到 service-b]
D -->|/| H[转发到 service-web]
F --> I[Service → Pod]
G --> I
H --> I
何时不需要 Ingress:集群内部 Pod 互相访问时直接通过 Service 即可;外部流量也可以通过 NodePort 或 LoadBalancer 类型的 Service 直接暴露,但这样每个服务需要独立的端口或外部 IP。
Service#
Service 是 Kubernetes 中最核心的服务发现和负载均衡抽象。它为一组功能相同的 Pod 提供一个稳定的虚拟 IP(ClusterIP)和 DNS 名称,屏蔽了后端 Pod 动态变化的细节。
核心作用#
- 稳定入口:Pod 随时可能被重建、IP 会变,但 Service 的 ClusterIP 和 DNS 名始终不变。
- 负载均衡:通过 kube-proxy 将流量均匀分发到后端多个 Pod。
- 服务发现:其他 Pod 通过 Service 名(DNS)即可访问,无需关心具体 Pod IP。
Service 类型:
| 类型 | 说明 |
|---|---|
| ClusterIP(默认) | 仅集群内部可达,分配一个虚拟 IP |
| NodePort | 在每个节点上暴露一个端口(30000-32767),外部可通过 节点IP:端口 访问 |
| LoadBalancer | 依赖云厂商,自动创建外部负载均衡器并分配公网 IP |
| ExternalName | 将 Service 映射为一个外部 DNS 名(CNAME),不创建 ClusterIP |
NodePort 如何在每个节点上暴露端口?
NodePort 并不是在节点上启动一个监听进程,而是由 kube-proxy 在每个节点的 iptables/IPVS 中写入规则:
- 用户创建 NodePort 类型 Service,apiserver 分配一个端口(如
30080)。 - 每个节点上的 kube-proxy 监听到 Service 变化,在本节点的 iptables/IPVS 中添加规则:
任何到达本机 30080 端口的流量 → DNAT 到后端 Pod IP:Port。 - 外部请求
任意节点IP:30080→ 内核 netfilter 匹配 iptables 规则 → 转发到某个后端 Pod(可能在本节点,也可能跨节点)。
因此,即使某个节点上没有对应的 Pod,访问该节点的 NodePort 一样能到达其他节点上的 Pod——流量会被 iptables 规则跨节点转发。
LoadBalancer 如何感知后端 Pod?
LoadBalancer 类型的 Service 并不直接感知 Pod IP,它的架构是分层的:
- 用户创建 LoadBalancer 类型 Service → Kubernetes 自动创建一个 NodePort(如
30080)。 - 云厂商的 Cloud Controller Manager(运行在控制面)监听到该 Service,执行以下操作:
- 通过 kube-apiserver 获取当前集群所有 Ready 状态的 Node IP 列表。
- 读取 Service 上分配的 NodePort 端口号(如
30080)。 - 调用云厂商 API,创建外部负载均衡器(如 AWS ELB、阿里云 SLB),并将后端池设置为
node-1:30080, node-2:30080, ...。 - 持续 Watch Node 资源变化:节点新增时自动将其加入 LB 后端池,节点下线时自动移除。
- 外部 LB 的后端池始终与集群节点保持同步,由 Cloud Controller Manager 负责维护。
- 流量路径:
客户端 → 外部 LB → 某个节点的 NodePort → kube-proxy iptables/IPVS → Pod。
也就是说,外部 LB 只关心节点 IP + NodePort,不需要知道 Pod IP;Pod 级别的负载均衡仍然由节点上的 kube-proxy 完成。
Pod 级别的转发细节:
当流量到达某个节点的 NodePort 后,内核的 netfilter 框架按照 kube-proxy 预先写入的规则进行处理:
- DNAT(目标地址转换):iptables/IPVS 规则将目标地址从
节点IP:NodePort改为某个后端PodIP:ContainerPort,选择哪个 Pod 由负载均衡算法决定(iptables 用随机概率,IPVS 支持轮询、最小连接等)。 - 路由判断:
- 如果目标 Pod 就在本节点:数据包直接通过本机网桥(如
cbr0/cni0)转发到 Pod 的 veth 网卡。 - 如果目标 Pod 在其他节点:数据包经由节点间的 overlay 网络(如 VXLAN)或路由(如 BGP)转发到目标节点,再由该节点转发到 Pod。
- 如果目标 Pod 就在本节点:数据包直接通过本机网桥(如
- SNAT(源地址转换,可选):默认情况下,跨节点转发时会做 SNAT(将源 IP 改为本节点 IP),确保回包能原路返回。如果设置了
externalTrafficPolicy: Local,则跳过 SNAT 保留客户端真实 IP,但流量只会转发到本节点上的 Pod。
实际上,无论哪种 Service 类型,流量到达节点后的 Pod 转发逻辑完全一致,区别仅在于流量如何到达节点:
| 类型 | 流量到达节点的方式 | 到达节点后的 Pod 转发 |
|---|---|---|
| ClusterIP | 集群内 Pod 直接访问 ClusterIP | 同一套 iptables/IPVS 规则做 DNAT |
| NodePort | 外部直接访问 节点IP:NodePort | 同一套 iptables/IPVS 规则做 DNAT |
| LoadBalancer | 外部 LB 转发到 节点IP:NodePort | 同一套 iptables/IPVS 规则做 DNAT |
LoadBalancer 本质上就是 在 NodePort 前面加了一层外部负载均衡器,到达节点之后的转发链路完全一致
部分云厂商支持"直通 Pod"模式(如 AWS 的 IP 模式 Target Group、阿里云的 ENI 模式),此时 Cloud Controller Manager 会直接读取 EndpointSlice 中的 Pod IP 写入 LB 后端池,跳过 NodePort 层,减少一跳延迟。
工作流程#
- 用户创建 Service,通过
selector指定匹配的 Pod 标签。 - Endpoint Controller 自动收集匹配且就绪的 Pod IP,生成 Endpoint/EndpointSlice。
- kube-proxy 监听 Service 和 Endpoint 变化,将规则写入节点的 iptables 或 IPVS。
- 当流量到达 Service ClusterIP 时,由 iptables/IPVS 规则进行 DNAT,转发到某个后端 Pod IP。
graph TD
A[请求到达 Service ClusterIP] --> B[kube-proxy 匹配 iptables/IPVS 规则]
B --> C{负载均衡选择}
C --> D[Pod-1]
C --> E[Pod-2]
C --> F[Pod-3]
G[用户创建 Service
selector: app=web] --> H[Endpoint Controller 收集就绪 Pod IP]
H --> I[生成 Endpoint/EndpointSlice]
I --> B
Endpoint / EndpointSlice#
Endpoint 和 EndpointSlice 是 Service 背后维护就绪 Pod IP 列表的资源对象。它们是 Service 能够正确转发流量的数据基础。
核心作用#
- 记录后端地址:存储与 Service selector 匹配且通过
readinessProbe的所有 Pod 的IP:Port。 - 动态更新:Pod 创建/销毁/就绪状态变化时,Endpoint Controller 自动增删对应条目。
- 驱动 kube-proxy:kube-proxy 监听 EndpointSlice 的变化来更新转发规则。
Endpoint vs EndpointSlice:
| 维度 | Endpoint | EndpointSlice |
|---|---|---|
| 粒度 | 一个 Service 对应一个 Endpoint 对象,包含所有 Pod IP | 一个 Service 可对应多个 EndpointSlice,每个最多 100 个端点 |
| 性能 | Pod 数量大时,单次更新需传输整个列表 | 分片更新,减少 apiserver 和 kube-proxy 的负载 |
| 版本 | v1(旧) | discovery.k8s.io/v1(推荐,K8s 1.21+ 默认) |
工作流程#
- Pod 启动并通过
readinessProbe→ Endpoint Controller 将其 IP 加入 EndpointSlice。 - Pod 失败或被删除 → Endpoint Controller 将其 IP 从 EndpointSlice 中移除。
- kube-proxy 感知变化 → 更新 iptables/IPVS 规则 → 流量不再转发到不可用的 Pod。
graph TD
A[Pod 启动] --> B{readinessProbe 通过?}
B -->|是| C[Endpoint Controller 将 Pod IP 加入 EndpointSlice]
B -->|否| D[不加入,不接收流量]
C --> E[kube-proxy 感知变化]
E --> F[更新 iptables/IPVS 转发规则]
F --> G[流量可达该 Pod]
H[Pod 异常/删除] --> I[Endpoint Controller 移除 Pod IP]
I --> E