亚马逊的分布式计算宣言 - werner


在将近25年之后,我将完整地发表《分布式计算宣言》,这是亚马逊早期的一份内部文档,它改变了我们电子商务平台的架构。

亚马逊的系统架构的一个非常简短的历史:

在我们深入了解亚马逊的架构历史之前,先了解一下我们25年前的情况是有帮助的。亚马逊正在快速发展,每隔几个月就建立和推出产品,这些创新在今天看来是理所当然的。一键购买、自助订购、即时退款、推荐、相似性、书内搜索、联营销售和第三方产品。这样的例子不胜枚举。而这些只是面向客户的创新,我们甚至还没有触及幕后发生的事情的表面。

亚马逊一开始就采用了传统的两层架构:一个单体的、无状态的应用程序(Obidos),用于服务页面和一整套数据库,这些数据库随着亚马逊推出的每一组新的产品类别、这些类别中的产品、客户和国家而增长。这些数据库是一个共享资源,最终成为我们想要创新的速度的瓶颈。

早在1998年,亚马逊的一批高级工程师开始为彻底改革亚马逊的架构奠定基础,以支持下一代以客户为中心的创新。一个核心要点是将表现层、业务逻辑和数据分开,同时确保可靠性、规模、性能和安全性达到一个令人难以置信的高标准,并保持成本可控。他们的建议被称为 "分布式计算宣言"。

我现在分享这些,是为了让你了解亚马逊工程团队在九十年代末的思维是多么的先进。他们不断地发明自己摆脱困境,将单体扩展为我们现在所说的面向服务的架构,这是支持快速创新的必要条件,而快速创新已经成为亚马逊的代名词。我们的领导原则之一是发明和简化--我们的工程师真的是以这一口号为目标。

分布式计算宣言
创建时间: 1998 年 5 月 24 日
修订日期: 1998 年 7 月 10 日

背景
很明显,如果亚马逊的处理能力要扩展到可以支持我们当前订单量十倍的程度,我们就需要创建和实施一个新的架构。问题是,新架构应该采取什么形式,我们如何实现它?
我们当前的两层客户端-服务器架构本质上是一种数据绑定架构。运行业务的应用程序直接访问数据库并了解其中嵌入的数据模型。这意味着应用程序和数据模型之间存在非常紧密的耦合,即使功能保持不变,数据模型更改也必须伴随应用程序更改。由于应用程序对数据元素之间的相互依赖关系很敏感,因此这种方法无法很好地扩展,并且很难根据数据所在的位置来分配和隔离处理。

关键概念
我们提出的新架构中有两个关键概念可以解决当前系统的缺点:

  • 第一个是转向基于服务的模型,
  • 第二个是改变我们的处理方式,使其更接近于工作流方法的模型。

本文没有说明应该使用什么具体技术来实现新架构。只有当我们确定新架构能够满足我们的要求并且我们着手实施它时,才应该确定这一点。

基于服务的模型
我们建议转向三层架构,其中表示(客户端)、业务逻辑和数据是分离的。这也被称为基于服务的架构。
应用程序(客户端)将不再能够直接访问数据库,而只能通过一个定义良好的接口来访问,该接口封装了执行该功能所需的业务逻辑。
这意味着客户端不再依赖底层数据结构甚至数据所在的位置。
业务逻辑(在服务中)和数据库之间的接口可以改变而不影响客户端,因为客户端通过它自己的接口与服务交互。
同样,客户端接口可以在不影响服务与底层数据库交互的情况下发展。

结合工作流的服务必须提供同步和异步方法。同步方法可能适用于立即响应的操作,例如添加客户或查找供应商信息。但是,其他本质上异步的操作不会提供即时响应。

这方面的一个示例是调用服务以将工作流元素传递到链中的下一个处理节点。请求者不希望立即返回结果,只是指示工作流元素已成功排队。但是,请求者可能有兴趣最终收到请求的结果. 为此,服务必须提供一种机制,请求者可以借此接收异步请求的结果。

有几个模型,轮询或回调。

  • 在回调模型中,请求者传递例程的地址以在请求完成时调用。当请求和回复之间的时间相对较短时,最常使用这种方法。回调方法的一个显着缺点是,当请求完成使回调地址无效时,请求者可能不再处于活动状态。
  • 然而,轮询模型会受到定期检查请求是否已完成所需的开销的影响。轮询模型可能是与异步服务交互最有用的模型。

