方便的架构模式

嘿哈布尔!

鉴于当前由冠状病毒引起的事件,许多互联网服务的负载已开始增加。 例如, 英国一家零售连锁店干脆停止了其在线订购网站。,因为容量不够。 并且并不总是可以通过简单地添加更强大的设备来加速服务器,但必须处理客户端请求(否则它们将流向竞争对手)。

在本文中,我将简要讨论一些流行的实践,这些实践将允许您创建快速且容错的服务。 不过,从可能的开发方案中,我只选择了目前正在开发的方案。 便于使用。 对于每个项目,您要么有现成的库,要么有机会使用云平台解决问题。

水平缩放

最简单也是最广为人知的一点。 传统上,最常见的两种负载分配方案是水平缩放和垂直缩放。 在第一种情况下 您允许服务并行运行,从而在它们之间分配负载。 在第二个 您订购更强大的服务器或优化代码。

例如,我将采用抽象的云文件存储,即 OwnCloud、OneDrive 等的一些类似物。

下面是此类电路的标准图片,但它仅展示了系统的复杂性。 毕竟,我们需要以某种方式同步服务。 如果用户从平板电脑保存文件然后想从手机查看该文件,会发生什么情况?

方便的架构模式
方法之间的区别:在垂直扩展中,我们准备增加节点的能力,而在水平扩展中,我们准备添加新节点来分配负载。

连续QRS

命令查询职责分离 这是一个相当重要的模式,因为它不仅允许不同的客户端连接到不同的服务,而且还允许接收相同的事件流。 对于简单的应用程序来说,它的好处并不那么明显,但对于繁忙的服务来说,它极其重要(而且简单)。 其本质是:传入和传出的数据流不应相交。 也就是说,您不能发送请求并期望得到响应;相反,您向服务 A 发送请求,但收到来自服务 B 的响应。

这种方法的第一个好处是能够在执行长请求时中断连接(广义上的连接)。 例如,让我们采取一个或多或少标准的序列:

  1. 客户端向服务器发送请求。
  2. 服务器启动处理时间较长。
  3. 服务器将结果响应给客户端。

让我们想象一下,在第 2 点,连接被中断(或者网络重新连接,或者用户转到另一个页面,从而中断了连接)。 在这种情况下,服务器将很难向用户发送包含有关具体处理内容的信息的响应。 使用 CQRS,顺序会略有不同:

  1. 客户端已订阅更新。
  2. 客户端向服务器发送请求。
  3. 服务器响应“请求已接受”。
  4. 服务器从点“1”通过通道响应结果。

方便的架构模式

正如您所看到的,该方案稍微复杂一些。 此外,这里缺少直观的请求-响应方法。 但是,正如您所看到的,处理请求时连接中断不会导致错误。 此外,如果实际上用户从多个设备(例如,从移动电话和平板电脑)连接到服务,您可以确保响应到达两个设备。

有趣的是,对于受客户端本身影响的事件和其他事件(包括来自其他客户端的事件),处理传入消息的代码变得相同(不是 100%)。

然而,实际上我们得到了额外的好处,因为单向流可以以函数式的方式处理(使用 RX 和类似的)。 这已经是一个重要的优点,因为本质上应用程序可以完全响应式,并且还可以使用函数式方法。 对于胖程序来说,这可以显着节省开发和支持资源。

如果我们将这种方法与水平扩展结合起来,那么作为奖励,我们就能够向一台服务器发送请求并从另一台服务器接收响应。 这样,客户端就可以选择对他来说方便的服务,而内部的系统仍然能够正确地处理事件。

活动采购

如您所知,分布式系统的主要特征之一是没有公共时间、公共临界区。 对于一个进程,您可以进行同步(在相同的互斥体上),在同步过程中您可以确保没有其他人正在执行此代码。 然而,这对于分布式系统来说是危险的,因为它需要开销,并且还会破坏扩展的所有优点——所有组件仍然会等待一个。

从这里我们得到一个重要的事实——快速的分布式系统无法同步,因为那样我们就会降低性能。 另一方面,我们常常需要组件之间有一定的一致性。 为此,您可以使用该方法 最终一致性,其中保证如果在上次更新后的一段时间内(“最终”)没有数据更改,则所有查询都将返回上次更新的值。

