负载均衡直通 Pod时的流量问题

32 阅读4分钟

负载均衡直通 Pod时的流量问题

一、场景背景

假设我们有一个3节点的K8s集群:

  • 节点:NODE1、NODE2、NODE3
  • 运行2副本Ingress Pod(Istio Ingress Gateway),分别在NODE1和NODE2上; 暴露NodePort类型Service,端口为32999
  • 运行2个副本的Application Pod,分别运行在NODE2和NODE3上;暴露ClusterIP类型Service,端口为8080
  • 流量入口为云厂商ELB,后端指向集群节点IP

二、externalTrafficPolicy 核心模式对比

K8s Service的externalTrafficPolicy参数决定了外部流量的转发策略,主要有两种模式:

Cluster模式(默认)

  • 所有节点都会暴露NodePort端口
  • 流量到达任意节点后,会通过IPVS/iptables再次负载均衡到所有Ingress Pod(可能跨节点转发)
  • 优点:高可用,任意节点都能接收流量
  • 缺点:额外的NAT转发增加延迟,破坏ELB的负载均衡策略
flowchart LR
    subgraph 集群外部
        CLIENT[客户端] --> ELB[负载均衡器]
    end
    
    subgraph Kubernetes集群
        subgraph NODE1[节点1]
            NP1[NodePort 32999]
            IPVS1[IPVS/iptables]
            PODIngress1[Ingress Pod 1]
            NP1 --> IPVS1 --> PODIngress1
            
        end
        
        subgraph NODE2[节点2]
            NP2[NodePort 32999]
            IPVS2[IPVS/iptables]
            PODIngress2[Ingress Pod2]
            PODAPP1[Application POD 1]
            NP2 --> IPVS2 --> PODIngress2
        end
        
        subgraph NODE3[节点3]
            NP3[NodePort 32999]
            IPVS3[IPVS/iptables]
            PODAPP2[Application POD 2]
            NP3 --> IPVS3 --> PODIngress1
            IPVS3 --> PODIngress2
            
        end
    end
    
    ELB -->|流量1| NP1
    ELB -->|流量2| NP2
    ELB -->|流量3| NP3
    
    PODIngress2 -->|路由到| PODAPP1
    PODIngress2 -->|路由到| PODAPP2
    
    style CLIENT fill:#f9f,stroke:#333,stroke-width:2px
    style ELB fill:#bbf,stroke:#333,stroke-width:2px
    style PODIngress1 fill:#bfb,stroke:#333,stroke-width:2px
    style PODIngress2 fill:#bfb,stroke:#333,stroke-width:2px
    style PODAPP1 fill:#ff9,stroke:#333,stroke-width:2px
    style PODAPP2 fill:#ff9,stroke:#333,stroke-width:2px

Local模式 (直通模式)

参考 在 TKE 上使用负载均衡直通 Pod

  • 所有节点都会打开NodePort端口(占用),但仅在运行后端Pod的节点提供转发服务
  • 流量仅转发到本机Pod,不会跨节点转发
  • 优点:减少NAT转发,保留客户端源IP,符合ELB加权策略
  • 缺点:Pod漂移后会导致原节点流量失效
flowchart LR
    subgraph 集群外部
        CLIENT[客户端] --> ELB[负载均衡器]
    end
    
    subgraph Kubernetes集群
        subgraph NODE1[节点1]
            NP1[NodePort 32999]
            PODIngress1[Ingress Pod 1]
            NP1 --> PODIngress1
        end
        
        subgraph NODE2[节点2]
            NP2[NodePort 32999]
            PODIngress2[Ingress Pod2]
            PODAPP1[Application POD 1]
            NP2 --> PODIngress2
        end
        
        subgraph NODE3[节点3]
            direction LR
            NP3[NodePort 32999]
            PODAPP2[Application POD 2]
        end
    end
    
    ELB -->|流量1| NP1
    ELB -->|流量2| NP2
    
    PODIngress2 -->|路由到| PODAPP1
    PODIngress2 -->|路由到| PODAPP2
    
    style CLIENT fill:#f9f,stroke:#333,stroke-width:2px
    style ELB fill:#bbf,stroke:#333,stroke-width:2px
    style PODIngress1 fill:#bfb,stroke:#333,stroke-width:2px
    style PODIngress2 fill:#bfb,stroke:#333,stroke-width:2px
    style PODAPP1 fill:#ff9,stroke:#333,stroke-width:2px
    style PODAPP2 fill:#ff9,stroke:#333,stroke-width:2px
    style NODE3 fill:#fdd,stroke:#333,stroke-width:2px