在我们转向基于服务的模型时,必须考虑几个重要的影响:
首先是我们将不得不采用更加规范的软件工程方法。目前,我们的大部分数据库访问都是临时的,大量的 Perl 脚本在很大程度上运行着我们的业务。转向基于服务的架构将需要在一段时间内逐步取消客户端对数据库的直接访问。否则,我们甚至无法希望在不对客户产生负面影响的情况下实现三层架构的优势,例如数据位置透明性和演进数据模型的能力。服务及其接口的规范、设计和开发不应随意发生。它必须仔细协调,这样我们就不会以目前同样混乱的扩散而告终。

与第一个相关的基于服务的方法的第二个含义是所有软件开发人员都需要进行重大的思维转变:
我们当前的思维方式是以数据为中心,当我们为业务需求建模时,我们使用以数据为中心的方法,我们的解决方案涉及更改数据库表或列以实施解决方案,并且我们将数据模型嵌入到访问应用程序中。
基于服务的方法将要求我们将业务需求的解决方案至少分成两部分:

  • 第一部分是数据元素之间关系的建模,就像我们一直做的那样。这包括将在与数据交互的服务中强制执行的数据模型和业务规则。
  • 然而,第二件是我们以前从未做过的事情,它正在设计客户端和服务之间的接口,以便客户端不会暴露或依赖底层数据模型。

这与上面讨论的软件工程问题密切相关。

基于工作流的模型和数据域
亚马逊的业务很适合基于工作流的处理模式:我们已经有了一个 "订单管道",从客户下单到发货,各种业务流程都会对其进行操作。我们的大部分处理已经是面向工作流程的,尽管工作流程的 "元素 "是静态的,主要是在一个单一的数据库中。

我们目前工作流程模型的一个例子是客户订单在系统中的进展情况。每个客户订单上的条件属性决定了工作流程中的下一个活动。然而,目前的数据库工作流程模型将不能很好地扩展,因为处理是针对一个中央实例进行的。随着工作量的增加(单位时间内订单数量的增加),针对中央实例的处理量将增加到无法持续的地步。

解决这个问题的方法是分配工作流的处理,使其可以从中央实例中卸载:
实现这一点需要像客户订单这样的工作流元素在业务处理("节点")之间移动,这些节点可能位于不同的机器上。与其说是流程来找数据,不如说是数据来找流程。
这意味着每个工作流元素都需要工作流中的下一个节点对其采取行动所需的所有信息。这个概念与面向消息的中间件所使用的概念相同,在那里,工作单位被表示为从一个节点(业务流程)分流到另一个节点的消息。

工作流的一个问题是它是如何被引导的:
每个处理节点是否有自主权,根据嵌入的业务规则将工作流元素重定向到下一个节点(自主),还是应该有某种工作流协调器来处理节点之间的工作转移(定向)?

为了说明这种区别,考虑一个执行信用卡收费的节点。它是否有内置的 "智能",将成功的订单转到订单流水线的下一个处理节点,并将失败的订单分流到其他节点进行例外处理?或者信用卡收费节点被认为是一个可以从任何地方调用的服务,并将其结果返回给请求者?在这种情况下,请求者将负责处理失败条件,并确定成功和失败的请求的下一个处理节点是什么。

有向工作流模型的一个主要优势是其灵活性。它在工作流处理节点之间移动工作是可互换的构件,可用于不同的组合和目的。有些处理方式很适合定向模式,例如信用卡收费处理,因为它可以在不同的情况下被调用。

在更大的范围内,DC处理被认为是一个单一的逻辑过程,从定向模型中受益。DC将接受客户订单进行处理,并将结果(发货、异常情况等)返回给给它工作的人。

另一方面,如果某些流程与相邻处理的互动是固定的,不可能改变,那么它们将从自主模型中受益。这方面的一个例子是,多本书总是从分拣单到重仓。

分布式工作流方法有几个优点
其中之一是一个业务流程,如完成订单,可以很容易地被建模以提高可扩展性。例如,如果向信用卡收费成为一个瓶颈,可以增加额外的收费节点而不影响工作流模型。另一个优点是,工作流程路径上的节点不一定要依赖访问远程数据库来操作工作流程中的元素。这意味着,当工作流程系统的其他部分(如数据库)不可用时,某些处理可以继续进行,从而提高系统的整体可用性。

然而,基于消息的分布式工作流模型也有一些缺点:
以数据库为中心的模型,每个流程都访问相同的中央数据存储,允许数据变化在系统中快速有效地传播。例如,如果一个客户想改变他的订单所使用的信用卡号码,因为他最初指定的信用卡号码已经过期或被拒绝,这可以很容易地完成,而且这种变化将立即在系统中到处体现。

