在Golang生态中构建百万级WebSocket系统的设计策略

发表时间: 2023-04-10 13:40


go websocket库介绍

Go 有几个流行的 WebSocket 库,可以轻松地向应用程序添加实时通信。让我们来看看一些最常用的库以及它们的比较。

  • net/websocket
    • net/websocket 是 Go 标准库中包含的标准 WebSocket 库。它提供了一个简单的API,用于创建WebSocket服务器和客户端,并支持WebSocket协议和较旧的Hixie 76协议。
    • 使用 net/WebSocket 的一个缺点是它缺少其他库提供的一些高级功能,例如消息压缩和 ping/pong 处理。但是,它仍然是基本WebSocket应用程序的可靠选择。
  • gorilla/websocket
    • gorilla/websocket 是一个流行的 WebSocket 库,它提供了一组丰富的功能和比 net/WebSocket 对开发人员更友好的 API。它支持 WebSocket 协议以及许多扩展,包括消息压缩和每条消息压缩。
    • Gorilla/webSocket 提供了许多其他功能,例如消息分段和消息广播,这使其成为构建实时应用程序的热门选择。
    • 需要注意的是,gorilla/websocket已移至公共存档状态,不再主动维护。虽然该库仍在运行并广泛使用,但它将来可能不会收到更新或错误修复。这意味着库的任何安全漏洞或其他问题可能无法解决。
    • 如果您决定在项目中使用gorilla/websocket,请务必了解此风险并采取措施缓解它。这可能包括:
      • 在将库部署到生产环境之前,对库进行彻底的测试和审核。
      • 密切关注与库相关的安全公告和更新,并准备在必要时切换到备用 WebSocket 库。
      • 考虑使用主动维护并接收定期更新和错误修复的替代 WebSocket 库,例如“gobwas/ws”。
  • gobwas/ws
    • gobwas/ws 是一个轻量级的 WebSocket 库,专为性能和易用性而设计。它支持 WebSocket 协议和有限数量的扩展,并提供用于创建 WebSocket 服务器和客户端的简单 API。
    • 它还得到积极维护,并定期接收更新、安全性和错误修复。
    • 该库还提供其他功能和自定义选项,例如对子协议、ping/pong 消息和自定义 WebSocket 帧处理的支持。
    • 使用它的示例代码如下所示:

使用 WebSocket 时,最大的挑战之一是管理内存利用率。在传统的 HTTP 连接中,每个 HTTP 编写器和读取器通常使用 4KB 的内存。使用 WebSockets,WebSocket HTTP 编写器需要额外的 4KB 内存,每个 goroutine 最多可以使用 8KB 的内存。这意味着对于一百万个连接,内存利用率可以达到 20GB。下图显示了存在的不同组件:

解决方案 1:EPolll

第一个解决方案是使用 Linux 系统调用 Epoll。Epoll 是一种可扩展的 I/O 事件通知机制,可监控 I/O 的多个文件描述符。通过实施 Epoll,内存利用率可以降低大约 30%。以下是使用 Epoll 的示例代码:

解决方案 2:优化缓冲区分配

  • 优化内存利用率的另一种解决方案是使用低级别 API 进行数据包处理和缓冲区,以避免在 I/O 期间进行中间分配。
  • 这种技术由“gobwas/ws”库使用,它也支持零拷贝升级。
  • 通过使用此库,内存利用率最多可降低 60%,从而在 97 万个连接(仅 600 MB)的情况下将内存利用率总共降低 60%。

同步通信

WebSocket 是一种协议,用于客户端和服务器之间通过长期的双向连接进行双向通信。因此,WebSocket 设计用于异步通信,其中服务器和客户端都可以随时相互发送消息。但是,可能存在需要同步请求/响应通信的用例。例如:一个服务器向客户端发送请求,需要立即响应另一个消费者服务器。

实现与 WebSocket 同步通信的一种方法是对每个请求消息使用唯一标识符,例如 UUID。服务器可以向客户端发送包含唯一标识符的请求消息以及任何其他必要的数据。然后,客户端可以处理请求,并使用相同的唯一标识符将响应消息发送回服务器。然后,服务器可以使用此标识符将响应消息与原始请求进行匹配,并相应地处理响应。

在 Go 中,通道可用于实现同步请求/响应方法,即使服务器发起请求也是如此。服务器可以为每个请求创建一个通道,并使用 UUID 作为通道的密钥。当服务器向客户端发送请求时,它可以创建具有唯一 UUID 的通道,通过 WebSocket 连接发送请求,并在通道上阻止,直到收到响应。同时,客户端可以侦听传入的请求,处理请求,并使用与原始请求相同的 UUID 通过 WebSocket 连接发送回响应。当服务器收到具有匹配 UUID 的响应时,它可以取消阻止通道并继续处理响应。下图详细介绍了此流程:

扩展性

WebSocket 服务器可以水平扩展,通过采用允许动态服务器生成和服务器注册表的体系结构来处理大量客户端连接。一种可能的方法是使用 AWS Auto Scaling Groups (ASG) 根据连接需求动态生成新服务器。然后,每个生成的服务器都可以向 AWS Route 53 DNS 注册自身,以确保客户端可以连接到它。

为了维护连接的客户端及其连接到的服务器的状态,可以在MySQL数据库之上实现服务器注册表。此注册表可以跟踪每个客户端连接到哪个服务器,并可用于将传入消息路由到正确的服务器。

当客户端发起连接请求时,可以将请求发送到负载均衡器,负载均衡器可以将请求分发到可用服务器之一。然后,服务器可以检查服务器注册表以查看客户端是否已连接到另一台服务器,如果是,则将连接路由到相应的服务器。如果客户端尚未连接,服务器可以将客户端的连接添加到注册表,并开始处理传入的消息。

如果服务器过载,ASG 可以自动生成新服务器来处理额外的负载,并且可以相应地更新服务器注册表以反映客户端应连接到的新服务器。

结论

在这篇文章中,我们探讨了在扩展 WebSocket 以进行涉及持久连接的大规模通信时面临的挑战和可能的解决方案。

我们深入研究了WebSockets的Go(Golang)生态系统,讨论了各种可用的库以及它们的比较。

通过实施优化的缓冲区分配和使用 EPoll,我们将内存利用率降低了多达 97%。此外,我们还介绍了同步通信的需求,以及如何在 Go 中使用消息 UUID 和通道来实现同步通信。

最后,我们研究了如何水平扩展系统,方法是使用 AWS ASG 动态生成服务器,维护基于 MySQL 构建的服务器注册表,并使用 AWS Route53 DNS 启用新服务器的动态注册。

总体而言,通过正确的架构设计和实现,WebSocket 可以有效地扩展,与传统的 TCP 连接相比,占用空间很小,并允许数百万规模的实时通信。