使用 Node.js 和 Redis 在内部构建 Pub/Sub 服务

已发表: 2022-03-10
快速总结 ↬由于我们系统中每条消息的数据大小从几个字节到高达 100MB 不等,因此我们需要一个可以支持多种场景的可扩展解决方案。 在本文中,Dhimil Gosalia 解释了为什么您也应该考虑构建内部 Pub/Sub 服务。

当今世界实时运行。 无论是交易股票还是订购食品,今天的消费者都期待立竿见影的效果。 同样,我们都希望立即了解事情——无论是新闻还是体育。 换句话说,零是新英雄。

这也适用于软件开发人员——可以说是一些最不耐烦的人! 在深入了解 BrowserStack 的故事之前,如果我不提供一些有关 Pub/Sub 的背景知识,那将是我的失职。 对于那些熟悉基础知识的人,请随意跳过接下来的两段。

当今的许多应用程序都依赖于实时数据传输。 让我们仔细看一个例子:社交网络。 Facebook 和 Twitter 之类的网站会生成相关的订阅源,而您(通过他们的应用程序)会使用它并监视您的朋友。 他们通过消息传递功能实现了这一点,如果用户生成数据,它将被发布给其他人以供其他人使用。 任何严重的延迟和用户都会抱怨,使用量会下降,如果持续存在,就会大量生产。 赌注很高,用户的期望也很高。 那么 WhatsApp、Facebook、TD Ameritrade、华尔街日报和 GrubHub 等服务如何支持大量实时数据传输呢?

跳跃后更多! 继续往下看↓

它们都使用类似的高级软件架构,称为“发布-订阅”模型,通常称为 Pub/Sub。

“在软件架构中,发布-订阅是一种消息传递模式,其中消息的发送者(称为发布者)不会将消息编程为直接发送给特定的接收者(称为订阅者),而是将发布的消息分类为不知道哪些订阅者的类,如果任何,可能有。 同样,订阅者对一个或多个类表示兴趣,并且只接收感兴趣的消息,而不知道有哪些发布者(如果有的话)。

— 维基百科

对定义感到厌烦? 回到我们的故事。

在 BrowserStack,我们所有的产品都支持(以某种方式)具有大量实时依赖组件的软件——无论是自动化测试日志、新鲜出炉的浏览器屏幕截图还是 15fps 移动流媒体。

在这种情况下,如果一条消息丢失,客户可能会丢失对于防止错误至关重要的信息。 因此,我们需要针对不同的数据大小要求进行扩展。 例如,对于给定时间点的设备记录器服务,一条消息下可能会生成 50MB 的数据。 像这样的尺寸可能会使浏览器崩溃。 更不用说 BrowserStack 的系统将来需要针对其他产品进行扩展。

由于每条消息的数据大小从几个字节到高达 100MB 不等,我们需要一个可以支持多种场景的可扩展解决方案。 换句话说,我们寻求一把可以切蛋糕的剑。 在本文中,我将讨论在内部构建 Pub/Sub 服务的原因、方式和结果。

通过 BrowserStack 的实际问题,您将更深入地了解构建您自己的 Pub/Sub 的要求和过程

我们需要发布/订阅服务

BrowserStack 有大约 100M+ 条消息,每条消息大约在 2 字节和 100+ MB 之间。 它们随时以不同的互联网速度在世界各地传递。

按消息大小计算,这些消息的最大生成器是我们的 BrowserStack Automate 产品。 两者都有实时仪表板,显示用户测试的每个命令的所有请求和响应。 因此,如果有人运行一个包含 100 个请求的测试,其中平均请求响应大小为 10 个字节,则传输 1×100×10 = 1000 个字节。

现在让我们考虑更大的图景——当然——我们不会每天只运行一次测试。 每天使用 BrowserStack 运行大约 850,000 多个 BrowserStack Automate 和 App Automate 测试。 是的,我们平均每个测试大约有 235 个请求-响应。 由于用户可以在 Selenium 中截屏或请求页面源,我们的平均请求-响应大小约为 220 字节。

所以,回到我们的计算器:

850,000×235×220 = 43,945,000,000 字节(大约)或每天仅 43.945GB

现在让我们谈谈 BrowserStack Live 和 App Live。 当然,在数据大小方面,我们有 Automate 是我们的赢家。 但是,在传递的消息数量方面,Live 产品处于领先地位。 对于每次实时测试,每转一分钟大约有 20 条消息通过。 我们运行了大约 100,000 个实时测试,每个测试平均大约 12 分钟,这意味着:

