文章目录
-
- [一、Service 的本质:不是负载均衡器,是服务发现机制](#一、Service 的本质:不是负载均衡器,是服务发现机制)
-
- [1.1 Service 与 Endpoints 的关系](#1.1 Service 与 Endpoints 的关系)
- [1.2 EndpointSlice:Endpoints 的升级版](#1.2 EndpointSlice:Endpoints 的升级版)
- [二、Service 五种类型完整对比](#二、Service 五种类型完整对比)
-
- [2.1 ClusterIP:最基础的虚拟 IP](#2.1 ClusterIP:最基础的虚拟 IP)
- [2.2 NodePort:在节点级别暴露端口](#2.2 NodePort:在节点级别暴露端口)
- [2.3 LoadBalancer:云厂商的集成入口](#2.3 LoadBalancer:云厂商的集成入口)
- [2.4 ExternalName:CNAME 转发而非代理](#2.4 ExternalName:CNAME 转发而非代理)
- [2.5 Headless Service:DNS 直连 Pod](#2.5 Headless Service:DNS 直连 Pod)
- 三、externalTrafficPolicy:流量转发策略的关键抉择
-
- [3.1 Cluster 模式(默认)](#3.1 Cluster 模式(默认))
- [3.2 Local 模式](#3.2 Local 模式)
- [四、Ingress Controller:七层路由的完整流量路径](#四、Ingress Controller:七层路由的完整流量路径)
-
- [4.1 为什么需要 Ingress](#4.1 为什么需要 Ingress)
- [4.2 完整流量路径:浏览器到 Pod](#4.2 完整流量路径:浏览器到 Pod)
- [4.3 主流 Ingress Controller 对比](#4.3 主流 Ingress Controller 对比)
- [4.4 Ingress 路由规则详解](#4.4 Ingress 路由规则详解)
- 五、生产环境选型决策树
- 六、验证命令清单
- 七、总结
前置知识 : kubectl apply 基本用法,Deployment 部署 Pod 的流程,Kubernetes DNS(CoreDNS)的基本概念。
一、Service 的本质:不是负载均衡器,是服务发现机制
在 Kubernetes 中,Service 的核心职责是服务发现,而不是负载均衡。负载均衡是 Service 实现服务发现时附带的效果,理解这一点是正确使用 Service 的前提。
1.1 Service 与 Endpoints 的关系
每创建一个带 selector 的 Service,Kubernetes 会自动创建一个同名的 Endpoints 资源。Endpoints 记录了所有匹配 selector 条件的 Pod IP 和端口。
yaml
# Service 定义
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
selector:
app: nginx
ports:
- port: 80
targetPort: 80
bash
# 自动生成的 Endpoints
kubectl get endpoints nginx
# 输出示例:
# NAME ENDPOINTS AGE
# nginx 10.244.1.15:80, 10.244.2.8:80 3d
Endpoints 背后是 Endpoints Controller(运行在 Controller Manager 中)持续监听 Pod 的变化:
是
否
Pod 创建/更新/删除
Endpoints Controller
Watch Pod 变更事件
Pod 是否匹配
Service selector?
Endpoints 添加
Pod IP:Port
Endpoints 移除
该 Pod 条目
etcd
Endpoints 变更
所有 Watch Endpoints 的组件
(Kube-Proxy/Ingress Controller)
更新本地缓存
准备响应请求
Endpoints Controller 与 Deployment Controller 一样,都是 Kubernetes 的被动控制器------不主动创建事件,而是监听 etcd 中的状态变化并驱动相关资源向期望状态收敛。
1.2 EndpointSlice:Endpoints 的升级版
K8s 1.16 引入了 EndpointSlice,作为 Endpoints 的可扩展替代方案。每个 EndpointSlice 最多包含 100 个端点,解决了大集群中单个 Endpoints 对象过大的问题。
bash
# 查看 EndpointSlice(替代了传统的 Endpoints)
kubectl get endpointslice -l kubernetes.io/service-name=nginx
# 输出示例:
# NAME ADDRS PORTS AGE
# nginx-abc123 10.244.1.15 80 3d
# nginx-abc123 10.244.2.8 80 3d
EndpointSlice 使用标签选择器而非固定命名,这使得控制器可以并行更新多个 Slice,减少了 Watch 事件的数量------在拥有数千个 Service 的大规模集群中,这能显著降低 API Server 的压力。
二、Service 五种类型完整对比
| 类型 | ClusterIP | NodePort | LoadBalancer | ExternalName | Headless |
|---|---|---|---|---|---|
| 访问范围 | 集群内部 | 集群内部+节点端口 | 集群内部+外部+节点端口 | 集群内部 | 集群内部 |
| IP 类型 | 虚拟 IP(集群内部) | 虚拟 IP + 节点端口 | 虚拟 IP + 节点端口 + 云厂商 LB IP | 无 ClusterIP | 无 ClusterIP |
| 负载均衡 | kube-proxy iptables/ipvs | kube-proxy iptables/ipvs | 云厂商 LB + kube-proxy | CNAME 转发 | 无 LB,DNS 直连 Pod |
| 外部流量入口 | 无 | 节点 IP:NodePort | LB IP:Port | 无 | 无 |
| 源 IP 保留 | 内部流量,无外部场景 | 受限于 externalTrafficPolicy | 受限于 externalTrafficPolicy | 不适用 | 不适用 |
| 适用场景 | 微服务间调用 | 开发测试/临时暴露 | 生产环境外部访问 | 连接外部数据库/第三方 API | StatefulSet 有状态服务 |
2.1 ClusterIP:最基础的虚拟 IP
ClusterIP 是默认类型,Service 在集群内部获得一个仅集群内可达的虚拟 IP。Pod 内通过 nginx.default.svc.cluster.local(或短名称 nginx.default)访问服务。
yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
type: ClusterIP
selector:
app: nginx
ports:
- port: 80
targetPort: 80
Kube-proxy 在每个节点上通过 iptables(或 ipvs)规则实现转发:
bash
# 查看 node1 上的 iptables 规则(简化输出)
iptables -t nat -L KUBE-SERVICES | grep nginx
# 示例输出:
# KUBE-SVC-XXXXX tcp -- anywhere 10.0.0.1 tcp dpt:http /* default/nginx:http cluster IP */
# KUBE-NODEPORTS-XXXXX tcp -- anywhere anywhere /* default/nginx:http nodePort */ ADDRTYPE match dst-type LOCAL
请求到达 Service IP 时,iptables 随机选择一条 KUBE-SEP-XXXXX 链,将目标 IP 替换为实际 Pod IP。这个随机转发过程就是 Service 的"负载均衡"------没有算法可言,纯粹的随机选择。
2.2 NodePort:在节点级别暴露端口
NodePort 在每个节点的相同端口上暴露 Service,外部流量通过 NodeIP:NodePort 访问。
yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
type: NodePort
selector:
app: nginx
ports:
- port: 80
targetPort: 80
nodePort: 30080 # 可选,不指定则随机分配(30000-32767)
客户端
NodeIP:30080
任意节点均可
Kube-Proxy
iptables/ipvs
Pod A
10.244.1.15:80
Pod B
10.244.2.8:80
关键点:任意节点的 NodePort 都能接收请求,kube-proxy 会将请求转发到任意节点的 Pod,无论该节点上是否实际运行着目标 Pod。这意味着节点 1 收到的请求可能被转发到节点 2 上的 Pod,这会带来跨节点流量和 SNAT 开销。
2.3 LoadBalancer:云厂商的集成入口
LoadBalancer 类型只在云环境(MCP/ACK/EKS/GKE)中有效,它请求云厂商的 Cloud Controller Manager(CCM)创建一个托管负载均衡器。
yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
service.beta.kubernetes.io/alibaba-cloud-loadbalancer-id: "lb-xxxx" # 阿里云示例
spec:
type: LoadBalancer
selector:
app: nginx
ports:
- port: 80
targetPort: 80
CCM 的工作流程:
创建 LoadBalancer Service
API Server 写入
Service status.loadBalancer.ingress
Cloud Controller Manager
Watch Service 创建事件
CCM 调用云厂商 API
创建 LB 实例
CCM 配置 LB 后端
挂载所有 Node 或 Pod
CCM 更新 Service
status.loadBalancer.ingress
填充公网/私网 IP
用户通过 LB IP 访问 Service
On-Premise 环境没有云厂商 CCM,通常使用 MetalLB 在二层网络广播一个虚拟 IP 来模拟 LoadBalancer 行为。MetalLB 支持两种模式:Layer 2 模式( ARP/NDP 广播)和 BGP 模式(需要网络设备支持 BGP 路由协议)。
2.4 ExternalName:CNAME 转发而非代理
ExternalName 将 Service 映射为外部域名的 CNAME 记录,不做流量代理。
yaml
apiVersion: v1
kind: Service
metadata:
name: mysql-external
spec:
type: ExternalName
externalName: mysql.prod.example.com
bash
# 在 Pod 内执行 nslookup 验证
kubectl exec -it test-pod -- nslookup mysql-external.default.svc.cluster.local
# 输出:
# Name: mysql-external.default.svc.cluster.local
# Address: xxx.xxx.xxx.xxx (mysql.prod.example.com 的 IP)
# mysql-external.default.svc.cluster.local canonical name = mysql.prod.example.com
使用时有一个常见的坑:ndots 问题 。K8s 集群内的 DNS 搜索域默认为 default.svc.cluster.local 和 svc.cluster.local。当 Pod 内请求 mysql-external(不带域名后缀)时,DNS 客户端会先尝试拼接搜索域查询:
1. mysql-external.default.svc.cluster.local → 不存在
2. mysql-external.svc.cluster.local → 不存在
3. mysql-external → 向外网查询,获取 CNAME → mysql.prod.example.com
前两次查询是浪费,每次 DNS 查询耗时 5ms,在高频调用场景下这是可观的延迟。解决方案是在 Pod 内配置 dnsConfig,减少 ndots 值:
yaml
spec:
containers:
- name: app
dnsPolicy: ClusterFirst
dnsConfig:
options:
- name: ndots
value: "2" # 仅对少于 2 个点的域名使用搜索域
2.5 Headless Service:DNS 直连 Pod
Headless Service 通过设置 clusterIP: None 禁用集群虚拟 IP,DNS 查询直接返回 Pod IP 而非 Service 虚拟 IP。
yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-headless
spec:
clusterIP: None # 关键:禁用虚拟 IP
selector:
app: nginx
ports:
- port: 80
targetPort: 80
bash
# DNS 查询返回 Pod IP 列表(而非 Service IP)
kubectl run dns-test --image=busybox --rm -it -- nslookup nginx-headless.default.svc.cluster.local
# 输出示例:
# Name: nginx-headless.default.svc.cluster.local
# Address: 10.244.1.15 (直接返回 Pod IP)
# Name: nginx-headless.default.svc.cluster.local
# Address: 10.244.2.8 (每个 Pod IP 单独一条记录)
Headless + StatefulSet 的典型用法:
yaml
# StatefulSet 关联 Headless Service
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql-headless # 关联 Headless Service
replicas: 3
selector:
matchLabels:
app: mysql
template:
spec:
containers:
- name: mysql
image: mysql:8.0
StatefulSet 中每个 Pod 有固定的序号和稳定的主机名:mysql-0.mysql-headless.default.svc.cluster.local。这个主机名就是 Pod 的网络身份------重启后 IP 变了,但主机名不变,客户端可以通过 DNS 固定找到同一个 Pod。
三、externalTrafficPolicy:流量转发策略的关键抉择
这个字段控制来自集群外部(NodePort/LoadBalancer)的流量如何转发:
yaml
spec:
externalTrafficPolicy: Cluster # 默认值
# 或
externalTrafficPolicy: Local # 保留源 IP,但可能丢失可用性
3.1 Cluster 模式(默认)
流量可以转发到任意节点的 Pod,kube-proxy 在转发时做 SNAT(源地址转换)。
请求到 NodePort
SNAT: 203.0.113.5 → Node1 IP
转发到其他节点
SNAT: IP 替换
返回给 Node1
反SNAT
客户端
收到返回
Node 1
(无目标 Pod)
Kube-Proxy
Node 2
(有 Pod)
Pod
源 IP = Node1 内部 IP
SNAT 的代价:Pod 看到的源 IP 不是真实客户端 IP,这对需要记录真实访问者 IP 的应用(如 Web 应用防火墙、访问日志分析)是致命的。
3.2 Local 模式
流量只发送到本节点上有 Pod 的 Service,不会跨节点转发。源 IP 被保留。
NodePort → Pod 同节点
直接返回
客户端
源 IP 保留 ✓
Node 1
(有 Pod)
Kube-Proxy
Pod
源 IP = 203.0.113.5 ✓
但 Local 模式有一个隐含代价:没有本节点 Pod 的 NodePort 不接收流量。如果 Node 1 有 2 个 Pod,Node 2 有 0 个 Pod,那么 Node 2 的 NodePort 会直接拒绝连接------这在高可用场景下可能导致部分请求失败。
| 特性 | Cluster 模式 | Local 模式 |
|---|---|---|
| 源 IP 保留 | ❌ SNAT 替换 | ✅ 保留 |
| 跨节点转发 | ✅ 支持 | ❌ 仅本节点 |
| 可用性 | ✅ 分散到所有 Pod | ⚠️ 仅本节点 Pod |
| SNAT 开销 | 有(额外延迟) | 无 |
| 适用场景 | 不需要真实源 IP | WAF、日志分析、监管要求 |
四、Ingress Controller:七层路由的完整流量路径
Ingress 是 Kubernetes 对 HTTP/HTTPS 路由的抽象,但 Ingress 本身只是声明式配置------真正执行流量转发的是 Ingress Controller。
4.1 为什么需要 Ingress
在没有 Ingress 的情况下,外部流量进入集群的路径:
外部客户端 → NodePort → kube-proxy → Service → Pod
这个路径有几个问题:
- 端口管理:每个 Service 都需要一个独立的 NodePort(30000-32767),端口资源有限且难以记忆
- 域名路由:无法基于域名区分不同服务,所有服务共享 NodePort
- TLS 终止:每个 Service 都要单独配置 TLS 证书
Ingress 通过七层(HTTP/HTTPS)路由解决了这些问题------多个域名和路径可以在同一个 NodePort/LB 后面复用。
4.2 完整流量路径:浏览器到 Pod
HTTPS:443
HTTP → Ingress Controller
解析 Ingress 规则
匹配 host + path
查询 EndpointSlice
获取 Pod IP 列表
负载均衡选择
round-robin/least-conn
浏览器
app.example.com/api
云厂商 LB
/或NodePort
Ingress Controller Pod
(NGINX/Contour/Traefik)
Ingress 规则
host: app.example.com
path: /api → nginx-svc:80
EndpointSlice
10.244.1.15:80
10.244.2.8:80
Kube-Proxy
或直连(通过 IPVS)
Pod A
10.244.1.15:80
Pod B
10.244.2.8:80
关键问题:Ingress Controller 怎么知道 Service 有哪些 Pod?
Ingress Controller 本身也是一个 Pod,它通过 Watch API Server 获取 EndpointSlice(而非 Endpoints)。EndpointSlice 由 EndpointSlice Controller 维护,随 Pod 变化自动更新------这个过程对 Ingress Controller 透明。
bash
# NGINX Ingress Controller 的 Pod 日志(调试用)
kubectl logs -n ingress-nginx ingress-nginx-controller-xxx | grep "endpoint"
# 可以看到 Controller 动态添加/删除后端的过程
4.3 主流 Ingress Controller 对比
| 特性 | NGINX Ingress Controller | Contour (Envoy) | Traefik |
|---|---|---|---|
| 底层代理 | NGINX | Envoy | Traefik (Go) |
| 配置热加载 | 支持(reload) | 支持(xDS API) | 支持(无 reload) |
| 协议支持 | HTTP/TCP/UDP | HTTP/gRPC/WebSocket | HTTP/gRPC/WebSocket/TCP |
| 限流 | 支持 | 支持 | 支持 |
| 认证 | Basic/OAuth/JWT | JWT/OAuth | JWT/OAuth |
| CRD | 原生 Ingress + 注解 | CRD (HTTPProxy) | CRD (IngressRoute) |
| 金丝雀发布 | 注解支持 | CRD 原生支持 | 注解支持 |
4.4 Ingress 路由规则详解
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2 # URL 重写
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
spec:
ingressClassName: nginx # K8s 1.18+ 推荐写法(替代弃用的 kubernetes.io/ingress.class)
rules:
- host: app.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: nginx-api
port:
number: 80
- path: /admin
pathType: Exact
backend:
service:
name: nginx-admin
port:
number: 80
pathType 的三种取值:
| pathType | 匹配规则 | 示例 |
|---|---|---|
Prefix |
URL 前缀匹配 | /api 匹配 /api、/api/users、/api/v2 |
Exact |
URL 完全匹配 | /api 只匹配 /api,不匹配 /api/ |
ImplementationSpecific |
由 IngressClass 决定 | 默认行为,通常等同于 Prefix |
rewrite-target注解是一个高频使用但容易被忽视的配置。如果 Ingress path 为/api,而后端服务期望的路径是/(根路径),不配置重写会导致请求/api/users被直接透传到后端的/api/users路径------而后端没有这个路由,就会返回 404。
五、生产环境选型决策树
HTTP/HTTPS
TCP/UDP
是
否
HTTP 内网
单服务/简单路径
多域名/多路径
云环境
自建数据中心
保留源 IP
高可用优先
外部流量入口
访问协议
使用 Ingress
是否有云厂商 LB?
LoadBalancer Service
MetalLB
ClusterIP + kubectl port-forward
需要多少路由规则?
NodePort
Ingress + IngressClass
集群环境
Cloud LB → Ingress Controller
MetalLB → Ingress Controller
ExternalTrafficPolicy?
Local 模式
Cluster 模式
六、验证命令清单
bash
# 1. 查看 Service 及其后端 Endpoints
kubectl get svc,endpoints nginx -o wide
# 2. 查看 EndpointSlice(K8s 1.16+)
kubectl get endpointslice -l kubernetes.io/service-name=nginx
# 3. 查看 Kube-Proxy 在各节点的转发规则数量
kubectl exec -it nginx-pod -- cat /proc/net/iptables覆盖率 # iptables 模式下查看规则数
# 4. 验证 DNS 解析(Headless vs ClusterIP)
kubectl run dns-test --image=busybox --rm -it -- nslookup nginx-headless
kubectl run dns-test --image=busybox --rm -it -- nslookup nginx-clusterip
# 5. 查看 LoadBalancer 外部 IP(云环境)
kubectl get svc nginx -o jsonpath='{.status.loadBalancer.ingress}'
# 6. 查看 Ingress 路由规则
kubectl get ingress -o wide
# 7. 查看 Ingress Controller Pod 日志(调试路由)
kubectl logs -n ingress-nginx ingress-nginx-controller-xxx -f
# 8. 测试 ExternalTrafficPolicy
# Cluster 模式:Pod 内查看源 IP
kubectl exec -it nginx-pod -- wget -qO- http://127.0.0.1/source-ip
# Local 模式:同上,源 IP 应为真实客户端 IP
# 9. 查看 MetalLB 分配的 IP(on-prem)
kubectl get pods -n metallb-system
kubectl get ipaddresspools -n metallb-system
# 10. 验证 ExternalName CNAME 解析
kubectl run dns-test --image=busybox --rm -it -- nslookup mysql-external
七、总结
Service 和 Ingress 是 K8s 网络模型中两个不同层次的抽象:Service 处理四层(TCP/UDP)的服务发现和负载均衡,Ingress 处理七层(HTTP/HTTPS)的路由。
理解 Endpoints/EndpointSlice 的自动维护机制,才能理解 Service 背后的 Pod 列表为什么会自动更新。理解 externalTrafficPolicy 的 Cluster 和 Local 差异,才能在"源 IP 保留"和"高可用"之间做出正确权衡。理解 Ingress Controller 完整流量路径,才能在路由不生效时准确定位问题------是 LB 层的问题、Ingress 规则的问题,还是 Endpoints 缺失的问题。
关键结论:
- Endpoints 是 Service 的后端 Pod 列表,由 Endpoints Controller 自动维护,无需手动管理
- EndpointSlice 解决了大集群中单个 Endpoints 对象过大的问题,推荐在 K8s 1.16+ 环境中使用
- ExternalName 是 CNAME 转发,注意 ndots 问题对高频调用的性能影响
- Headless Service 让 DNS 直连 Pod IP,适用于 StatefulSet 的固定网络身份
- Ingress Controller 通过 Watch EndpointSlice 感知后端 Pod 变化,这是它能动态路由的根本
- ExternalTrafficPolicy=Local 保留源 IP,但仅转发到本节点 Pod;Cluster 模式跨节点转发但源 IP 被 SNAT 替换
觉得这篇文章有收获的话,欢迎点赞、关注。技术创作不易,每一份支持都是坚持下去的动力。