重要的是要理解,对于经典数据库来说,它经常被使用 强一致性,其中每个节点都有相同的信息(这通常是在第二个服务器响应后才认为事务已建立的情况下实现的)。 由于隔离级别的原因,这里有一些放松,但总体思路保持不变 - 您可以生活在一个完全和谐的世界中。

不过,让我们回到最初的任务。 如果系统的一部分可以用 最终一致性,那么我们可以构造下图。

方便的架构模式

这种方法的重要特点:

  • 每个传入请求都放置在一个队列中。
  • 在处理请求时,服务还可以将任务放入其他队列中。
  • 每个传入事件都有一个标识符(这是重复数据删除所必需的)。
  • 队列在思想上按照“仅附加”方案工作。 您无法从中删除元素或重新排列它们。
  • 队列按照 FIFO 方案工作(抱歉是同义反复)。 如果您需要并行执行,那么在某一阶段您应该将对象移动到不同的队列。

让我提醒您,我们正在考虑在线文件存储的情况。 在这种情况下,系统将如下所示:

方便的架构模式

重要的是,图中的服务不一定意味着单独的服务器。 甚至过程也可能是一样的。 另一件重要的事情是:从意识形态上来说,这些东西是分开的,以便可以轻松应用水平扩展。

对于两个用户,该图将如下所示(针对不同用户的服务以不同的颜色表示):

方便的架构模式

这种组合的奖金:

  • 信息处理服务是分开的。 队列也是分开的。 如果我们需要提高系统吞吐量,那么我们只需要在更多服务器上启动更多服务即可。
  • 当我们接收来自用户的信息时,我们不必等到数据完全保存。 相反,我们只需要回答“好”,然后逐渐开始工作。 同时,队列可以消除峰值,因为添加新对象的速度很快,并且用户不必等待整个周期的完整通过。
  • 例如,我添加了一个尝试合并相同文件的重复数据删除服务。 如果它在 1% 的情况下长时间工作,客户几乎不会注意到它(见上文),这是一个很大的优势,因为我们不再要求 XNUMX% 的速度和可靠性。

然而,缺点也是显而易见的:

  • 我们的系统已经失去了严格的一致性。 这意味着,例如,如果您订阅了不同的服务,那么理论上您可以获得不同的状态(因为其中一项服务可能没有时间从内部队列接收通知)。 另一个结果是,系统现在没有公共时间。 也就是说,例如,不可能简单地按到达时间对所有事件进行排序,因为服务器之间的时钟可能不同步(此外,两个服务器上的相同时间是乌托邦)。
  • 现在不能简单地回滚任何事件(就像使用数据库可以完成的那样)。 相反,您需要添加一个新事件 - 补偿事件,这会将最后一个状态更改为所需的状态。 举一个类似领域的例子:如果不重写历史记录(这在某些情况下很糟糕),你就不能回滚 git 中的提交,但你可以做一个特殊的 回滚提交,它本质上只是返回旧状态。 然而,错误的提交和回滚都将留在历史中。
  • 数据模式可能会因版本而异,但旧事件将不再能够更新到新标准(因为原则上事件无法更改)。

如您所见,事件溯源与 CQRS 配合良好。 此外,实现一个具有高效且方便的队列但不分离数据流的系统本身就已经很困难,因为您必须添加同步点,这将抵消队列的整体积极作用。 同时应用这两种方法,需要稍微调整程序代码。 在我们的例子中,当向服务器发送文件时,响应仅是“ok”,这仅意味着“添加文件的操作已保存”。 从形式上来说,这并不意味着数据在其他设备上已经可用(例如,重复数据删除服务可以重建索引)。 然而,一段时间后,客户端将收到“文件 X 已保存”样式的通知。

结果:

  • 文件发送状态的数量正在增加:不再是经典的“文件已发送”,而是两个:“文件已添加到服务器上的队列”和“文件已保存在存储中”。 后者意味着其他设备已经可以开始接收文件(根据队列以不同速度运行的事实进行调整)。
  • 由于现在提交信息来自不同的渠道,我们需要想出解决方案来接收文件的处理状态。 结果是:与经典的请求-响应不同,客户端可以在处理文件时重新启动,但此处理本身的状态将是正确的。 此外,该产品本质上是开箱即用的。 结果是:我们现在对失败更加宽容。

