当与那些还没有使用过WebSockets的开发人员交谈时,他们通常会有同样的担忧:如何将它扩展到多个服务器上?
发布到一台服务器上的通道是可以的,前提是所有订阅者都连接到那台服务器。一旦您有多个服务器,您就需要添加一些其他的东西。这就是这篇文章试图解决的问题。
缩放HTTP vs WebSockets
要了解为什么扩展WebSockets似乎令人生畏,让我们将其与HTTP进行对比,因为大多数人都很了解它。
使用HTTP,您有一个once off请求/应答模式,您不期望客户机的下一个请求返回到相同的服务器。至少您不应该这样做,因为这意味着您有一个棘手的会话问题,并且您不能轻易地向外扩展以获得性能或冗余。
使用HTTP,您可以在负载均衡器后运行几乎无限数量的web服务器实例。当请求进入时,负载平衡器将请求传递给健康的web服务器实例,并且在web服务器计算完响应后将其传递回客户机。HTTP连接的生命周期通常很短,它们只存在到给出响应为止。这是一种很容易理解的、普遍存在的方法,而且扩展性很好。在长轮询中有一个例外,但它并不常见,对本文也不重要。
另一方面,WebSockets与HTTP请求的区别在于它们是持久的。WebSocket客户端打开一个到服务器的连接并重用它。在这个长时间运行的连接上,服务器和客户机都可以发布和响应事件。这个概念称为双工连接。可以通过负载均衡器打开连接,但一旦打开连接,它就会一直与同一服务器在一起,直到关闭或中断。
这意味着交互是有状态的;对于每个打开的客户端连接,您最终将至少在WebSocket服务器的内存中存储一些数据。例如,您可能知道哪个用户位于套接字的客户端,以及用户感兴趣的是什么类型的数据。
WebSocket连接的持久性使得它在实时应用中如此强大,但这也使得它难以扩展。
一个WebSocket应用程序示例
让我们来讨论一个示例应用程序,这样我们就可以在更具体的上下文中讨论问题和方法。
以我们为例,让我们确定一个协作白板应用程序。在这个应用程序中,有多个白板,这意味着人们可以协作绘制多个草图。当一个用户在一个特定的白板上作画时,它通过一个WebSocket连接发布坐标,并通过WebSocket连接发布到打开相同白板的所有其他用户。换句话说,我们在WebSockets上公开了一个pub/sub模式。
在本例中,这意味着应用程序的每个用户的套接字连接的服务器端至少需要知道用户打开了什么白板。
Web套接字实现,如套接字。io有通道的概念。可以将其视为客户端订阅的地址,服务或其他客户端发布到该地址。
它可能容易认为所有我们需要构建协作白板应用是采用渠道(每个白板都有它自己的通道),然后坐下来放松一下,但你将会看到在这篇文章中,你仍然有问题扩展和容错。
您需要一个发布/订阅代理
首先,我说的“pub/sub broker”是什么意思?有各种各样的技术在相当大的规模上支持发布/订阅模式。
当你需要在套接字上扩展发布/订阅架构时,你需要找到一个好的发布/订阅技术来作为你的解决方案的核心。
我们不需要为这篇文章确定一个特定的选项,但这里有一些不错的选择:Redis, RabbitMQ, Kafka,和RethinkDB。
为了了解为什么我们需要添加一个pub/sub代理来帮助你扩展你的WebSockets,让我们先以一个服务器为背景来考虑我们的例子。
对于一个服务器来说,用WebSockets构建一个发布/订阅服务其实很容易。这是因为在一台服务器上,服务器将知道所有客户机以及客户机感兴趣的数据。
考虑一下我们的示例应用程序。当客户端发送绘图的坐标时,我们只需找到绘图的正确通道,并将对绘图所做的更新发布到该通道。所有的客户端都连接到一台服务器上,因此它们都会得到更改的通知。这有点像内存中的pub/sub。
但在现实中,我们希望跨多个服务器扩展,我们这么做有两个原因:1)共享处理能力,2)冗余。
那么我们怎样才能确保我们的应用扩展?嗯,我们需要一些方法让其他与连接的客户端服务知道数据已经改变。
在构建这样一个应用程序时,你可能已经有了一个数据库,甚至在你开始考虑扩展之前。你不会仅仅信任连接的客户来存储所有图纸的数据。不,您将希望在绘图数据从客户端传入时持久保存它,以便在用户打开绘图时随时提供绘图数据。
但问题来了。如果服务器a上的WebSocket写入一些数据到数据库,服务器B上的WebSocket如何知道去获取数据库的最新数据以便通知它的客户端新的数据?
让我们谈谈在解决方案的中心使用Redis的过程。尽管您的集群中可能有数百个WebSocket服务器,让我们假设您只有3个服务器,这样可以使事情更简单一些。我们将把这些服务器称为WS1、WS2和WS3。有时我会用我的创意名字来给自己取个惊喜!
好,假设你有9个人打开了一幅特定的画,画的是一只狗骑着一匹小马驹骑着一只恐龙,id为abc123保存在你的数据库中。假设有3个人连接到集群中的每个服务器(WS1、WS2、WS3)。
一个连接到WS1的用户在白板上画一些东西。在您的WebSocket服务器逻辑中,您写入数据库,以确保更改已经被持久化,然后根据与绘图相关联的唯一标识符(很可能是基于绘图的数据库id)发布到一个通道。在这个例子中,我们假设通道名是drawing_abc123。
在这一点上,你已经把数据安全地写入数据库,并且你已经发布了一个事件到你的pub/sub代理(Redis频道),通知其他有新数据的相关方。
因为你有用户连接到其他WebSocket服务器(WS2, WS3),对同一个绘图感兴趣,他们将在drawing_abc123通道上开放订阅Redis。他们得到事件的通知,并且每个服务器查询DB更新,并在你的WebSocket层使用的WebSocket通道上发出它。
您可以看到,发布/订阅代理用于允许您通过扩展的WebSocket集群公开发布/订阅模型。
处理故障转移
使用pub/sub代理来协调WebSockets的另一个好处是,现在可以轻松处理故障转移。
当一个客户端连接到一个WebSocket服务器时,该服务器崩溃了,客户端可以通过负载平衡器打开一个连接到另一个WebSocket服务器。新的WebSocket服务器将确保对WebSocket客户端感兴趣的数据的发布/订阅代理有一个订阅,并在WebSocket发生变化时通过管道传输。
使用增量
当客户端重新连接时,需要考虑的一件事是使客户端足够智能,通过某种数据同步偏移量(可能以时间戳的形式)发送,以便服务器不会再次发送所有数据。
如果对图纸的每次更新都打上了时间戳,那么客户端可以很容易地存储他们收到的最新时间戳。当客户端失去了连接到一个特定的服务器,它可以连接到你的websocket集群(通过你的负载平衡器)通过在过去的时间戳,收到这样的查询DB只能建立,这样它会返回更新后出现客户端最后成功收到更新。
在应用程序的负载中,担心副本传到客户机可能并不重要。但即便如此,使用时间戳方法来节省资源和用户的带宽也是一个好主意。
结论
构建运行在一台服务器上的发布/订阅服务相对简单。挑战在于构建一个可以水平伸缩以实现负载共享和容错的服务。
当您向外扩展时,您需要一种方法让web套接字服务订阅已更改的数据,因为对所述数据的更改也将来自其他服务器而不是自身。支持实时查询的数据库非常适合用于此目的,例如RethinkDB。这样你只有WebSockets和你的DB。也就是说,你可能已经在你的环境中使用了支持pub/sub的技术(Redis, RabbitMQ, Kafka),这将比在混合中引入一种新的DB技术更容易销售。