当前位置: 首页 > 科技观察

Netflix的六边形架构实践

时间:2023-03-12 20:13:40 科技观察

本文介绍了Netflix是如何基于六边形架构开发新应用的。随着Netflix的原创内容每年都在增长,我们希望构建能够使整个创作过程更加高效的应用程序。我们的一个大部门,即工作室工程团队,已经构建了应用程序来帮助完成从脚本编写到广播、从获取脚本内容到谈判交易和管理供应商,以及调度、简化制作等的整个过程。1.从一开始就高度集成大约一年前,我们的Studio流程团队开始开发一个跨越多个业务线的全新应用程序。当时,我们面临一个有趣的挑战:虽然我们需要从头开始构建应用程序的核心,但我们需要的数据分布在许多不同的系统中。我们需要的一些数据,例如关于电影信息、制作日期、人员和拍摄地点的数据,分布在许多服务中。而且,它们使用不同的协议,包括gRPC、JSONAPI、GraphQL等。现有数据对于我们应用程序的行为和业务逻辑非常重要。我们从一开始就需要高度集成。2.可切换数据源将可见性引入我们产品的早期应用程序被设计为单体。在领域知识体系尚未建立的情况下,单体架构可以实现快速发展和快速变化。后来有30多个开发者使用,有300多张数据库表。随着时间的推移,应用程序从广泛的服务演变为高度专业化的产品。在此背景下,团队决定将单体架构解构为一系列专用服务。做出这个决定不是性能问题,而是为所有这些领域设定界限,并允许专门的团队为每个特定领域独立开发服务。我们新应用程序所需的大部分数据仍然由以前的单体提供,但我们知道这个单体总有一天会崩溃。我们不能说什么时候,但我们知道这是不可避免的,所以我们需要做好准备。通过这种方式,我们可以在一开始利用单体应用中的一些数据,因为它们仍然是可信来源;但我们也需要做好准备,一旦新的微服务上线,就切换到这些数据源。3.使用六边形架构,我们需要能够在不影响业务逻辑的情况下切换数据源,所以我们需要保持它们的解耦。我们决定根据六边形架构的原则构建应用程序。六边形架构的思想是将输入和输出都放在设计的边缘。无论我们公开REST还是GraphQLAPI,无论我们从哪里获取数据——通过数据库、通过gRPC或REST公开的微服务API,或者只是一个简单的CSV文件——它都不应该影响业务逻辑。这种模式使我们能够将应用程序的核心逻辑与外部关注点隔离开来。隔离核心逻辑意味着我们可以轻松更改数据源的细节,而不会产生重大影响或需要在代码库中进行大量代码重写。我们还看到,在应用程序中拥有清晰边界的另一大优势是测试策略——我们的大多数测试不需要依赖在验证业务逻辑时容易更改的协议。4.定义核心概念借鉴六边形架构,定义我们业务逻辑的三个概念是实体、存储库和交互器。实体是不知道它们存储在哪里的域对象(如电影或位置)(与RubyonRails中的ActiveRecord或JavaPersistenceAPI不同)。存储库是获取实体以及创建和更改实体的接口。它们包含一系列用于与数据源通信并返回单个实体或实体列表的方法。(例如UserRepository)交互器是用于编排和执行域操作的类-想想服务对象或用例对象。他们为特定领域的操作(例如流式传输节目)实施复杂的业务规则和验证逻辑。有了这三种类型的对象,我们就可以定义业务逻辑,而无需知道或关心数据存储在哪里,或者业务逻辑是如何触发的。业务逻辑之外是数据源和传输层:数据源(DataSources)是不同存储实现的适配器(Adaptor)。数据源可能是SQL数据库的适配器(Rails中的ActiveRecord类或Java中的JPA)、弹性搜索适配器、RESTAPI,甚至像CSV文件或哈希这样简单的东西。数据源实现在存储库上定义的方法,并存储用于获取和推送数据的实现。传输层可以触发交互器执行业务逻辑。我们将其视为系统的输入。微服务最常见的传输层是HTTPAPI层和一组用于处理请求的控制器(Controllers)。通过将业务逻辑提取到交互器中,我们不会耦合到特定的传输层或控制器实现。交互器不仅可以由控制器触发,还可以由事件、cron作业或命令行触发。六边形架构的依赖图向内收缩。在传统的分层架构中,我们所有的依赖都指向一个方向,上面的每一层都依赖于下面的层。传输层将依赖于交互器,交互器将依赖于持久存储层。在六边形架构中,所有依赖项都指向中心方向。我们的核心业务逻辑对传输层或数据源一无所知。但是传输层仍然知道如何使用交互器,数据源知道如何与存储库交互。这样,我们就可以为将来切换到其他Studio系统的更改做好准备,并且在采取该步骤时,我们可以轻松完成切换数据源的任务。5.切换数据源切换数据源的需求比我们预想的来得更早——我们的单体架构突然遇到读取瓶颈,需要将一个实体的特定读取切换到GraphQL聚合层上的新版本公共微服务上。微服务和单体是保持同步的,数据是一样的,他们从各个服务读取的结果也是一致的。我们设法在2小时内将数据读取从JSONAPI切换到GraphQL数据源。我们之所以能够如此快速地做到这一点,很大程度上是由于六边形架构。我们没有让任何持久存储细节泄漏到业务逻辑中。我们创建一个实现存储库接口的GraphQL数据源。因此,您可以通过简单的一行代码更改开始从新数据源读取数据。通过适当的抽象,很容易改变数据源。在这一点上,我们知道我们正在用六边形架构做正确的事情。一行代码更改的一大优势是它降低了发布风险。如果下游微服务在初始部署时失败,回滚也非常容易。这也使我们能够分离部署和激活作业,因为可以通过配置确定要使用的数据源。6.隐藏数据源细节这种架构的优点之一是它允许我们封装数据源的实现细节。我们遇到过一种情况,我们需要一个尚不存在的API调用——有一个服务使用API来获取单个资源,但没有实现批量获取。在与提供API的团队交谈后,我们听说这个批量获取端点需要一些时间才能交付。因此,我们决定在构建端点时使用另一种方案来解决这个问题。我们定义了一个存储库方法,它可以在给定多个记录标识符的情况下获取多个资源——并且该方法在数据源的初始实现会向下游服务发送多个并发调用。我们知道这是一个临时解决方案,数据源实现的下一步改进是在构建批量API后切换到新API。我们的业务逻辑不需要了解特定的数据源约束。这种设计使我们能够继续开发以满足业务需求,而不会积累过多的技术债务或事后更改任何业务逻辑。7.测试策略当我们开始试验六边形架构时,我们知道我们需要提出一个测试策略。提高开发速度的先决条件是拥有可靠且非常快速的测试套件。我们不认为这是锦上添花,而是必要条件。我们决定在三个不同的层上测试应用程序:我们测试了交互器,业务逻辑的核心所在,但与任何类型的持久层或传输层无关。我们使用依赖注入并模拟任何类型的存储库交互。这里我们详细测试业务逻辑,大部分测试都位于这里。我们测试数据源以查看它们是否与其他服务正确集成,是否与存储库交互,并在出现错误时检查它们的行为。我们尽量减少这些测试的数量。我们在整个堆栈中都有集成规范,从我们的传输/API层到交互器、存储库、数据源和重要的下游服务。这些规范测试的是我们是否正确地“连接”了所有内容。如果数据源是外部API,我们将访问该端点并记录响应(并将其存储在git中),从而使我们的测试套件能够在每次后续调用时快速运行。我们不会在这一层进行广泛的测试,通常每个域操作只有一个成功和一个失败场景。我们不测试存储库,因为它们是由数据源实现的简单接口;我们很少测试实体,因为它们是定义了属性的普通对象。我们将测试实体是否有其他方法(这里不涉及持久层)。我们还有改进的余地,比如以后不能ping通我们依赖的任何服务,而是100%依赖合约测试。使用以上述方式编写的测试套件,我们可以在100秒内在单个进程中运行大约3000个规范。一个可以在任何机器上轻松运行的测试套件,它运行良好,我们的开发团队可以不间断地进行日常功能测试。8.延迟决策现在我们可以轻松地将数据源切换到不同的微服务。一个关键的好处是我们可以延迟一些关于是否以及如何在应用程序中存储数据的决定。根据功能用例,我们甚至可以灵活地确定数据存储的类型——它可以是关系型或文档型。当这个项目开始时,我们对我们正在构建的系统知之甚少。我们不应该将自己锁定在导致项目悖论和不明智决策的架构中。我们现在做出的决定符合我们的需要,让我们能够迅速行动。六边形架构的最大优点是它使我们的应用程序具有灵活性和面向未来的能力。