拆分

如上所述,事件溯源系统缺乏严格的一致性。 这意味着我们可以使用多个存储,而无需它们之间进行任何同步。 解决我们的问题,我们可以:

  • 按类型分隔文件。 例如,可以对图片/视频进行解码并可以选择更有效的格式。
  • 按国家/地区分开帐户。 由于许多法律,这可能是必需的,但是这种架构方案自动提供了这样的机会

方便的架构模式

如果您想将数据从一个存储传输到另一个存储,那么标准方法已经不够了。 不幸的是,在这种情况下,您需要停止队列,进行迁移,然后启动它。 在一般情况下,数据不能“即时”传输,但是,如果事件队列已完全存储,并且您有以前存储状态的快照,那么我们可以按如下方式重放事件:

  • 在事件源中,每个事件都有自己的标识符(理想情况下是非递减的)。 这意味着我们可以向存储添加一个字段 - 最后处理的元素的 id。
  • 我们复制队列,以便可以为多个独立存储处理所有事件(第一个是已存储数据的存储,第二个是新的,但仍然是空的)。 当然,第二个队列还没有被处理。
  • 我们启动第二个队列(即,我们开始重播事件)。
  • 当新队列相对空时(即添加元素和检索元素之间的平均时间差可以接受),您可以开始将读取器切换到新存储。

正如您所看到的,我们的系统过去没有、现在仍然没有严格的一致性。 只有最终的一致性,即保证事件以相同的顺序处理(但可能有不同的延迟)。 而且,使用它,我们可以相对轻松地将数据传输到地球的另一端,而无需停止系统。

因此,继续我们关于文件在线存储的示例,这样的架构已经给我们带来了许多好处:

  • 我们可以以动态的方式将对象移近用户。 这样您就可以提高服务质量。
  • 我们可能会在公司内部存储一些数据。 例如,企业用户通常要求将其数据存储在受控数据中心(以避免数据泄露)。 通过分片,我们可以轻松支持这一点。 如果客户拥有兼容的云(例如, Azure 自托管).
  • 最重要的是我们不必这样做。 毕竟,首先,我们会很高兴为所有帐户提供一个存储(以便快速开始工作)。 该系统的主要特点是虽然具有可扩展性,但在初始阶段却相当简单。 您只是不必立即编写与一百万个单独的独立队列等一起使用的代码。 如果有必要,将来可以这样做。

静态内容托管

这一点看起来似乎很明显,但对于或多或少标准加载的应用程序来说,它仍然是必要的。 其本质很简单:所有静态内容不是从应用程序所在的同一服务器分发的,而是从专门致力于此任务的特殊服务器分发的。 因此,这些操作执行得更快(条件 nginx 比 Java 服务器更快地提供文件且更便宜)。 加上CDN架构(内容交付网络)使我们能够将文件定位到更靠近最终用户的位置,这对使用该服务的便利性产生积极影响。

静态内容最简单、最标准的示例是网站的一组脚本和图像。 对于它们来说,一切都很简单 - 它们是预先知道的,然后将存档上传到 CDN 服务器,从那里将它们分发给最终用户。

然而,实际上,对于静态内容,您可以使用类似于 lambda 架构的方法。 让我们回到我们的任务(在线文件存储),其中我们需要将文件分发给用户。 最简单的解决方案是创建一个服务,针对每个用户请求,执行所有必要的检查(授权等),然后直接从我们的存储下载文件。 这种方法的主要缺点是静态内容(经过一定修订的文件实际上是静态内容)由包含业务逻辑的同一服务器分发。 相反,您可以制作下图:

  • 服务器提供下载URL。 它可以采用 file_id + key 的形式,其中 key 是一个迷你数字签名,授予在接下来的 XNUMX 小时内访问资源的权利。
  • 该文件由简单的 nginx 分发,具有以下选项:
    • 内容缓存。 由于该服务可以位于单独的服务器上,因此我们为未来留出了储备,能够将所有最新下载的文件存储在磁盘上。
    • 创建连接时检查密钥
  • 可选:流内容处理。 例如,如果我们压缩服务中的所有文件,那么我们可以直接在该模块中进行解压缩。 结果是:IO 操作在它们所属的地方完成。 Java 中的归档器很容易分配大量额外内存,但将具有业务逻辑的服务重写为 Rust/C++ 条件也可能无效。 在我们的例子中,使用了不同的流程(甚至服务),因此我们可以非常有效地分离业务逻辑和 IO 操作。

