开发人员没有为服务器端事件做好准备的一个问题
作者:互联网
在分布式架构中,一切都变得更加复杂——我们应该预见到它的到来。
我们刚刚完成了全新功能的开发,使用OpenAI 的技术为客户的视频生成字幕。该架构令人印象深刻,具有微服务、事件队列和工作线程——足以让任何开发人员感到自豪。我们唯一不自豪的是在浏览器中使用标准轮询系统每 2 秒检查字幕是否准备好。
它有效,但它可以更有效,并且有更好的解决方案。
这就是服务器端事件或网络套接字发挥作用的时候。
它们都允许浏览器和服务器之间的连接保持活动状态,以便数据可以流动而无需进行其他查询 - 太棒了!它们都可以与一个浏览器和一个服务器实例完美配合。尽管如此,当您希望应用程序能够扩展时,您可能会拥有一个具有唯一端点和其背后的多个后端实例的负载均衡器。
这就是问题开始出现的地方。从浏览器到后端的连接不在负载均衡器上;它位于服务器实例之一上。
假设您的用户连接到SSE并进行需要异步进程的调用,例如使用工作线程发送队列中的事件。
一旦worker完成,它需要通知连接到用户浏览器的精确服务器实例;当您只能调用负载均衡器时,如何定位正确的目标?
在今天的文章中,我想利用我作为技术主管在高度分布式系统上处理服务器端事件的直接经验来分享我们面临的问题以及我们用来克服这些问题的解决方案。
什么是服务器端事件以及它们如何工作
服务器端事件是鲜为人知的浏览器功能,而不是其更成功的表亲:Websockets。
它们都使用相同的技术来允许浏览器保持浏览器和服务器之间的连接。
它不是进行直接获取答案并关闭连接的 HTTP 调用,而是在收到服务器响应后仍保持连接处于活动状态,以便浏览器和服务器可以继续通信。
WebSocket 和服务器端事件之间的区别是:
对于Websockets来说,交换是双向的;浏览器和服务器都可以互相发送事件。它非常适合交互式聊天之类的事情。
服务器端事件是单向的,只有服务器可以将事件发送到浏览器。例如,对于加载程序来说,更新正在进行的操作的百分比是最佳的。
显示浏览器和服务器之间交互的小序列图。
我们认为这项技术及其实施方式相对简单。
简化的分布式架构
代表我们字幕生成系统的简化版本的架构模式
在此架构中,我简化了架构以消除噪音。我们可以看到当我们的一位用户要求为其一个视频提供新字幕时会发生什么。
浏览器只知道负载均衡器的端点,所以这就是它的调用方式。
负载均衡器随机选择一个后端实例,然后后端在字幕生成队列中发送一条消息。
正在收听新消息的字幕工作人员开始处理视频并使用 OpenAI 转录来获取视频文本,然后将内容存储在数据库中。
我还没有特意谈论服务器端事件。对于简单的轮询系统来说,这就足够了。
表示来自浏览器的轮询系统的架构
浏览器可以定期发送 HTTP 调用来检查字幕是否准备就绪;任何后端实例都可以被调用来检查数据库并回答——故事结束了。
但这并不有趣;我们不想进行民意调查。我们希望服务器在准备就绪时通知浏览器。
因此,如果我们再次查看第一个模式,在生成字幕的整个过程结束后(该模式从底部开始)。
显示了解要调用哪个服务器实例的困难的架构
SSE连接位于精确的后端实例和浏览器之间。负载均衡器如何知道对于来自字幕工作器的调用,它需要选择实例二而不是另一个实例?
我们的思考过程和解决方案。
关于如何解决这个问题,我们有很多想法——但大多数都很糟糕。
我们的问题是:“我们如何通知正确的后端实例用户正在等待的进程已完成。”
我们想到实施的第一个解决方案是将链接到字幕 ID 的服务器实例的 IP 地址存储在外部缓存中。这样,当工作进程完成生成字幕时,我们可以获得 IP 列表(如果多个用户正在等待结果)并通知所有实例已完成。
从理论上讲,这可行,但存在一些问题。
服务器实例自动伸缩;协调器可以随时终止服务器,即使连接正在进行。当连接丢失时,服务器端事件会尝试自动重新连接,以便可以毫无问题地重新连接到另一台服务器。
我们仍然需要引入一种方法来清理缓存以删除服务器的 IP,但现在该 IP 已经消失了。此外,如果进程在此时完成并在存储新 IP 之前尝试调用旧 IP,它也会丢失。
总而言之,必须有更好的解决方案。
我们的工作解决方案
活动!自从这个问题开始以来,我们就已经有了解决方案。
一旦字幕工作人员完成处理,他必须将事件发送到特定队列。出于方便,我们决定使用 Redis Pub/Sub,因为它已经可用并且可以在我们的系统中运行,但它可以与其他队列服务一起实现。
中间有 redis pub/sub 的架构的完整架构
当用户要求为其视频创建字幕时,我们会为此字幕 ID 创建一个新队列(频道)并订阅它。只有打开 SSE 连接的服务器实例才会订阅该通道。
如果另一个用户在该过程正在进行时登陆该页面,并在另一个实例中注册到服务器端事件。该实例还将订阅现有频道。
一旦字幕工作器处理完字幕,它所要做的就是向 Redis 通道发送一个该字幕 ID 的事件。所有注册到它的服务器实例将能够通知仍然连接到服务器端事件的用户。
假设其中一台服务器由于自动降级而被终止。在这种情况下,浏览器将自动重新连接,重新连接后,服务器将订阅该频道(如果尚未订阅)。
如此简单的任务最初让我们视野狭隘,试图在字幕工作器和主后端之间进行直接调用,这几乎让我们放弃了服务器端事件。
我们只需要退后一步,意识到事件从一开始就是答案——它们在我们的应用程序中无处不在,而且它太大了,我们看不到。
我们刚刚完成了全新功能的开发,使用OpenAI 的技术为客户的视频生成字幕。该架构令人印象深刻,具有微服务、事件队列和工作线程——足以让任何开发人员感到自豪。我们唯一不自豪的是在浏览器中使用标准轮询系统每 2 秒检查字幕是否准备好。
它有效,但它可以更有效,并且有更好的解决方案。
这就是服务器端事件或网络套接字发挥作用的时候。
它们都允许浏览器和服务器之间的连接保持活动状态,以便数据可以流动而无需进行其他查询 - 太棒了!它们都可以与一个浏览器和一个服务器实例完美配合。尽管如此,当您希望应用程序能够扩展时,您可能会拥有一个具有唯一端点和其背后的多个后端实例的负载均衡器。
这就是问题开始出现的地方。从浏览器到后端的连接不在负载均衡器上;它位于服务器实例之一上。
假设您的用户连接到SSE并进行需要异步进程的调用,例如使用工作线程发送队列中的事件。
一旦worker完成,它需要通知连接到用户浏览器的精确服务器实例;当您只能调用负载均衡器时,如何定位正确的目标?
在今天的文章中,我想利用我作为技术主管在高度分布式系统上处理服务器端事件的直接经验来分享我们面临的问题以及我们用来克服这些问题的解决方案。
什么是服务器端事件以及它们如何工作
服务器端事件是鲜为人知的浏览器功能,而不是其更成功的表亲:Websockets。
它们都使用相同的技术来允许浏览器保持浏览器和服务器之间的连接。
它不是进行直接获取答案并关闭连接的 HTTP 调用,而是在收到服务器响应后仍保持连接处于活动状态,以便浏览器和服务器可以继续通信。
WebSocket 和服务器端事件之间的区别是:
对于Websockets来说,交换是双向的;浏览器和服务器都可以互相发送事件。它非常适合交互式聊天之类的事情。
服务器端事件是单向的,只有服务器可以将事件发送到浏览器。例如,对于加载程序来说,更新正在进行的操作的百分比是最佳的。
显示浏览器和服务器之间交互的小序列图。
我们认为这项技术及其实施方式相对简单。
简化的分布式架构
代表我们字幕生成系统的简化版本的架构模式
在此架构中,我简化了架构以消除噪音。我们可以看到当我们的一位用户要求为其一个视频提供新字幕时会发生什么。
浏览器只知道负载均衡器的端点,所以这就是它的调用方式。
负载均衡器随机选择一个后端实例,然后后端在字幕生成队列中发送一条消息。
正在收听新消息的字幕工作人员开始处理视频并使用 OpenAI 转录来获取视频文本,然后将内容存储在数据库中。
我还没有特意谈论服务器端事件。对于简单的轮询系统来说,这就足够了。
表示来自浏览器的轮询系统的架构
浏览器可以定期发送 HTTP 调用来检查字幕是否准备就绪;任何后端实例都可以被调用来检查数据库并回答——故事结束了。
但这并不有趣;我们不想进行民意调查。我们希望服务器在准备就绪时通知浏览器。
因此,如果我们再次查看第一个模式,在生成字幕的整个过程结束后(该模式从底部开始)。
显示了解要调用哪个服务器实例的困难的架构
SSE连接位于精确的后端实例和浏览器之间。负载均衡器如何知道对于来自字幕工作器的调用,它需要选择实例二而不是另一个实例?
我们的思考过程和解决方案。
关于如何解决这个问题,我们有很多想法——但大多数都很糟糕。
我们的问题是:“我们如何通知正确的后端实例用户正在等待的进程已完成。”
我们想到实施的第一个解决方案是将链接到字幕 ID 的服务器实例的 IP 地址存储在外部缓存中。这样,当工作进程完成生成字幕时,我们可以获得 IP 列表(如果多个用户正在等待结果)并通知所有实例已完成。
从理论上讲,这可行,但存在一些问题。
服务器实例自动伸缩;协调器可以随时终止服务器,即使连接正在进行。当连接丢失时,服务器端事件会尝试自动重新连接,以便可以毫无问题地重新连接到另一台服务器。
我们仍然需要引入一种方法来清理缓存以删除服务器的 IP,但现在该 IP 已经消失了。此外,如果进程在此时完成并在存储新 IP 之前尝试调用旧 IP,它也会丢失。
总而言之,必须有更好的解决方案。
我们的工作解决方案
活动!自从这个问题开始以来,我们就已经有了解决方案。
一旦字幕工作人员完成处理,他必须将事件发送到特定队列。出于方便,我们决定使用 Redis Pub/Sub,因为它已经可用并且可以在我们的系统中运行,但它可以与其他队列服务一起实现。
中间有 redis pub/sub 的架构的完整架构
当用户要求为其视频创建字幕时,我们会为此字幕 ID 创建一个新队列(频道)并订阅它。只有打开 SSE 连接的服务器实例才会订阅该通道。
如果另一个用户在该过程正在进行时登陆该页面,并在另一个实例中注册到服务器端事件。该实例还将订阅现有频道。
一旦字幕工作器处理完字幕,它所要做的就是向 Redis 通道发送一个该字幕 ID 的事件。所有注册到它的服务器实例将能够通知仍然连接到服务器端事件的用户。
假设其中一台服务器由于自动降级而被终止。在这种情况下,浏览器将自动重新连接,重新连接后,服务器将订阅该频道(如果尚未订阅)。
如此简单的任务最初让我们视野狭隘,试图在字幕工作器和主后端之间进行直接调用,这几乎让我们放弃了服务器端事件。
我们只需要退后一步,意识到事件从一开始就是答案——它们在我们的应用程序中无处不在,而且它太大了,我们看不到。
标签:分布式,服务器端事件,Websockets 来源: