「TCP握手成功一次,失败一次,再成功一次。」
DevOps工程师Le Phan Tan Loc在部署RabbitMQ到Amazon EKS时,遇到了这个诡异现象。没有错误日志,没有健康检查告警,端口测试却像抛硬币一样随机。问题最终指向一个默认关闭的AWS功能——跨区负载均衡(Cross-Zone Load Balancing)。
架构设计:省钱的巧妙与隐患
项目需求很明确:每个项目独立部署RabbitMQ集群,外部VPC通过VPC Peering访问AMQP 5672端口。
团队选择了AWS Load Balancer Controller的TargetGroupBinding方案。这个自定义资源(CRD)允许将Kubernetes Service绑定到现有NLB的Target Group,而非为每个集群创建独立NLB。多个项目共享同一台NLB,仅通过不同端口区分——rabbitmq-1用5672,rabbitmq-2用5673,以此类推。
成本省了,但网络路径变复杂了。EKS集群跨多个可用区部署,NLB在每个可用区都有节点(ENI),Target Group则指向Pod的IP地址。当外部流量从VPC Peering(10.21.0.0/16)进入时,路径涉及:NLB节点→跨可用区路由→Pod IP。
故障复现:没有规律的连接失败
测试命令很简单:
bash -c "timeout 2 bash -c 'cat < /dev/tcp//5672'"
结果令人困惑:连续执行10次,可能成功6次,失败4次,无固定模式。Telnet表现相同。NLB控制台显示所有Target健康,RabbitMQ应用日志无异常。
关键线索藏在网络层。用tcpdump抓包发现,失败的连接在TCP三次握手的第二步(SYN-ACK)无响应。但诡异的是,这个SYN-ACK并非来自客户端期望的源地址——NLB的跨区流量转发打破了TCP的状态对称性。
根因分析:跨区负载均衡的默认陷阱
AWS NLB的跨区负载均衡默认关闭。这意味着:NLB节点只会将流量转发到本可用区的Target,即使其他可用区的Target更空闲或更健康。
在TargetGroupBinding架构中,这个问题被放大。Pod IP可能位于可用区A,但客户端连接的NLB节点却在可用区B。由于跨区转发被禁用,流量到达可用区B的NLB节点后,无法路由到可用区A的Pod——连接直接失败。
为什么有时成功?因为NLB有多个IP地址(每个可用区一个),DNS轮询或客户端重试可能换到正确可用区的节点。这种随机性让故障难以定位。
更隐蔽的是健康检查机制。NLB的健康检查从每个可用区独立发起,只要本可用区能连通Target,就标记为健康——即使该Target实际位于其他可用区,正常流量根本无法到达。
解决方案与权衡
修复很简单:在NLB属性中启用跨区负载均衡(Cross-Zone Load Balancing)。AWS控制台、CLI或Terraform均可操作,变更即时生效,无需重建NLB。
但需注意成本影响。跨区流量会产生数据传输费用,在高吞吐量场景下不可忽视。团队评估了RabbitMQ的实际流量规模后,认为成本增量在可接受范围内。
替代方案包括:确保Pod与NLB节点同可用区部署(通过节点亲和性),或为每个可用区创建独立Target Group。但这些方案增加了运维复杂度,与最初"简化架构"的设计目标相悖。
为什么这件事值得记住
这个案例暴露了云原生架构中"默认配置"的隐性风险。AWS NLB的跨区负载均衡设计初衷是减少跨区流量成本,但在Kubernetes + VPC Peering + TargetGroupBinding的组合场景下,默认关闭变成了功能缺陷。
对于在EKS上部署有状态服务的团队,建议将跨区负载均衡检查纳入部署清单。特别是在使用共享NLB、多可用区Pod分布、外部VPC访问这三要素叠加时,这个设置从"可选优化"变为"必需配置"。
排查此类问题的关键,是理解NLB的可用区边界行为与Kubernetes网络模型的交互。当连接失败呈现随机性、无应用层日志、健康检查正常时,优先验证跨可用区的网络路径连通性。
热门跟贴