转载

奈飞Netflix如何在数据集成API领域使用六边形架构与Clean架构切换到微服务架构? - Netflix TechBlog

大约一年前,我们的工作流程团队开始开发跨业务多个领域的新应用。我们面临着一个有趣的挑战:是从头开始构建应用程序的核心,同时还需要使用许多不同系统中存在的数据。

我们需要的一些数据点,例如有关电影,制作日期,员工和拍摄地点的数据,分布在实现各种协议的许多服务中:gRPC,JSON API,GraphQL等。现有数据对于我们应用程序的行为和业务逻辑至关重要。我们从一开始就需要高度集成。

可切换的数据源

将可见性引入我们产品的早期应用程序之一是作为单体构建的。单体架构允许快速开发和快速更改。某一时刻,有30多个开发人员正在使用它,并且它具有300多个数据库表。

随着时间的流逝,应用程序从广泛的服务产品演变为高度专业化的产品。这导致决定将单体分解为特定的服务。该决定并非针对性能问题,而是针对所有这些不同领域设置了界限,并使专门团队能够独立开发针对特定领域的服务。

单体仍然提供了我们为新应用所需的大量数据,但我们知道单体将在某个时候分解。我们不确定分手的时间,但是我们知道分手是不可避免的,我们需要做好准备。

因此,我们一开始可以利用来自单体的某些数据,因为它们仍然是真实的来源,但要准备将这些数据源在联机后立即交换给新的微服务

利用六角形架构

在不影响业务逻辑的情况下我们还需要支持交换数据源的能力,因此需要将这两者分离。我们决定根据 Hexagonal Architecture 和Bob叔叔的 Clean Architecture 背后的原理来构建我们的应用程序。

六边形架构思想是将输入和输出置于我们设计的边缘。业务逻辑不应该依赖于我们公开REST还是GraphQL API,也不应该依赖于我们从何处获取数据,不依赖于数据库,通过gRPC或REST公开的微服务API,或者仅仅是一个简单的CSV文件。

该模式使我们能够将应用程序的核心逻辑与外界的关注隔离开。将我们的核心逻辑隔离开意味着我们可以轻松更改数据源详细信息,而不会造成重大影响或无需将主要代码重写为代码库。

在具有清晰边界的应用程序中,我们还看到的主要优势之一是测试策略-我们的大多数测试都可以验证我们的业务逻辑,而无需依赖易于更改的协议。

定义核心概念

借鉴六边形/六角形结构,定义业务逻辑的三个主要概念是实体,存储库和交互器。

  • 实体是领域对象(例如电影或拍摄地点),它们不知道它们自己的存储位置(与Ruby on Rails或Java Persistence API中的Active Record不同)。
  • 存储库是获取实体以及创建和更改实体的接口。它们保留用于与数据源通信并返回单个实体或实体列表的方法列表。(例如UserRepository)
  • 交互器是用于编排和执行领域操作的类-考虑服务对象或用例对象。他们实施特定于领域操作的复杂业务规则和验证逻辑(例如,入职生产)

使用这三种主要类型的对象,我们可以定义业务逻辑,而无需任何知识或关心数据保存在何处以及如何触发业务逻辑。业务逻辑之外是数据源和传输层:

  • 数据源是不同存储实现的适配器。数据源可能是SQL数据库的适配器(Rails中的Active Record类或Java中的JPA),弹性搜索适配器,REST API,甚至是诸如CSV文件或Hash之类的简单适配器。数据源实现在存储库上定义的方法,并存储获取和推送数据的实现。
  • 传输层可以触发交互器执行业务逻辑。我们将其视为系统的输入。微服务最常见的传输层是HTTP API层和一组处理请求的控制器。通过将业务逻辑提取到交互器中,我们不会耦合到特定的传输层或控制器实现。交互器不仅可以由控制器触发,还可以由事件,cron作业或命令行触发。

奈飞Netflix如何在数据集成API领域使用六边形架构与Clean架构切换到微服务架构? - Netflix TechBlog

使用传统的分层体系结构,我们将使所有依赖项指向一个方向,上面的每一层都取决于下面的层。传输层将取决于交互程序,交互程序将取决于持久层。

在“六角体系结构”中,所有依赖点都向内指向-我们的核心业务逻辑对传输层或数据源一无所知。传输层仍然知道如何使用交互器,数据源知道如何符合存储库接口。

