土法炼钢兴趣小组的算法知识备份

Service 进阶:ExternalTrafficPolicy、拓扑感知路由与流量本地化

目录

上一篇我们拆解了 kube-proxy 四种模式的内核实现,搞清楚了 ClusterIP 的 DNAT 是怎么写进 iptables/IPVS/nftables 的。那篇文章的核心结论是:kube-proxy 把 Service 的虚拟 IP 翻译成后端 Pod IP,流量在内核态完成转发,不经过用户态。

但那只是故事的一半。kube-proxy 默认行为下有两个问题你迟早会遇到:

  1. 源 IP 丢失。外部流量经过 NodePort 进入集群后,如果 kube-proxy 把包转发到另一个节点上的 Pod,它会做一次 SNAT – 把源 IP 改成当前节点的 IP。后端 Pod 看到的源地址是节点 IP 而不是真实的客户端 IP。对于需要做访问控制、审计、地理限流的场景,这是致命的。

  2. 跨节点额外一跳。默认模式下,即使流量到达的节点上没有后端 Pod,kube-proxy 仍然会接受这个包然后转发到另一个节点。这多了一跳网络开销,延迟增加,带宽浪费。

Kubernetes 为这两个问题提供了三层递进的解决方案:externalTrafficPolicyinternalTrafficPolicy、以及拓扑感知路由(TopologyAwareHints)。它们的思路一脉相承 – 让流量尽可能在本地消费

本文要做的事:

  1. 拆解 externalTrafficPolicy: LocalCluster 的真实数据面行为差异
  2. 解释 internalTrafficPolicy: Local 如何实现集群内流量本地化
  3. 深入 TopologyAwareHints 的 EndpointSlice 机制和失效场景
  4. 对比 LoadBalancer 的云厂商实现差异(AWS NLB、GCP ILB、MetalLB)
  5. 深度拆解 MetalLB:L2 模式与 BGP 模式的原理
  6. 讨论三种流量策略的组合使用场景
  7. 实验:对比 Local 和 Cluster 模式下的源 IP 保留行为

本文基于 Kubernetes 1.31, kube-proxy v1.31, Linux 6.x 内核。 实验环境:kind v0.24, MetalLB v0.14, Ubuntu 22.04

ExternalTrafficPolicy Local 与 Cluster 数据流对比

一、ExternalTrafficPolicy:Local vs Cluster

externalTrafficPolicy 是 Service 上的一个字段,只对 NodePortLoadBalancer 类型生效。它决定了外部流量到达节点后,kube-proxy 如何选择后端 Pod。

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local    # 或 Cluster(默认)
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 8080

Cluster 模式(默认):均匀但丢源 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 改变了两件事:

  1. kube-proxy 只把 NodePort 流量 DNAT 到本节点上的后端 Pod
  2. 不做 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 级均衡。

缓解方法:


二、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: 6379

Cluster 模式(默认)

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 类型的后端服务:

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)

externalTrafficPolicyinternalTrafficPolicy 都是粗粒度的二选一:要么全局均匀,要么严格本地。有没有更灵活的方案?

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: TCP

kube-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 的分配算法核心逻辑:

  1. 统计每个 zone 的 CPU 容量比例(基于节点的 allocatable CPU)
  2. 按比例把 endpoint 分配到各 zone
  3. 优先把 endpoint 分配到自己所在的 zone
  4. 如果某个 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 被移除,回退到全局路由:

  1. endpoint 数量不足 – 某个 zone 一个 endpoint 都没有,controller 无法满足分配要求
  2. zone 间负载比例严重失衡 – 预期分配比例超出可接受范围,controller 认为无法保证合理的负载分配
  3. 节点数量过少 – 单 zone 部署,拓扑感知路由没有意义
  4. 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 是互补而非互斥的:


四、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: 8080

NLB 的关键特性:

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 由两个组件构成:

# 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/28

L2 模式: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 模式的限制:

# 查看哪个节点持有哪个 IP
kubectl -n metallb-system logs -l app=metallb,component=speaker | grep "announcing"

# 手动触发 ARP 查看
arping -c 3 192.168.1.240

BGP 模式:真正的负载分散

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 模式的注意事项:

L2 vs BGP 选型

维度              L2 模式                BGP 模式
------------------------------------------------------
部署复杂度        极低(即装即用)        需要路由器配合
网络拓扑要求      同一 L2 广播域          需要 BGP peer
流量分散          单节点(瓶颈)          多节点 ECMP
故障切换速度      2-10 秒                秒级
跨子网            不支持                 支持
推荐场景          小集群、测试环境        生产环境、大流量

六、组合策略:三种流量控制的协作

externalTrafficPolicyinternalTrafficPolicy、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: 8080
kubectl 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:推荐阅读


系列导航 - 上一篇:Gateway API - 下一篇:DNS 与服务发现 - 相关:Service 与 kube-proxy:ClusterIP 背后的四种实现


By .