在一个基于消息的工作流程模型中,这就变得更加复杂了。工作流的设计必须适应这样一个事实:当一个工作流元素从系统的一端到达另一端时,一些基础数据可能会发生变化。此外,在传统的基于队列的工作流程中,要确定任何特定工作流程元素的状态是比较困难的。为了克服这个问题,必须建立一些机制,允许在不影响工作流流程的可用性和自主性的情况下,为外部流程的利益记录状态转换。这些问题使得正确的初始设计比单体系统更重要,并与其他地方讨论的软件工程实践相呼应。

工作流模型适用于在我们的系统中瞬息万变的数据,并经历了定义明确的状态变化,然而,还有一类数据并不适合工作流的方法。这类数据在很大程度上是持久性的,不会像工作流数据那样以同样的频率或可预测性来改变。

在我们的案例中,这类数据描述的是客户、供应商和我们的目录。重要的是,这些数据是高度可用的,而且我们要维护这些数据之间的关系(比如知道哪些地址与客户有关)。创建数据域的想法允许我们根据它与其他数据的关系来分割这类数据。

例如:

  • 所有与客户有关的数据将构成一个领域,
  • 所有关于供应商的数据构成另一个领域,
  • 所有关于我们目录的数据构成第三个领域。

这允许我们创建服务,客户通过这些服务与不同的数据域进行交互,并开启了复制域数据的可能性,从而使其更接近消费者。这方面的一个例子是将客户数据域复制到英国和德国,这样客户服务机构就可以在本地数据存储的基础上运作,而不依赖于数据的单一实例的可用性。数据的服务接口将是相同的,但他们访问的域的副本将是不同的。创建数据域和访问它们的服务接口是将客户与数据的内部结构和位置的知识分开的一个重要因素。

应用这些概念
DC处理很适合作为上面讨论的工作流和数据域概念应用的例子。
通过DC的数据流分为三个不同的类别:

  • 第一类是很适合顺序队列处理的。这方面的一个例子是由vreceive填写的 received_items队列。
  • 第二类是应该驻留在数据域中的数据,这是因为它的持久性或要求它能被广泛使用。库存信息(bin_items)就属于这一类,因为DC和其他业务功能如采购和客户支持都需要它。
  • 第三类数据既不适合排队,也不适合领域化模型。这类数据是瞬时的,只在本地(在DC内)需要。然而,它不太适合于顺序队列处理,因为它是以聚合方式操作的。这方面的一个例子是生成挑选清单所需的数据。一批客户的货物必须累积起来,以便拣选表有足够的信息来根据运输方式等打印出拣选单。一旦拣货单处理完成,货物就会进入其工作流程的下一站。

这第三类数据的存放区被称为聚合队列,因为它们同时表现出队列和数据库表的属性。

跟踪状态变化
外部流程能够通过系统跟踪工作流元素的移动和状态变化是非常必要的。在直流处理的情况下,客户服务和其他功能需要能够确定客户订单或货物在管道中的位置。

我们建议使用的机制是,沿着工作流程的某些节点向某个集中式数据库实例插入一行,以表明正在处理的工作流程元素的当前状态。这种信息不仅对跟踪工作流程中的某个环节很有用,而且还能为我们的订单管道的运作和低效率提供重要的洞察力。当客户订单处于活动状态时,状态信息将只保留在生产数据库中。一旦完成,状态变化信息将被转移到数据仓库,在那里它将被用于历史分析。

对In-flight 中的工作流程元素进行修改
工作流处理会产生一个数据货币问题,因为工作流元素包含了进入下一个工作流节点所需的所有信息。
如果客户想在订单处理过程中更改订单的送货地址,该怎么办?
目前,CS代表可以改变customer_order中的送货地址(前提是在创建pending_customer_shipment之前),因为订单和客户数据都是集中在一起的。

然而,在一个工作流程模型中,客户订单将在其他地方被处理,通过不同的阶段,成为对客户的发货。为了影响工作流元素的变化,必须有一种机制来传播属性变化。

发布和订阅模型是实现这一目的的方法之一:
为了实现P&S模型,工作流处理节点将订阅以接收某些事件或异常的通知。属性变化将构成一类事件。要改变一个飞行中的订单的地址,一个表明订单和改变的属性的消息将被发送到所有订阅该特定事件的处理节点。此外,一个状态变化行将被插入跟踪表中,表明一个属性变化被请求。如果其中一个节点能够影响属性变化,它将在状态变化表中插入另一行,表明它已经对订单进行了更改。这种机制意味着将有一个属性变化事件的永久记录以及它们是否被应用。

P&S模型的另一个变体是,工作流协调者,而不是工作流处理节点,影响到飞行中的工作流元素的变化,而不是工作流处理节点。与上述机制一样,工作流协调器将订阅接收事件或异常的通知,并在其处理时将这些通知应用于适用的工作流元素。

详细点击标题