负载均衡直通 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模式 (直通模式)
- 所有节点都会打开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重启时的流量丢失问题:
- Pod漂移问题:当Ingress Pod重启后可能漂移到其他节点,原节点的NodePort转发会被关闭,但ELB上游列表需要由CCM(云控制器管理器)及时更新
- 竞态条件:kube-proxy和CCM同时监听Endpoint变化,无法保证执行顺序。
竞态场景一:新Pod上线(CCM先于kube-proxy)
- 新Pod在NODE1上创建并就绪。
- CCM监听到变化,迅速将NODE1加入ELB上游组。
- kube-proxy稍慢一步,尚未在NODE1上打开32999端口的转发服务。
- 结果:ELB将流量导向NODE1的31999端口,但该端口尚未准备好接收转发,导致502 Bad Gateway错误。
竞态场景二:旧Pod下线(kube-proxy先于CCM
- 旧Pod在NODE2上被删除,进入Terminating状态。
- kube-proxy监听到变化,立即关闭NODE2上的31999端口服务。
- CCM动作稍慢,还未将NODE2从ELB上游组中摘除。
- 结果: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: 无竞态条件,流量无丢失