负载均衡100,000个WebSocket连接:HAProxy、Nginx与自定义方案的对比
通知在凌晨2点47分传来。我们的实时聊天应用程序在80,000个并发WebSocket连接的压力下不堪重负,用户遇到消息丢失和连接超时的问题。到早上,我们需要可靠地处理100,000多个连接。这就是我们如何解决这个问题的故事,以及在大规模WebSocket负载均衡方面来之不易的经验教训。
问题:当简单的解决方案失效时
传统的HTTP负载均衡看似简单——将请求分发到各个服务器,就大功告成了。而WebSocket连接则完全是另一回事。它们具有持久性、有状态性和粘性。当用户连接时,在整个会话期间(有时一次长达数小时),他们会一直保持该连接。
我们最初的设置简单得有些离谱:仅用一个Node.js服务器来处理所有连接。对于最初的10000名用户,它运行得非常出色。但到了50000名用户时,我们开始察觉到内存压力。而到了80000名用户时,整个系统就变得不稳定了。
警钟敲响是在我们最大的客户——一家金融交易平台——在开市期间无法获取实时价格更新之时。系统每宕机一秒,就意味着实实在在的资金损失。
第一轮:HAProxy(老牌可靠工具)
HAProxy似乎是显而易见的选择。它久经考验,每天在互联网上处理数百万个连接,并且对WebSocket有出色的支持。我们的配置很简单:
frontend websocket_frontend
bind *:80
default_backend websocket_servers
backend websocket_servers
balance source
server ws1 10.0.1.10:3000 check
server ws2 10.0.1.11:3000 check
server ws3 10.0.1.12:3000 check
balance source
指令可确保来自同一IP地址的连接始终路由到同一后端服务器,这对于维护WebSocket会话状态至关重要。
结果:
- 成功处理了10万多个并发连接
- 我们的负载均衡器上的CPU使用率保持在15%以下
- 内存占用出奇地小(约2GB)
- 连接建立时间:平均约45毫秒
注意事项: 当用户从公司的网络地址转换(NAT)出网连接时,基于源IP的HAProxy会话持久性出现了问题。我们会看到数千个连接都集中到一台后端服务器上,而其他服务器则处于闲置状态。
解决方案需要切换到基于cookie的会话持久性,并修改我们的WebSocket握手以包含会话令牌。这增加了复杂性,但使负载分配更加均匀。
第二轮:Nginx(瑞士军刀)
Nginx的吸引力在于它的通用性。我们已经在使用它来处理HTTP流量,因此整合负载均衡似乎是顺理成章的。配置需要使用 stream
模块:
stream {
upstream websocket_backend {
ip_hash;
server 10.0.1.10:3000;
server 10.0.1.11:3000;
server 10.0.1.12:3000;
}
server {
listen 80;
proxy_pass websocket_backend;
proxy_timeout 1s;
proxy_responses 1;
}
}
结果:
- 处理了10万个连接,仍有剩余空间
- 与我们现有基础设施更好地集成
- 内置健康检查和故障转移
- 连接建立时间:平均约52毫秒
意外发现:在相同的工作负载下,Nginx 消耗的内存比 HAProxy 显著更多,大约多出 40%。我们在任何地方都没有找到相关记录,但我们的监测结果清晰地显示了这一点。每增加10,000个并发连接,Nginx 比 HAProxy 大约多使用800MB内存。
ip_hash
指令比HAProxy的默认源负载均衡提供了更好的分配效果,但我们仍然遇到了 NAT 的问题。
第三轮:定制解决方案(终极手段)
在解决了HAProxy和Nginx中的会话持久化问题后,我们决定构建自己的支持WebSocket的负载均衡器。这个想法很简单:我们不再依赖基于IP的路由,而是实现基于用户会话的应用层路由。
使用Go语言,我们构建了一个轻量级代理,该代理:
- 拦截WebSocket握手
- 提取用户身份验证令牌
- 根据用户ID的一致性哈希对连接进行路由
- 维护用户到服务器分配的实时映射
type ConnectionRouter struct {
servers []string
userMap sync.Map
hashRing *consistent.Map
}
func (cr *ConnectionRouter) RouteConnection(userID string) string {
return cr.hashRing.Get(userID)
}
结果:
- 完美的负载分配 —— 不再有NAT问题
- 连接建立时间:平均约38毫秒
- 三种解决方案中最低的内存使用量
- 内置连接跟踪与分析
成本:开发时间相当长——初始版本花了三周时间,还有持续的维护工作,以及维护关键基础设施的责任。每个漏洞都成了我们需要解决的问题。
性能对比:数据
在对这三种解决方案运行相同的工作负载后:
隐性成本
除了原始性能数据外,每种解决方案还带来了不同的运营负担:
- 要深入理解HAProxy,就必须熟悉其配置语法。相关文档很出色,但学习难度较大。调试连接问题时,常常需要深入研究HAProxy日志,并将其与应用程序日志关联起来。
- nginx 让人感觉似曾相识,但与HTTP相比,它在WebSocket处理方面存在细微差异。流模块的配置文档比HTTP模块少,这导致需要反复试验来调试。
- 定制解决方案让我们拥有完全的控制权,但也让我们要对未曾考虑到的极端情况负责。处理连接超时、服务器故障和正常关闭等问题需要仔细思考和测试。
现实世界的经验教训
三个月后,以下是在生产过程中真正重要的因素:
- 监控就是一切。 无论你选择哪种负载均衡器,你都需要了解连接数、分布情况和健康检查。我们构建了自定义仪表板,用于展示实时连接映射和服务器负载情况。
- 为故障做好规划。 这三种解决方案都能处理服务器故障,但它们的表现有所不同。HAProxy和Nginx会立即重定向新连接,但可能会中断现有连接。我们的定制解决方案可以平稳地迁移连接,但这需要额外的复杂操作。
- 网络拓扑比你想象的更重要。 企业防火墙、NAT 配置和代理服务器对 WebSocket 连接的影响与对 HTTP 请求的影响截然不同。要使用真实用户网络进行测试,而不仅仅是在开发环境中测试。
建议
对于大多数面临这一挑战的团队来说,可以从HAProxy入手。它是高连接数场景下久经考验的解决方案,有出色的文档资料,并且拥有庞大的社区。其配置的复杂性与其可靠性相比是值得的。
如果您已经深度融入Nginx生态系统,并且能够接受较高的内存使用量,那么可以考虑使用Nginx。管理单一类型的负载均衡器在操作上的简便性,其优势可能超过资源成本。
只有在你有HAProxy和Nginx都无法满足的特定需求、强大的开发团队以及维护自定义基础设施的资源时,才构建自定义解决方案。性能提升是实实在在的,但这是以高昂的运营成本为代价的。
六个月后
我们的HAProxy解决方案已在生产环境中运行了六个月,在重大市场活动期间能够处理180,000个并发连接的峰值负载。凌晨2点47分的警报已经停止,而且我们的交易平台客户续签了合同。
有时候,那些看似乏味却行之有效的解决方案才是正确的选择。但了解其他替代方案及其利弊,能让我们成为更出色的工程师,帮助我们做出明智的决策,而不仅仅是照着教程去做。
下次你面临类似的扩展难题时,请记住,最佳解决方案并不总是技术上最令人赞叹的那个。而是你的团队能够在生产环境中可靠地实施、监控和维护的方案。
原文:https://medium.com/@yashbatra11111/load-balancing-100-000-websocket-connections-haproxy-vs-nginx-vs-custom-4fe78f68c1ce