上一篇我们拆解了 kube-proxy 四种模式的内核实现,搞清楚了 ClusterIP 的 DNAT 是怎么写进 iptables/IPVS/nftables 的。那篇文章的核心结论是:kube-proxy 把 Service 的虚拟 IP 翻译成后端 Pod IP,流量在内核态完成转发,不经过用户态。
但那只是故事的一半。kube-proxy 默认行为下有两个问题你迟早会遇到:
源 IP 丢失。外部流量经过 NodePort 进入集群后,如果 kube-proxy 把包转发到另一个节点上的 Pod,它会做一次 SNAT – 把源 IP 改成当前节点的 IP。后端 Pod 看到的源地址是节点 IP 而不是真实的客户端 IP。对于需要做访问控制、审计、地理限流的场景,这是致命的。
跨节点额外一跳。默认模式下,即使流量到达的节点上没有后端 Pod,kube-proxy 仍然会接受这个包然后转发到另一个节点。这多了一跳网络开销,延迟增加,带宽浪费。
Kubernetes
为这两个问题提供了三层递进的解决方案:externalTrafficPolicy、internalTrafficPolicy、以及拓扑感知路由(TopologyAwareHints)。它们的思路一脉相承
– 让流量尽可能在本地消费。
本文要做的事:
- 拆解
externalTrafficPolicy: Local与Cluster的真实数据面行为差异 - 解释
internalTrafficPolicy: Local如何实现集群内流量本地化 - 深入 TopologyAwareHints 的 EndpointSlice 机制和失效场景
- 对比 LoadBalancer 的云厂商实现差异(AWS NLB、GCP ILB、MetalLB)
- 深度拆解 MetalLB:L2 模式与 BGP 模式的原理
- 讨论三种流量策略的组合使用场景
- 实验:对比 Local 和 Cluster 模式下的源 IP 保留行为
本文基于 Kubernetes 1.31, kube-proxy v1.31, Linux 6.x 内核。 实验环境:kind v0.24, MetalLB v0.14, Ubuntu 22.04
一、ExternalTrafficPolicy:Local vs Cluster
externalTrafficPolicy 是 Service
上的一个字段,只对 NodePort 和
LoadBalancer
类型生效。它决定了外部流量到达节点后,kube-proxy
如何选择后端 Pod。
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: LoadBalancer
externalTrafficPolicy: Local # 或 Cluster(默认)
selector:
app: web
ports:
- port: 80
targetPort: 8080Cluster 模式(默认):均匀但丢源 IP
externalTrafficPolicy: Cluster
是默认行为。kube-proxy 在每个节点上为 NodePort 写入 DNAT
规则,目标是所有后端 Pod,不管 Pod 在哪个节点:
外部客户端 203.0.113.5
|
v
Cloud LB (EIP)
|
v (任意节点的 NodePort 30080)
Node A (10.0.1.10) -- 本节点没有 svc 的后端 Pod
|
| kube-proxy DNAT: dst -> Pod 10.244.2.8:8080 (在 Node B 上)
| SNAT: src 203.0.113.5 -> 10.0.1.10 <-- 源 IP 被替换
|
v
Node B: Pod 10.244.2.8:8080
Pod 看到的源 IP: 10.0.1.10 (不是客户端真实 IP)
为什么必须做 SNAT?因为如果不做,回包的源是 Pod IP,目的是客户端 IP,Pod 所在的 Node B 会直接把回包发给客户端,绕过了 Node A。客户端收到一个从未请求过的 IP(Node B)发来的包,TCP 连接会被 RST。SNAT 保证回包走原路返回,经过 Node A 的 conntrack 做反向 NAT。
Cluster 模式的 iptables 规则长这样:
# 查看 NodePort 的 DNAT 链
iptables-save -t nat | grep -A5 "KUBE-NODEPORTS"
# 典型输出(简化):
-A KUBE-NODEPORTS -p tcp --dport 30080 -j KUBE-EXT-XXXXXXXX
-A KUBE-EXT-XXXXXXXX -j KUBE-MARK-MASQ # 标记需要 MASQUERADE
-A KUBE-EXT-XXXXXXXX -j KUBE-SVC-XXXXXXXX # 进入 Service 链
-A KUBE-SVC-XXXXXXXX -m statistic --mode random --probability 0.50 -j KUBE-SEP-AAAA
-A KUBE-SVC-XXXXXXXX -j KUBE-SEP-BBBB
-A KUBE-SEP-AAAA -p tcp -j DNAT --to-destination 10.244.1.5:8080
-A KUBE-SEP-BBBB -p tcp -j DNAT --to-destination 10.244.2.8:8080关键在 KUBE-MARK-MASQ。这条规则给包打上
0x4000 标记,在 POSTROUTING 阶段被
KUBE-POSTROUTING 链匹配并执行 MASQUERADE(SNAT
的一种形式,自动选择出口 IP)。
Local 模式:保留源 IP 但可能负载不均
externalTrafficPolicy: Local
改变了两件事:
- kube-proxy 只把 NodePort 流量 DNAT 到本节点上的后端 Pod
- 不做 SNAT – 因为不存在跨节点转发,回包自然走原路
外部客户端 203.0.113.5
|
v
Cloud LB (EIP)
|
| healthCheckNodePort 探测各节点
| Node C: 无本地 Pod -> 503 -> LB 移除
| Node D: 有本地 Pod -> 200 -> LB 保留
|
v (只发往有后端 Pod 的节点)
Node D (10.0.1.40)
|
| kube-proxy DNAT: dst -> Pod 10.244.4.3:8080 (本地 Pod)
| 无 SNAT
|
v
Pod 10.244.4.3:8080
Pod 看到的源 IP: 203.0.113.5 (真实客户端 IP)
Local 模式的 iptables 规则与 Cluster 的关键区别:
iptables-save -t nat | grep -A5 "KUBE-EXT"
# Local 模式下:
-A KUBE-EXT-XXXXXXXX -s <podCIDR> -j KUBE-SVC-XXXXXXXX # 集群内流量走正常链
-A KUBE-EXT-XXXXXXXX -j KUBE-SVL-XXXXXXXX # 外部流量走 Local 链
-A KUBE-SVL-XXXXXXXX -j KUBE-SEP-CCCC # 只有本地 Pod
-A KUBE-SEP-CCCC -p tcp -j DNAT --to-destination 10.244.4.3:8080
# 注意:没有 KUBE-MARK-MASQ -> 不做 SNAT注意链名从 KUBE-SVC 变成了
KUBE-SVL(Service Local),里面只包含本节点上的
Pod 作为后端。
healthCheckNodePort
Local 模式下,没有本地 Pod
的节点不应该接收外部流量。Kubernetes 通过
healthCheckNodePort 机制解决这个问题:
# 创建 Local 模式的 Service 后查看
kubectl get svc web -o yaml | grep healthCheckNodePort
# healthCheckNodePort: 31234这是一个自动分配的 NodePort。kube-proxy 在每个节点上监听这个端口,返回本节点的后端 Pod 数量:
# 有本地 Pod 的节点
curl http://10.0.1.40:31234/healthz
# 返回 200, body: {"localEndpoints": 2, "serviceProxyHealthy": true}
# 没有本地 Pod 的节点
curl http://10.0.1.30:31234/healthz
# 返回 503, body: {"localEndpoints": 0, "serviceProxyHealthy": true}云厂商的 LB 控制器会定期调用这个健康检查端口,把返回 503 的节点从后端池中移除。
负载不均问题
Local 模式的主要代价是负载不均。考虑以下场景:
3 个节点,3 个 Pod,LB 权重相同:
Node A: Pod-1
Node B: Pod-2, Pod-3
Node C: (无 Pod)
LB 权重:
Node A: 1/2 流量 (LB 在 A 和 B 之间均分)
Node B: 1/2 流量
实际 Pod 负载:
Pod-1: 50% (Node A 的全部流量)
Pod-2: 25% (Node B 流量的一半)
Pod-3: 25% (Node B 流量的另一半)
Pod-1 承受了两倍于 Pod-2、Pod-3 的流量。节点级均衡不等于 Pod 级均衡。
缓解方法:
- 使用
topologySpreadConstraints确保 Pod 在节点间均匀分布 - 云厂商 LB 支持按节点后端数加权(AWS NLB 从 2022 年开始支持)
- 使用拓扑感知路由(后文详述)
二、InternalTrafficPolicy:集群内流量本地化
externalTrafficPolicy
只影响外部流量(NodePort / LoadBalancer)。那集群内 Pod 到
Service 的流量呢?
Kubernetes 1.26 起,internalTrafficPolicy
正式
GA。它控制的是集群内流量的后端选择:
apiVersion: v1
kind: Service
metadata:
name: cache
spec:
type: ClusterIP
internalTrafficPolicy: Local # 默认是 Cluster
selector:
app: redis
ports:
- port: 6379Cluster 模式(默认)
和 externalTrafficPolicy: Cluster
类似,kube-proxy 把 ClusterIP 的流量 DNAT 到所有后端
Pod,不区分节点。这是默认行为,保证了均匀的负载分配。
Local 模式
设置为 Local 后,kube-proxy 只把 ClusterIP
流量转发到本节点上的
Pod。如果本节点没有对应的后端
Pod,流量会被丢弃(返回连接拒绝或超时)。
internalTrafficPolicy: Local
Node A:
Client Pod -> ClusterIP -> kube-proxy -> 只选择 Node A 上的后端 Pod
如果 Node A 没有后端 Pod -> 连接失败(不会转发到其他节点)
这和 externalTrafficPolicy: Local 不同 –
外部流量有 LB 和 healthCheckNodePort
来避免发往没有后端的节点,而内部流量没有这个保护机制。因此使用
internalTrafficPolicy: Local
时必须确保每个节点都有后端
Pod,通常意味着用 DaemonSet 部署后端。
典型用例
internalTrafficPolicy: Local 最适合
DaemonSet 类型的后端服务:
- 日志收集 / 监控代理 – 每个节点一个 Fluentd / node-exporter,Pod 只需要访问本节点的代理
- 节点级缓存 – 每个节点一个 Redis 实例,减少跨节点网络延迟
apiVersion: v1
kind: Service
metadata:
name: log-agent
spec:
type: ClusterIP
internalTrafficPolicy: Local
selector:
app: log-agent # 对应一个 DaemonSet
ports:
- port: 24224
targetPort: 24224数据面实现
kube-proxy 在 iptables 模式下,会为
internalTrafficPolicy: Local 的 Service
生成类似 KUBE-SVL 的链,只包含本节点 Pod
作为后端。IPVS 模式下,会把本节点 Pod
的权重设为正常值,其他节点 Pod 权重设为 0。
# iptables 模式下查看 internalTrafficPolicy: Local 的规则
iptables-save -t nat | grep "KUBE-SVL"
# IPVS 模式下查看
ipvsadm -Ln | grep -A5 "<ClusterIP>"
# 只有本节点的 Pod IP 权重 > 0三、拓扑感知路由(TopologyAwareHints)
externalTrafficPolicy 和
internalTrafficPolicy
都是粗粒度的二选一:要么全局均匀,要么严格本地。有没有更灵活的方案?
TopologyAwareHints(拓扑感知提示)在 Kubernetes 1.27 正式
GA(通过 service.kubernetes.io/topology-mode
注解),提供了一种基于可用区(Zone)的流量调度机制。
核心思想
让流量优先路由到同一可用区的后端 Pod,减少跨区流量(云厂商通常对跨区流量收费),同时保持一定程度的负载均衡。
without TopologyAwareHints:
Client Pod (zone-a) -> Service -> Pod (zone-a / zone-b / zone-c, 随机)
with TopologyAwareHints:
Client Pod (zone-a) -> Service -> Pod (zone-a, 优先)
启用方式
apiVersion: v1
kind: Service
metadata:
name: web
annotations:
service.kubernetes.io/topology-mode: Auto
spec:
selector:
app: web
ports:
- port: 80旧版注解
service.kubernetes.io/topology-aware-hints: Auto
在 1.27 之前使用,1.27+ 推荐使用
topology-mode。
EndpointSlice 的 hints 字段
TopologyAwareHints 的实现依赖
EndpointSlice。kube-controller-manager 中的 EndpointSlice
controller 会给每个 endpoint 打上
hints.forZones 字段,告诉 kube-proxy 这个
endpoint 应该服务哪些 zone 的流量:
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: web-abc12
labels:
kubernetes.io/service-name: web
addressType: IPv4
endpoints:
- addresses: ["10.244.1.5"]
zone: "us-east-1a"
conditions:
ready: true
hints:
forZones:
- name: "us-east-1a" # 这个 endpoint 服务 zone-a 的流量
- addresses: ["10.244.2.8"]
zone: "us-east-1b"
conditions:
ready: true
hints:
forZones:
- name: "us-east-1b"
- addresses: ["10.244.3.3"]
zone: "us-east-1c"
conditions:
ready: true
hints:
forZones:
- name: "us-east-1c"
ports:
- port: 8080
protocol: TCPkube-proxy 读取 EndpointSlice 时,如果 endpoint 有
hints 字段,就只把匹配本节点 zone 的
endpoint 写入 iptables/IPVS 规则。
# 查看节点的 zone 标签
kubectl get nodes -o custom-columns=NAME:.metadata.name,ZONE:.metadata.labels."topology\.kubernetes\.io/zone"
# 查看 EndpointSlice 的 hints
kubectl get endpointslice -l kubernetes.io/service-name=web -o yaml | grep -A3 hints分配算法
EndpointSlice controller 的分配算法核心逻辑:
- 统计每个 zone 的 CPU 容量比例(基于节点的 allocatable CPU)
- 按比例把 endpoint 分配到各 zone
- 优先把 endpoint 分配到自己所在的 zone
- 如果某个 zone 的 endpoint 不够覆盖其 CPU 比例,从邻近 zone 借用
示例:3 个 zone, 6 个 Pod
Zone CPU比例 分配的 endpoint
a 40% Pod-1(zone-a), Pod-2(zone-a), Pod-3(zone-b) -- 借了一个
b 30% Pod-4(zone-b)
c 30% Pod-5(zone-c), Pod-6(zone-c)
失效场景与回退
TopologyAwareHints 并非万能。以下场景会导致 hints 被移除,回退到全局路由:
- endpoint 数量不足 – 某个 zone 一个 endpoint 都没有,controller 无法满足分配要求
- zone 间负载比例严重失衡 – 预期分配比例超出可接受范围,controller 认为无法保证合理的负载分配
- 节点数量过少 – 单 zone 部署,拓扑感知路由没有意义
- kube-proxy 或 CNI 不支持 – 较旧的 kube-proxy 版本可能忽略 hints 字段;Cilium 有自己的拓扑感知实现
# 排查 hints 是否生效
kubectl get endpointslice -l kubernetes.io/service-name=web -o yaml
# 如果 endpoints 里没有 hints 字段,说明 controller 判断条件不满足
# 检查 kube-controller-manager 日志
kubectl -n kube-system logs -l component=kube-controller-manager | grep -i "topology\|hints"与 trafficPolicy 的关系
TopologyAwareHints 和
internalTrafficPolicy: Local
是互补而非互斥的:
internalTrafficPolicy: Local是节点级严格本地化,没有回退- TopologyAwareHints 是 zone 级软亲和,有回退机制
- 两者可以同时存在,但
internalTrafficPolicy: Local的优先级更高
四、LoadBalancer 的云厂商实现差异
type: LoadBalancer 是 Kubernetes
暴露服务到外部的标准方式,但它背后的实现完全依赖云厂商的
Cloud Controller
Manager(CCM)或者裸金属环境下的第三方实现。不同实现之间的差异远比你想象的大。
AWS:Network Load Balancer (NLB)
AWS NLB 工作在 L4(TCP/UDP),是 EKS 最常用的 LB 后端:
apiVersion: v1
kind: Service
metadata:
name: web
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "external"
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app: web
ports:
- port: 80
targetPort: 8080NLB 的关键特性:
- target-type: ip – NLB 直接把 Pod IP
作为后端(需要 AWS VPC CNI),绕过
NodePort,
externalTrafficPolicy失去意义 - target-type: instance – 传统模式,NLB
把 NodePort 作为后端,
externalTrafficPolicy正常生效 - 源 IP 保留 – ip 模式天然保留源 IP
- 跨区感知 – 支持 Cross-Zone Load Balancing 开关
GCP:Container-native Load Balancing
GCP 使用 NEG(Network Endpoint Group)实现类似 AWS ip target 的能力:
metadata:
annotations:
cloud.google.com/l4-rbs: "enabled"
networking.gke.io/load-balancer-type: "Internal"GCP 的关键差异:使用 Subsetting(每个后端组最多 250 节点)、默认不支持跨 region。
三大云厂商对比
特性 AWS NLB GCP Network LB Azure SLB
---------------------------------------------------------------------
层级 L4 (TCP/UDP) L4 (TCP/UDP) L4 (TCP/UDP)
直达 Pod ip target mode NEG (Container-native) 不支持
源 IP 保留 ip mode 天然保留 DSR 模式 Local policy
跨区负载均衡 可配置关闭 默认开启 默认开启
健康检查 HTTP/TCP/gRPC HTTP/TCP HTTP/TCP
每 LB 后端上限 无硬性限制 250/backend-group N/A
五、MetalLB 深度拆解
在云环境中,type: LoadBalancer
由云厂商提供。但在裸金属(bare-metal)或私有云环境中,创建
LoadBalancer Service 后它会一直处于 Pending
状态 – 因为没有 Cloud Controller Manager 来分配外部 IP。
MetalLB
就是为了解决这个问题。它是一个纯软件的负载均衡器实现,给裸金属
Kubernetes 集群提供 type: LoadBalancer
的能力。
架构
MetalLB 由两个组件构成:
- controller(Deployment):监听 Service 对象,从配置的 IP 池中分配 External IP
- speaker(DaemonSet):在每个节点上运行,负责宣告分配的 IP。根据模式不同,宣告方式是 ARP/NDP(L2 模式)或 BGP UPDATE(BGP 模式)
# MetalLB IP 地址池配置
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: production
namespace: metallb-system
spec:
addresses:
- 192.168.1.240-192.168.1.250
- 10.0.100.0/28L2 模式:ARP/NDP
L2 模式是最简单的部署方式。MetalLB speaker 选举出一个”领导者”节点来持有某个 Service 的 External IP,然后通过 Gratuitous ARP(IPv4)或 NDP Neighbor Advertisement(IPv6)宣告”这个 IP 在我这里”。
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default
namespace: metallb-system
spec:
ipAddressPools:
- production工作流程:
1. Service 创建,controller 从池中分配 IP 192.168.1.240
2. speaker 通过 memberlist 协议选举出 Node A 作为 192.168.1.240 的持有者
3. Node A 的 speaker 发送 Gratuitous ARP:
"192.168.1.240 is at <Node A MAC address>"
4. 同一 L2 广播域的路由器/交换机更新 ARP 表
5. 外部流量发往 192.168.1.240 时,交换机把包转发给 Node A
6. 到达 Node A 后,kube-proxy 正常进行 DNAT(根据 externalTrafficPolicy)
L2 模式的限制:
- 单节点瓶颈 – 所有流量都经过一个节点,该节点的带宽是瓶颈
- 故障切换延迟 – 持有者节点故障后,speaker 需要重新选举并发送新的 Gratuitous ARP,交换机需要更新 ARP 缓存。切换时间通常 2-10 秒
- 广播域限制 – 只在同一 L2 网络段内有效
# 查看哪个节点持有哪个 IP
kubectl -n metallb-system logs -l app=metallb,component=speaker | grep "announcing"
# 手动触发 ARP 查看
arping -c 3 192.168.1.240BGP 模式:真正的负载分散
BGP 模式下,每个节点的 speaker 与上游路由器建立 BGP session,把分配的 External IP 作为 /32 路由通告出去。上游路由器收到多条等价路由后,使用 ECMP(Equal-Cost Multi-Path)把流量分散到多个节点。
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: tor-switch
namespace: metallb-system
spec:
myASN: 64500
peerASN: 64501
peerAddress: 10.0.0.1
keepaliveTime: 20s
holdTime: 60s
---
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: default
namespace: metallb-system
spec:
ipAddressPools:
- production
communities:
- 64500:100工作流程:
1. Service 创建,controller 分配 IP 192.168.1.240
2. 所有节点的 speaker 与上游路由器 10.0.0.1 建立 BGP session
3. 每个 speaker 发送 BGP UPDATE:
"192.168.1.240/32 via <本节点 IP>, AS 64500"
4. 上游路由器收到 3 条等价路由(假设 3 节点):
192.168.1.240/32 via 10.0.1.10 [BGP]
192.168.1.240/32 via 10.0.1.20 [BGP]
192.168.1.240/32 via 10.0.1.40 [BGP]
5. 路由器用 ECMP 对流量做哈希,分散到 3 个节点
6. 到达节点后,kube-proxy 正常进行 DNAT
BGP 模式的优势:
- 真正的多节点负载分散 – 不存在单节点瓶颈
- 快速收敛 – BGP session 断开后,路由器在 holdTime 内撤回路由,通常秒级
- 跨 L2 域 – BGP 是 L3 协议,不受广播域限制
BGP 模式的注意事项:
- 需要上游路由器支持 BGP – 并且需要网络团队配合配置 peer
- ECMP 哈希粘性 – 节点数变化时,ECMP
重新哈希可能导致已有连接被发往不同节点。可以用
resilient ECMP(如果路由器支持)缓解
L2 vs BGP 选型
维度 L2 模式 BGP 模式
------------------------------------------------------
部署复杂度 极低(即装即用) 需要路由器配合
网络拓扑要求 同一 L2 广播域 需要 BGP peer
流量分散 单节点(瓶颈) 多节点 ECMP
故障切换速度 2-10 秒 秒级
跨子网 不支持 支持
推荐场景 小集群、测试环境 生产环境、大流量
六、组合策略:三种流量控制的协作
externalTrafficPolicy、internalTrafficPolicy、TopologyAwareHints
三者可以组合使用。它们作用于不同的流量路径,互不冲突:
流量来源 控制字段 作用范围
--------------------------------------------------------------
外部 (NodePort/LB) externalTrafficPolicy 外部 -> NodePort -> Pod
集群内 (ClusterIP) internalTrafficPolicy Pod -> ClusterIP -> Pod
两者 TopologyAwareHints zone 级优先选择
推荐组合一:Web 服务暴露到公网
apiVersion: v1
kind: Service
metadata:
name: web
annotations:
service.kubernetes.io/topology-mode: Auto
spec:
type: LoadBalancer
externalTrafficPolicy: Local # 保留外部客户端源 IP
internalTrafficPolicy: Cluster # 内部流量全局均匀
selector:
app: web
ports:
- port: 443
targetPort: 8443推荐组合二:节点级日志收集
只需 internalTrafficPolicy: Local(DaemonSet
后端),不需要外部暴露,不需要 TopologyAwareHints。
推荐组合三:多 zone 微服务
internalTrafficPolicy: Cluster +
topology-mode: Auto。不要求严格本地(Pod 不是
DaemonSet),TopologyAwareHints 优先同 zone 路由,Pod
不足时自动回退。
组合速查表
组合 效果
------------------------------------------------------------------------
eTP: Local + iTP: Local 外部和内部都严格本地化,最激进
eTP: Local + iTP: Cluster 外部保留源IP,内部全局均匀(最常见)
eTP: Cluster + iTP: Local 外部不保留源IP(不推荐),内部本地化
eTP: Cluster + TAH: Auto 外部全局,内部 zone 级优先
eTP: Local + iTP: Cluster + TAH: Auto 三者结合,覆盖最全面
(eTP = externalTrafficPolicy, iTP = internalTrafficPolicy, TAH = TopologyAwareHints)
七、实验:对比 Local 和 Cluster 模式下的源 IP 保留
这个实验在 kind 集群中进行,验证
externalTrafficPolicy 对源 IP 的真实影响。
环境准备
# 创建 3 节点的 kind 集群
cat <<'EOF' > kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
EOF
kind create cluster --name svc-test --config kind-config.yaml部署一个返回请求源 IP 的 HTTP 服务:
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo
spec:
replicas: 2
selector:
matchLabels:
app: echo
template:
metadata:
labels:
app: echo
spec:
containers:
- name: echo
image: registry.k8s.io/echoserver:1.10
ports:
- containerPort: 8080kubectl apply -f echo-server.yaml
kubectl wait --for=condition=Ready pod -l app=echo --timeout=60s对比两种模式
分别创建 Cluster 和 Local 模式的 NodePort Service:
# Cluster 模式
kubectl create service nodeport echo-cluster --tcp=80:8080 --node-port=30080 \
--dry-run=client -o yaml | kubectl apply -f -
kubectl patch svc echo-cluster -p '{"spec":{"externalTrafficPolicy":"Cluster","selector":{"app":"echo"}}}'
# Local 模式
kubectl create service nodeport echo-local --tcp=80:8080 --node-port=30081 \
--dry-run=client -o yaml | kubectl apply -f -
kubectl patch svc echo-local -p '{"spec":{"externalTrafficPolicy":"Local","selector":{"app":"echo"}}}'NODE_IP=$(kubectl get nodes -o jsonpath='{.items[1].status.addresses[?(@.type=="InternalIP")].address}')
# Cluster 模式: 源 IP 被 SNAT
curl -s http://${NODE_IP}:30080 | grep -i "client_address"
# 预期: client_address=10.244.x.x (节点 IP)
# Local 模式: 源 IP 保留(从有 Pod 的节点访问)
NODE_WITH_POD=$(kubectl get pods -l app=echo -o jsonpath='{.items[0].status.hostIP}')
curl -s http://${NODE_WITH_POD}:30081 | grep -i "client_address"
# 预期: client_address=<你的真实 IP>对比 iptables 规则
# 进入 kind 的 worker 节点
docker exec -it svc-test-worker bash
iptables-save -t nat | grep "echo-cluster" | head -10
# 关键: 有 KUBE-MARK-MASQ (会做 SNAT)
iptables-save -t nat | grep "echo-local" | head -10
# 关键: 有 KUBE-SVL 链 (只包含本地 Pod, 不做 SNAT)验证 healthCheckNodePort
HEALTH_PORT=$(kubectl get svc echo-local -o jsonpath='{.spec.healthCheckNodePort}')
# 有 Pod 的节点 -> 200
curl -s http://${NODE_WITH_POD}:${HEALTH_PORT}/healthz
# {"localEndpoints":1,"serviceProxyHealthy":true}
# 无 Pod 的节点 -> 503
NODE_WITHOUT_POD=$(kubectl get nodes -o jsonpath='{.items[2].status.addresses[?(@.type=="InternalIP")].address}')
curl -s http://${NODE_WITHOUT_POD}:${HEALTH_PORT}/healthz
# {"localEndpoints":0,"serviceProxyHealthy":true}实验结果汇总
测试项 Cluster 模式 Local 模式
---------------------------------------------------------------
源 IP 节点 IP (SNAT) 真实客户端 IP
无 Pod 节点访问 正常(跨节点转发) 失败(无本地后端)
iptables 链 KUBE-SVC + MARK-MASQ KUBE-SVL (无 MASQ)
healthCheckNodePort 无 自动分配
清理
kubectl delete svc echo-cluster echo-local
kubectl delete deployment echo
kind delete cluster --name svc-test八、生产环境最佳实践
源 IP 保留的替代方案
如果你需要保留源 IP 但又担心 Local 模式的负载不均,还有几个替代方案:
Proxy Protocol:在 LB 和后端之间用 Proxy Protocol 传递源 IP。需要后端应用或 Ingress Controller 支持解析 Proxy Protocol header。
# AWS NLB 启用 Proxy Protocol v2
metadata:
annotations:
service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"X-Forwarded-For:如果前面有 L7
负载均衡器(ALB、Nginx Ingress),源 IP 可以通过 HTTP header
传递,不依赖 externalTrafficPolicy。
Client -> ALB (X-Forwarded-For: client-ip) -> NodePort -> Pod
Pod 从 X-Forwarded-For header 读取真实 IP
kube-proxy 配置调优
# kube-proxy ConfigMap
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: ipvs
ipvs:
scheduler: rr
strictARP: true # MetalLB L2 模式必须开启
nodePortAddresses:
- 10.0.0.0/8 # 限制 NodePort 监听的网络接口strictARP: true 在使用 MetalLB L2
模式时是必须的。默认情况下 Linux 会在所有网络接口上响应 ARP
请求,这会干扰 MetalLB 的 ARP 宣告。
# 验证 strictARP 配置
kubectl -n kube-system get configmap kube-proxy -o yaml | grep strictARP监控指标
kubeproxy_sync_proxy_rules_duration_seconds # kube-proxy 同步耗时
endpointslice_controller_changes # EndpointSlice 变更频率
metallb_bgp_session_up{peer="10.0.0.1"} # MetalLB BGP session 状态常见问题排查速查
# === externalTrafficPolicy 排查 ===
kubectl get pods -l <selector> -o wide # Pod 分布是否合理?
curl <nodeIP>:<healthCheckNodePort>/healthz # 健康检查是否正常?
# === TopologyAwareHints 排查 ===
kubectl get endpointslice -l kubernetes.io/service-name=<svc> -o yaml
# 检查 endpoints[].hints 是否存在;不存在则 controller 判定条件不满足
# === MetalLB 排查 ===
kubectl -n metallb-system logs -l component=speaker # speaker 日志
kubectl get svc <name> -o wide # EXTERNAL-IP 是否分配?附录 A:关键字段速查
spec:
externalTrafficPolicy: Local|Cluster # 外部流量策略(NodePort/LB)
internalTrafficPolicy: Local|Cluster # 内部流量策略(ClusterIP, 1.26 GA)
healthCheckNodePort: 31234 # eTP: Local 自动分配
metadata:
annotations:
service.kubernetes.io/topology-mode: Auto|Disabled # 拓扑感知路由 (1.27 GA)附录 B:版本演进
版本 变更
------------------------------------------------------
1.7 externalTrafficPolicy: Local 进入 GA
1.21 TopologyAwareHints 进入 Alpha
1.22 internalTrafficPolicy 进入 Alpha
1.23 TopologyAwareHints 进入 Beta
1.24 internalTrafficPolicy 进入 Beta
1.26 internalTrafficPolicy 进入 GA
1.27 TopologyAwareHints 进入 GA
topology-mode 注解替代 topology-aware-hints
1.30 EndpointSlice 成为唯一的 endpoint 发现机制
Endpoints API 正式废弃
附录 C:推荐阅读
- Kubernetes 官方文档:Service Traffic Policy
- Kubernetes 官方文档:Topology Aware Routing
- KEP-2086:Service Internal Traffic Policy
- KEP-2433: Topology Aware Hints
- MetalLB 官方文档:Concepts
- 本系列前置:Service 与 kube-proxy、BGP 协议、Calico
系列导航 - 上一篇:Gateway API - 下一篇:DNS 与服务发现 - 相关:Service 与 kube-proxy:ClusterIP 背后的四种实现