方便的架构模式

这种方案与分发静态内容不太相似(因为我们不会将整个静态包上传到某个地方),但实际上,这种方法恰恰涉及分发不可变数据。 此外,该方案可以推广到其他情况,其中内容不仅仅是静态的,而是可以表示为一组不可变和不可删除的块(尽管可以添加它们)。

另一个例子(为了强化):如果您使用过 Jenkins/TeamCity,那么您知道这两个解决方案都是用 Java 编写的。 它们都是一个 Java 进程,可以处理构建编排和内容管理。 特别是,它们都有诸如“从服务器传输文件/文件夹”之类的任务。 举个例子:发布工件、传输源代码(当代理不直接从存储库下载代码,而是服务器为他下载时)、访问日志。 所有这些任务的 IO 负载都不同。 也就是说,事实证明,负责复杂业务逻辑的服务器必须同时能够通过自身有效地推送大量数据。 最有趣的是,这样的操作可以根据完全相同的方案委托给同一个 nginx(除了数据密钥应该添加到请求中)。

然而,如果我们返回到我们的系统,我们会得到一个类似的图表:

方便的架构模式

正如您所看到的,系统变得更加复杂。 现在它不仅仅是一个在本地存储文件的迷你进程。 现在需要的不是最简单的支持、API版本控制等。 因此,在绘制完所有图之后,最好详细评估一下可扩展性是否值得为此付出代价。 但是,如果您希望能够扩展系统(包括与更多数量的用户一起工作),那么您将不得不采用此类解决方案。 但是,因此,系统在架构上已准备好增加负载(几乎每个组件都可以克隆以进行水平扩展)。 系统可以在不停止的情况下更新(只是某些操作会稍微变慢)。

正如我一开始所说,现在一些互联网服务的负载已经开始增加。 其中一些根本就开始停止正常工作。 事实上,正是在企业应该赚钱的时候,系统却出现了故障。 也就是说,系统不会推迟交货,也不会建议客户“计划未来几个月的交货”,而是简单地说“去找你的竞争对手”。 事实上,这就是低生产率的代价:利润最高的时候却恰恰发生了损失。

结论

所有这些方法以前都是已知的。 同一个VK长期以来一直在使用静态内容托管的思想来显示图像。 许多在线游戏都使用分片方案将玩家划分为区域或分隔游戏位置(如果世界本身就是一个)。 事件溯源方法在电子邮件中被积极使用。 大多数不断接收数据的交易应用程序实际上都是基于 CQRS 方法构建的,以便能够过滤接收到的数据。 嗯,水平扩展已经在许多服务中使用了相当长的时间。

然而,最重要的是,所有这些模式都变得非常容易在现代应用程序中应用(当然,如果它们合适的话)。 云立即提供分片和水平扩展,这比您自己在不同数据中心订购不同的专用服务器要容易得多。 如果仅仅因为 RX 等库的开发,CQRS 就变得更加容易。 大约十年前,很少有网站可以支持这一点。 得益于 Apache Kafka 的现成容器,事件溯源也非常容易设置。 10年前,这可能是一项创新,但现在这已经司空见惯了。 静态内容托管也是如此:由于更方便的技术(包括有详细的文档和庞大的答案数据库),这种方法变得更加简单。

因此,许多相当复杂的架构模式的实现现在变得更加简单,这意味着最好提前仔细研究一下。 如果在一个已有十年历史的应用程序中,上述解决方案之一由于实施和运营成本高昂而被放弃,那么现在,在一个新的应用程序中,或者在重构之后,您可以创建一个在架构上已经可扩展的服务(性能方面)并可满足客户的新要求(例如,本地化个人数据)。

最重要的是:如果您有一个简单的应用程序,请不要使用这些方法。 是的,它们很漂亮而且很有趣,但是对于一个峰值访问量为 100 人的网站,您通常可以使用经典的整体架构(至少在外部,内部的所有内容都可以分为模块等)。

来源: habr.com

添加评论