这样,我们就可以为其他Studio系统的不可避免的更改做好准备,并且只要需要进行更改,就很容易完成交换数据源的任务。

切换数据源

交换数据源的需求比我们预期的要早:我们突然遇到了一个单体的读取限制,并且需要将某个实体的特定读取切换到在GraphQL聚合层上公开的较新的微服务。微服务和单体保持同步,并且从一个服务或另一个服务读取的数据相同,产生的结果相同。

我们设法在2小时内将读取数据从JSON API传输到GraphQL数据源。

我们之所以能够如此快地完成它,主要原因是六角结构(通过适当的抽象,就很容易更改数据源)。我们没有让任何持久性细节泄漏到我们的业务逻辑中。我们创建了一个实现存储库接口的GraphQL数据源。一个简单的单行(one-line)变化是所有我们需要开始从不同的数据源读取。

此时,我们知道六角建筑已经为我们发挥作用了。

单行更改的很大一部分在于,它可以减轻发布风险。如果下游微服务在初始部署时失败,则回滚非常容易。这也使我们能够分离部署和激活,因为我们可以决定通过配置使用哪个数据源。

隐藏数据源详细信息

该体系结构的一大优势是我们能够封装数据源实现细节。我们遇到了这样一种情况:我们需要一个尚不存在的API调用-服务具有一个API来获取单个资源,但没有实现批量获取。与提供API的团队进行交流后,我们意识到此端点需要一些时间才能交付。因此,我们决定在构建此端点的同时,提出另一种解决方案来解决该问题。

我们定义了一个存储库方法,该方法可以在给定多个记录标识符的情况下获取多个资源-并且该方法在数据源上的初始实现向下游服务发送了多个并发调用。我们知道这是一个临时解决方案,数据源实现的第二个要点是在实现后使用批量API。

这样的设计使我们能够继续满足业务需求,而不会产生太多技术债务,也无需事后更改任何业务逻辑。因为我们的业务逻辑不需要了解特定的数据源限制。

测试策略

当我们开始尝试六角结构时,我们知道我们需要提出一种测试策略。我们知道,要获得高速发展的先决条件就是拥有可靠且超快速的测试套件。我们认为它不是一个很好的选择,而是必须具备的。

我们决定在三个不同的层次上测试我们的应用程序:

  • 测试我们的交互器,我们的业务逻辑的核心生活在其中,但与任何类型的持久性或传输无关。我们利用依赖注入并模拟任何类型的存储库交互。这是对我们的业务逻辑进行详细测试的地方,而这些正是我们力求进行的大部分测试。
  • 测试数据源,以确定它们是否与其他服务正确集成,它们是否符合存储库接口,并检查它们在出现错误时的行为。我们尝试最小化这些测试的数量。
  • 我们具有集成规范,遍及整个堆栈,从我们的Transport / API层到交互器,存储库,数据源以及重要的下游服务。这些规格测试了我们是否正确“布线”了一切。如果数据源是外部API,我们将命中该端点并记录响应(并将其存储在git中),从而使我们的测试套件可以在每次后续调用时快速运行。我们不会在这一层进行广泛的测试,通常每个域操作只有一个成功方案和一个失败方案。

我们不测试存储库,因为它们是数据源实现的简单接口,并且我们很少测试实体,因为它们是定义了属性的普通对象。我们测试实体是否具有其他方法(不涉及持久层)。

与可以轻松在任何机器上运行的测试套件一起工作非常好,并且我们的开发团队可以在不中断的情况下处理其日常功能。

延迟决策

在将数据源切换到不同的微服务时,我们处于非常有利的位置。关键好处之一是,我们可以延迟有关是否以及如何存储应用程序内部数据的某些决定。根据功能的用例,我们甚至可以灵活地确定数据存储的类型-是关系型还是文档型。

鲍伯叔叔说得很好:

好的架构的目的是延迟决策。为什么?因为当我们推迟做出决定时,我们需要更多的信息来做出决定。

在项目开始时,关于我们正在构建的系统的信息量最少。我们不应该将自己锁定在一个不知情的决定而导致 项目悖论 的体系结构中。

我们现在做出的决定对我们的需求很有意义,并且使我们能够快速行动。六角结构的最好之处在于,它可以使我们的应用程序灵活地适应将来的需求。

原文  https://www.jdon.com/53882
正文到此结束
Loading...