面向数据的架构
译自:Data-Oriented Architecture
2007年,Rajive Joshi在RTI 白皮书中首次提出了面向数据的架构,后在2017年,Christian Vorhemus 和 Erich Schikuta 在维也纳大学的这篇iiWAS论文中再次进行了阐述。DOA是对传统二元架构(即一体式架构和微服务、面向服务的架构)进行翻转的结果。在面向数据的架构中,一体式数据存储是系统的唯一状态源,作用于松耦合、无状态微服务。
面向数据的构架并不是万能的,它也有其成本和好处。但在现实中,很多大公司和生态系统都受困于一类瓶颈,而面向数据的构架正好可以解决这类瓶颈。
一体式架构
尽管很多架构的定义都会与一体式架构进行对比,但本质上,它们是服务侧软件开发的自然状态。
在一个一体式服务中,大量服务侧代码会与一个或多个数据库进行交互,并处理功能运算的方方面面。想象一个交易系统,它会接收用户的请求,进行购买或出售证券,对其定价以及填写订单等。
在一体式服务中,代码仍然是组件化的,并划分到不同的模块中,但程序的不同组件之间并没有明确的API边界。程序中唯一刚性定义的API通常是(a)UI和服务之间的交互协议(即是否使用了REST/HTTP协议);(b)服务和数据存储使用的请求语言;(c)服务和外部依赖。
面向服务的架构和微服务
面向服务的架构(SOA)将一体式程序打散为功能独立、组件化的服务。在我们的交易APP中,可能会使用一个独立的服务作为对外API,接收请求并响应客户,并使用另一个系统来接收定价和市场的其他信息,再使用一个系统来跟踪订单和风险等。这些服务之间的接口是正式定义的API层,服务间通常会使用RPCs来一对一进行交互,其他系统也会使用如消息传递和订阅等方式。
面向服务的架构允许独立(且并行)开发和实现不同的服务。服务间是松耦合的,这意味着一个新的服务可以重用其他服务。
由于SOA中的每个服务都有其各自的API,因此可以独立与各个服务进行交互。开发者可以通过调试和模拟不同的组件,将不同的服务重组成新的流程,进而表现出不同的行为。
微服务是面向服务架构的一种类型。取决于问问题的人,他们认为的微服务可能和SOA不同,可能仅仅指代一些小型且轻量的服务,也可能与SOA的意思完全一样。
扩展问题
在SOA中,组件间通过特定的API进行交互。为了交互,每个组件需要可达,即使用IP地址、服务地址或其他内部标识来不断发送请求/消息。这意味着架构中的每个组件必须要了解它的外部依赖,并与之集成到一起。
根据架构的拓扑,当需要添加新的组件时,需要了解前面所有的组件。意味着移除一个已经集成到架构中(以及与其他多个服务建立连接)的服务可能具有一定的挑战:需要预留临时定义的API,并制定一个迁移计划来转移各个组件(移除旧服务,并接入新服务)。由于服务到服务的API是特定的,这也意味着组件间的RPC可能会异常复杂,进而增加了未来可能变化的API的范围。变更一个其他服务依赖的API将是一项艰巨的任务。
随着微服务的生态的增长,在进行扩展时,会对如下问题愈发敏感:
- 随着组件数目的增加,集成复杂度为N2
- 难以根据经验来理解网络结构,即创建或维护一个测试环境或沙箱将需要确保没有外部依赖的组件。
一些SOA使用者反馈的问题如下:
- 服务间的循环依赖:由于服务是滚动式发布的,且无法在一开始就掌控整个系统,因此很容易引入循环并打破DAG
- SOA扩展的另外一个问题是,它要求提前了解所有未来的客户工作流。如果事先不知道,并在多个垂直维度上对数据进行隔离,那么后续可能会面临如何保证跨多个持久化存储的事务处理性能,以及如何定义哪些存储需要备份数据。
面向数据的架构
面向数据的架构(DOA)同SOA一样,围绕小的、松耦合的组件进行组织,但DOA使用两种主要的方式来划分微服务。
- 组件总是无状态的 相比为每个相关的组件创建组件化以及联合的数据存储,在管理全局schema时,DOA托管描述数据或状态层。
- 最小化组件间的交互,尽量通过数据层来交互。 在我们的交易系统中,一个组件接收到不同证券的报价后,会以一种标准格式将报价发布到我们的数据存储中。一个系统可以通过数据层来请求并消费这些报价,而非通过特定服务的特定API来获取。 因此,集成成本是线性化的。一个DOA架构的变更可能设计N个组件的更新,而非N2个组件。
DOA架构真正的亮点在于,它允许不同的供应商发布独立的高层数据类型。如果我们移除了使用某个表的某个服务,那么后续将不需要做其他变动,特别是当相同的数据类型有多个数据源时。例如一个交易系统连接到多个市场,每个市场都会将客户的请求发布到一个RFQ表,后续下游系统就可以请求该表,而无需关心客户请求来自哪里。
组件通信类型
由于DOA最小化了组件间的交互,那么该如何使用数据层来移除SOA中的内部组件通信?
1.数据生产和消费
将组件组织成生产者和消费者是一种主要的DOA系统设计方式。
如果方便的话,可以在上层使用一系列map
,filter
,reduce
,flatMap
以及其他一元运算来组织业务逻辑,可以将DOA系统分解为一系列组件,每个组件会请求或订阅业务逻辑输入,并产生输出。DOA的挑战在于这些中间步骤都是可见的,这意味着请求的数据要求封装良好、表现良好,且能够对应到特定的业务逻辑。当然,好处是系统的行为是外部可观察的,可追溯和可审计的。
在一个SOA交易系统中,一个组件接收到一个市场的订单后,可能会通过RPCs来确定如何进行定价、报价和交易。在DOA中,一个微服务会从市场(通常以SOA的方式)接收请求,并产生RFQs,而其他生产者则产生定价等等。另外一个服务请求RFQs时,会聚合所有需要的定价、产品配额、订单以及其他自定义响应数据需要的数据。
2.触发动作和行为
有时,最简单的组件间通信方式是RPC,而设计周到的DOA系统可能会使用生产者/消费者模式来替换内部组件间的交互,但也可能会使用直接的方式来让组件X通知组件Y执行动作Z。
首先,最重要的是考虑是否可以将RPCs重新组织为事件以及事件结果,即相比组件X通过RPCs通知组件Y哪里发生了事件E,是否可以让X产生事件E,然后让组件Y通过消费这些事件来作出响应。
这种方式也被称为基于数据的事件,跟我们通常使用的方式相反,但很强大。这种方式强大的原因是它升级了松耦合的概念。系统不需要了解谁会消费它们的事件(相反,RPC调用者需要明确知道正在调用的一方),且生产者不需要担心事件是哪里产生的,事件仅代表了业务逻辑。
实现基于数据的事件的一种不恰当的方式是将事件持久化到数据库表中,数据库表与序列化版本的RPC请求维持1:1的关系。这种情况下,基于数据的事件并不会与系统解耦。为了让基于数据的事件正常运作,需要将一个请求/响应转换为持久化的事件,但前提是这些请求/响应具有业务逻辑意义。
有时可能并不适用基于数据的事件。例如,如果想要触发一个特定组件的行为,这类场景下可能会使用(有限的)组件间的RPCs方式。
面向数据架构的亮点
高集成问题空间
我一直提到交易/金融软件的原因是它们的集成范围通常会比较大。一个(允许用户进行交易的)典型销售端的公司,通常会集成很多市场来与用户进行交互、以及其他流动性提供者来进行定价和下单。业务逻辑涉及到市场的请求以及返回给用户的响应,是一个复杂、多阶段的过程。
在一个高集成的问题空间中,一个服务可能需要了解很多其他服务。为了避免复杂度为O(N2)的集成成本以及服务的高扇出比率(一个服务对应多个服务),围绕数据生产者和消费者来重新设计系统可以简化集成度。在使用新的集成方式时,相比于修改N个新的系统,或将某个系统扇出到N个其他系统,新的集成过程只需编写一个适配器,将产生的数据存储到通用的DOA schema中,消费最终的数据并以正确的格式进行呈现。
DOA中隐含了一种新的复杂性:数据schema。在集成新的服务时不需要对系统做大的变动,且在扩展架构时不需要添加新的中间层、方法或特殊的处理场景。因此数据schema的设计也是一个艰难的过程。但随着集成复杂度的提升,分摊的难度反而会减少。
沙盒数据,以及对数据隔离的响应
如果你正在手动调试或测试某些功能,可能会希望在生产之外的环境中做这些事情。然而某些SOA生态架构中经常很难区别哪些服务处理哪些环境以及哪些环境是自包含的。
一个环境是一个内部一致的、始终连接的服务的集合,通常(理想情况下)使用与生产相同的拓扑来组织这些服务。由于SOA服务通常是独立寻址的,环境的一致性要求每个服务与环境中调用的其他服务保持一致。RPC以及pubsub不能从一个环境泄露到另一个环境中。
SOA中有一些方法来避免发生上述问题,如使用服务注册来为服务生成正确的配置,当使用URI访问服务时,可以使用带环境前缀的不同地址来隐藏(直接的)服务地址。
在DOA中,环境的概念更加简单。通过组件连接的存储层就足以描述它所在的环境。由于所有组件存储内部是无状态的,数据通过定义进行隔离。由于组件间只会通过数据存储进行通信,因此环境之间不存在数据泄露的风险。
面向数据的架构比您想象的更近
现今有很多近似面向数据的架构的例子。一体式数据,即所有数据持久化到一个大型数据存储的方式,通常透露出该系统的架构正在朝着DOA发展。
例如,知识图谱是一个概括式的一体式数据,但往往不够通用,缺少很多与业务逻辑有关的状态。
与一体式数据类似,GraphQL通常作为范化数据存储层。使用GraphQL作为DOA系统的后端的程度更多与系统架构设计的选择有关:使用与业务逻辑概念相关的概括性架构和表,而非特定数据源的架构和表。
权衡
该架构并不是万能的。虽然面向数据的架构消除了很多问题,但也产生了新的问题:它需要设计者更多地关注数据所有者。例如,在处理多个编辑者同时修改相同记录的情况时,通常对该记录的写所有权进行划分。且由于数据中编码了内部组件API,因此需要认真考虑共享的全局schema。
提到Google的Protocol Buffers文档,其中有对schema中的字段设置required
时进行警告的讨论:Required Is Forever。Broadway Technology的CTO Joshua Walsky 说过类似DOA schema的话:Data Is Forever。出于与Protobuf的告警类似的原因,从一个松耦合的分布式系统的表中移除一列是非常非常困难的。
总结
DOA其实是一种事件驱动架构的变种,服务(或组件)使用事件进行交互,而事件来源于数据,因此面向数据的架构准确地说是面向业务逻辑数据的架构,它将数据存储作为整个架构的中心,服务之间通过数据存储和访问简介进行交互。
使用DOA可以将SOA的集成复杂度从N2 降低到N,大大减低了服务的新增或移除对系统造成的影响。
但DOA也不是万能的,它同样带来了新的复杂度,即数据schema设计上的复杂度。
与所有EDA相同,有些场景下并不适合使用DOA,特别是延迟敏感性服务,可能会导致响应不及时或影响QPS。