100,000×12×20 = 每天 24,000,000 条消息

现在是令人敬畏和非凡的一点:我们为这个名为 pusher 的应用程序构建、运行和维护应用程序,其中包含 6 个 t1.micro ec2 实例。 运行服务的成本? 每月约 70 美元

选择建造与购买

首先要做的事情:作为一家初创公司,像大多数其他人一样,我们总是很高兴能够在内部构建东西。 但我们仍然评估了一些服务。 我们的主要要求是:

  1. 可靠性和稳定性,
  2. 高性能,和
  3. 成本效益。

让我们把成本效益标准排除在外,因为我想不出任何每月费用低于 70 美元的外部服务(如果知道你这样做,请发推特给我!)。 所以我们的答案是显而易见的。

在可靠性和稳定性方面,我们发现提供 Pub/Sub 即服务的公司具有 99.9% 以上的正常运行时间 SLA,但附加了许多 T&C。 问题并不像您想象的那么简单,尤其是当您考虑位于系统和客户端之间的广阔的开放 Internet 土地时。 熟悉互联网基础设施的人都知道,稳定的连接是最大的挑战。 此外,发送的数据量取决于流量。 例如,一分钟为零的数据管道可能会在下一分钟内爆裂。 在这种突发时刻提供足够可靠性的服务很少见(谷歌和亚马逊)。

我们项目的性能意味着以接近零的延迟获取数据并将数据发送到所有侦听节点。 在 BrowserStack,我们利用云服务 (AWS) 和主机托管。 但是,我们的发布者和/或订阅者可以放置在任何地方。 例如,它可能涉及生成急需的日志数据的 AWS 应用程序服务器或终端(用户可以安全连接以进行测试的机器)。 再次回到开放的 Internet 问题,如果我们要降低风险,我们将不得不确保我们的 Pub/Sub 利用最好的主机服务和 AWS。

另一个基本要求是能够传输所有类型的数据(字节、文本、奇怪的媒体数据等)。 综合考虑,依靠第三方解决方案来支持我们的产品是没有意义的。 反过来,我们决定重振创业精神,卷起袖子编写自己的解决方案。

构建我们的解决方案

Pub/Sub 的设计意味着将有一个发布者,生成和发送数据,以及一个订阅者接受和处理它。 这类似于广播:广播频道在一定范围内的任何地方广播(发布)内容。 作为订阅者,您可以决定是否收听该频道并收听(或完全关闭您的收音机)。

与广播类比中数据对所有人免费且任何人都可以决定收听的无线电类比不同,在我们的数字场景中,我们需要身份验证,这意味着发布者生成的数据只能用于单个特定的客户或订阅者。

Pub/Sub 的基本工作
Pub/Sub 的基本工作(大预览)

上图提供了一个良好的 Pub/Sub 示例,其中包含:

  • 出版商
    在这里,我们有两个发布者根据预定义的逻辑生成消息。 在我们的广播类比中,这些是我们创建内容的广播节目主持人。
  • 话题
    这里有两个,意味着有两种类型的数据。 我们可以说这些是我们的广播频道 1 和 2。
  • 订户
    我们有三个,每个都读取特定主题的数据。 需要注意的一件事是,订阅者 2 正在阅读多个主题。 在我们的无线电类比中,这些人是被调到无线电频道的人。

让我们开始了解服务的必要要求。

  1. 事件组件
    这只有在有东西可以启动时才会启动。
  2. 瞬态存储
    这可以使数据在短时间内保持不变,因此如果订阅者速度较慢,它仍然有一个窗口可以使用它。
  3. 减少延迟
    通过网络以最小的跳数和距离连接两个实体。

我们选择了一个满足上述要求的技术栈:

  1. 节点.js
    因为为什么不呢? Evented,我们不需要繁重的数据处理,而且很容易上手。
  2. 雷迪斯
    支持完美的短期数据。 它具有启动、更新和自动过期的所有功能。 它还减少了应用程序的负载。

用于业务逻辑连接的 Node.js

在编写包含 IO 和事件的代码时,Node.js 是一种近乎完美的语言。 我们特定的给定问题两者都有,这使得这个选项对我们的需求最实用。