三、Local 模式下的痛点

Local模式虽然性能更优,但存在Pod重启时的流量丢失问题:

  1. Pod漂移问题:当Ingress Pod重启后可能漂移到其他节点,原节点的NodePort转发会被关闭,但ELB上游列表需要由CCM(云控制器管理器)及时更新
  2. 竞态条件:kube-proxy和CCM同时监听Endpoint变化,无法保证执行顺序。

竞态场景一:新Pod上线(CCM先于kube-proxy)

  1. 新Pod在NODE1上创建并就绪。
  2. CCM监听到变化,迅速将NODE1加入ELB上游组。
  3. kube-proxy稍慢一步,尚未在NODE1上打开32999端口的转发服务。
  4. 结果:ELB将流量导向NODE1的31999端口,但该端口尚未准备好接收转发,导致502 Bad Gateway错误。

竞态场景二:旧Pod下线(kube-proxy先于CCM

  1. 旧Pod在NODE2上被删除,进入Terminating状态。
  2. kube-proxy监听到变化,立即关闭NODE2上的31999端口服务。
  3. CCM动作稍慢,还未将NODE2从ELB上游组中摘除。
  4. 结果:ELB仍将流量导向NODE2,但端口服务已关闭,导致连接被拒绝。由于kube-proxy执行通常更快,此问题几乎必然发生

四、解决方案

1. 云控制器管理器(CCM)

  • 监听Endpoint变化,自动更新ELB上游列表
  • 确保ELB上游始终包含运行Ingress Pod的节点
  • 注意:需要在CCM中添加延迟,确保kube-proxy完成转发规则更新后再修改ELB上游

2. ProxyTerminatingEndpoints(K8s 1.22+)

  • 从K8s 1.24版本开始进入Beta阶段,1.26+默认启用
  • 允许kube-proxy继续转发流量到处于Terminating状态但仍就绪的Pod
  • 确保在CCM移除节点前,原节点仍能处理流量
# kube-proxy ConfigMap配置(K8s 1.22-1.xx)
apiVersion: v1
kind: ConfigMap
metadata:
  name: kube-proxy
  namespace: kube-system
data:
  config.conf: |
    apiVersion: kube-proxy.config.k8s.io/v1alpha1
    kind: KubeProxyConfiguration
    featureGates:
      ProxyTerminatingEndpoints: true

3. PreStop钩子

  • 让Pod在收到Term信号前,执行preStop。 触发时机早于 Pod 从服务端点(Service Endpoints)中被移除的后续流程(确保流量不再导入后,应用有时间收尾)确保CCM完成ELB上游更新后再关闭NodePort端口服务
  • 需要配合terminationGracePeriodSeconds(宽限期)参数调整,preStop执行的时间是包含在宽限期中的。若宽限期设为 60 秒,PreStop 脚本执行了 40 秒,那么应用还有 20 秒时间处理 SIGTERM 信号后的收尾;若 PreStop 执行了 70 秒,那么 60 秒时 Pod 会被SIGKILL。
lifecycle:
  preStop:
    exec:
      command: ["/bin/sleep", "10"]

改进后的流程

sequenceDiagram
    participant ELB
    participant CCM
    participant KubeProxy
    participant Kubelet
    
    Note over ELB,Kubelet: 启用ProxyTerminatingEndpoints后的流程
    Kubelet->>Kubelet: 在Node3创建新Pod并就绪
    KubeProxy->>KubeProxy: 开启Node3的32999转发
    CCM->>ELB: 将Node3加入ELB上游
    Note over ELB: 上游组: Node1,Node2,Node3
    
    Kubelet->>Kubelet: 删除Node1的Pod,进入Terminating状态
    Note over KubeProxy: 保持Node1的32999转发直到Pod就绪检查失败
    CCM->>ELB: 从ELB上游移除Node1
    KubeProxy->>KubeProxy: 关闭Node1的32999转发
    Note over ELB: 上游组: Node2,Node3
    Note over KubeProxy,CCM: 无竞态条件,流量无丢失