当然,其他语言(如 Java)可能会更优化,或者 Python 等语言提供可扩展性。 然而,从这些语言开始的成本是如此之高,以至于开发人员可以在相同的时间内完成在 Node 中编写代码。

老实说,如果该服务有机会添加更复杂的功能,我们可以查看其他语言或完整的堆栈。 但这里是天作之合。 这是我们的package.json

 { "name": "Pusher", "version": "1.0.0", "dependencies": { "bstack-analytics": "*****", // Hidden for BrowserStack reasons. :) "ioredis": "^2.5.0", "socket.io": "^1.4.4" }, "devDependencies": {}, "scripts": { "start": "node server.js" } }

简单地说,我们相信极简主义,尤其是在编写代码时。 另一方面,我们可以使用 Express 之类的库来为这个项目编写可扩展的代码。 但是,我们的创业本能决定将其传递下去,并将其保存到下一个项目中。 我们使用的其他工具:

  • 奥雷迪斯
    这是包括阿里巴巴在内的公司使用的最受支持的 Redis 与 Node.js 连接库之一。
  • 套接字.io
    使用 WebSocket 和 HTTP 进行优雅连接和回退的最佳库。

Redis 用于瞬态存储

Redis 即服务可扩展性非常可靠且可配置。 此外,还有许多可靠的 Redis 托管服务提供商,包括 AWS。 即使您不想使用提供程序,Redis 也很容易上手。

让我们分解可配置部分。 我们从通常的主从配置开始,但 Redis 也带有集群或哨兵模式。 每种模式都有自己的优势。

如果我们可以通过某种方式共享数据,那么 Redis 集群将是最佳选择。 但是,如果我们通过任何启发式共享数据,我们的灵活性就会降低,因为必须遵循启发式。 少一些规则,多一些控制对生活有好处!

Redis Sentinel 最适合我们,因为数据查找仅在一个节点中完成,在给定时间点连接,而数据没有分片。 这也意味着即使多个节点丢失,数据仍然分布并存在于其他节点中。 所以你有更多的 HA 和更少的损失机会。 当然,这使专业人员不再拥有集群,但我们的用例有所不同。

30000 英尺的建筑

下图提供了我们的 Automate 和 App Automate 仪表板如何工作的非常高级的图片。 还记得我们在前面部分中的实时系统吗?

BrowserStack 的实时 Automate 和 App Automate 仪表板。
BrowserStack 的实时 Automate 和 App Automate 仪表板(大预览)

在我们的图表中,我们的主要工作流程以较粗的边框突出显示。 “自动化”部分包括:

  1. 终端
    由您在 BrowserStack 上测试时获得的原始版本的 Windows、OSX、Android 或 iOS 组成。
  2. 中心
    使用 BrowserStack 进行所有 Selenium 和 Appium 测试的联系点。

这里的“用户服务”部分是我们的看门人,确保将数据发送给正确的个人并保存。 它也是我们的安全守护者。 “推动者”部分包含我们在本文中讨论的核心内容。 它由通常的嫌疑人组成,包括:

  1. 雷迪斯
    我们用于消息的临时存储,在我们的例子中,自动化日志是临时存储的。
  2. 出版商
    这基本上是从集线器获取数据的实体。 您的所有请求响应都由该组件捕获,该组件以session_id作为通道写入 Redis。
  3. 订户
    这将从为session_id生成的 Redis 中读取数据。 它也是客户端通过WebSocket(或HTTP)连接以获取数据然后将其发送给经过身份验证的客户端的Web服务器。

最后,我们有用户的浏览器部分,代表一个经过身份验证的 WebSocket 连接,以确保发送session_id日志。 这使得前端JS可以为用户解析和美化它。

与日志服务类似,我们这里有用于其他产品集成的推送器。 我们使用另一种形式的 ID 代替session_id来表示该通道。 这一切都靠推杆!

结论 (TLDR)

我们在构建 Pub/Sub 方面取得了相当大的成功。 总结一下我们内部构建它的原因:

  1. 更好地满足我们的需求;
  2. 比外包服务便宜;
  3. 完全控制整体架构。

更不用说 JS 非常适合这种场景。 事件循环和海量IO才是问题所需要的! JavaScript 是单伪线程的魔法。

事件和 Redis 作为一个系统使开发人员的工作变得简单,因为您可以从一个来源获取数据并通过 Redis 将其推送到另一个来源。 所以我们建造了它。

如果该用法适合您的系统,我建议您也这样做!