你很可能正在和庞大复杂的单体应用打交道,每天开发和部署应用的经历都是缓慢而痛苦的。微服务可能看起来非常适合您的应用程序,但它也看起来像是一个遥远的必杀技。您如何走上微服务架构的道路?这里有一些策略可以帮助您摆脱单体地狱,而无需从头开始重写您的应用程序。通过开发所谓的strangler应用程序,可以将单体架构逐渐转变为微服务架构。strangler应用程序的创意来自生长在热带雨林中的strangler藤蔓,它们缠绕在树木上,有时甚至会杀死树木。扼杀者应用是由微服务组成的新应用,通过将新功能实现为服务,逐步从单体应用中抽取服务来实现。随着时间的推移,随着扼杀应用程序实现越来越多的功能,它会缩小并最终杀死单体应用程序。开发strangler应用程序的一个重要好处是,与大爆炸式完全重写不同,它可以立即实施并更快地为业务创造价值。“扼杀”单体应用并逐渐用微服务取代它有三种主要策略:1)将新功能实现为服务。2)将表现层与后端分离。3)通过将功能提取到服务中来分解整体。第一种策略阻止了单体应用的发展。这通常是展示微服务价值并帮助在公司内的所有级别支持迁移和重构的快速方法。其他两种策略打破了整体。在重构单体应用时,您有时可能会使用第二种策略,但您肯定会使用第三种策略,因为它可以将功能从单体应用迁移到扼杀应用程序中。让我们来看看其中的一些策略。1.将新功能实现为服务“洞法则”指出:如果你发现自己身处洞中,就不要再给自己挖洞了。当您的整体变得难以管理时,这是一个很好的建议。换句话说,如果您有一个大型、复杂的单体应用程序,请不要通过向单体添加代码来实现新功能。这将使您的整体变得更大并且更难管理。相反,您应该将新功能实现为服务。这是开始将单体应用程序迁移到微服务架构的好方法。它降低了单体应用的增长速度,加快了新功能的开发(因为它是在一个全新的代码库中开发的),并快速展示了采用微服务架构的价值。将新服务与单体集成图1显示了将新功能实现为服务后的应用程序架构。除了新服务和单体应用之外,该体系结构还包括将服务集成到应用程序中的两个其他元素:■API网关:将新功能请求路由到新服务,并将遗留请求路由到单体应用。■集成胶水代码:将服务与单体结合起来。它使服务能够访问单体拥有的数据并调用单体实现的功能。集成胶水的代码不是一个独立的组件。相反,它由单体中的适配器和使用一种或多种进程间通信机制的服务组成。何时将新功能实现为服务理想情况下,您应该在strangler应用程序中而不是在单体中实现每个新功能。您将把新功能实现为新服务或现有服务的一部分。这样你就可以避免处理单一的代码库。不幸的是,并非每个新功能都可以作为服务来实现。因为微服务架构的本质是围绕业务功能组织的一组松耦合的服务。例如,某个功能可能太小而不能成为有意义的服务。例如,您可能只需要向现有类添加一些字段和方法。或者新功能可能与单体中的代码紧密耦合。如果您尝试将此类功能实现为服务,您通常会发现由于过多的进程间通信导致性能下降。您还可能遇到数据一致性问题。如果新功能不能作为服务实现,解决方案通常是首先在整体中实现新功能。之后,您可以将该功能和其他相关功能提取到您自己的服务中。将新功能实现为服务可以加快这些功能的开发。这是快速展示微服务架构价值的好方法。它还可以降低单体的生长速度。但最终,您需要使用另外两种策略来分解单体。您需要通过将功能从单体应用程序提取到服务来将功能从单体应用程序迁移到扼杀应用程序。您还可以通过水平拆分单体来提高开发速度。让我们看看如何做到这一点。2.将表示层与后端隔离缩小单体应用程序的一种策略是将表示层与业务逻辑和数据访问层分开。典型的企业应用程序由以下层组成:■表示逻辑层:它由处理HTTP请求和生成实现WebUI的HTML页面的模块组成。在具有复杂用户界面的应用程序中,表示层通常包含大量代码。■业务逻辑层:由实施业务规则的模块组成,这些规则在企业应用程序中可能很复杂。■数据访问逻辑层:包含用于访问基础设施服务(如数据库和消息代理)的模块。表示逻辑层与业务和数据访问逻辑层之间通常有明确的界限。业务层具有粗粒度的API,由一个或多个封装业务逻辑的门面(Facade)组成。这个API是一个自然的接缝,您可以沿着它把单体分成两个较小的应用程序,如图2所示。一个应用程序包含表示层,另一个包含业务和数据访问逻辑层。拆分后,表示逻辑应用程序远程调用业务逻辑应用程序。以这种方式拆分单体应用程序有两个主要好处。它使您能够彼此独立地开发、部署和扩展这两个应用程序。特别是,它允许演示开发人员快速迭代用户界面并轻松执行A/B测试,而无需部署后端。这种方法的另一个好处是,它为业务逻辑公开了一组远程API,供以后开发的微服务调用。但这种策略只是部分解决方案。至少有一个或两个终端应用程序可能仍然是一个难以管理的整体。您需要使用第三种策略来用服务替换单体应用。3.将业务能力提取到服务中将新功能实现为服务并将前端Web应用程序与后端分离不会让您走向胜利的另一边。您最终仍将在单体中进行大量开发。如果您想显着改进应用程序的架构并提高开发速度,则需要通过逐渐将业务功能从单体迁移到服务来分解单体。当您使用此策略时,服务实现的业务功能的数量会随着时间的推移而增加,而整体会缩小。您想要提取到服务中的功能是单体应用程序从上到下的“垂直切片”。该切片包含以下内容:■实现API端点的入站适配器。■域逻辑。■出站适配器,例如数据库访问逻辑。■单体数据库模式。如图3所示,这段代码是从单体中提取出来的,并移到了一个独立的服务中。API网关将调用提取的业务功能的请求路由到服务,并将其他请求路由到单体。单体和服务通过集成胶水代码进行协作。集成胶由服务中的适配器和使用一种或多种进程间通信机制的单体组成。提取服务具有挑战性。您需要确定如何将单体的领域模型拆分为两个独立的领域模型,其中一个成为服务的领域模型。您需要打破对象引用等依赖关系。您甚至可能需要拆分类以将功能移动到服务中。对了,你还需要重构数据库。提取服务通常很耗时,尤其是在单体代码库杂乱无章的情况下。因此,您需要仔细考虑要提取的服务。专注于重构应用程序中提供大量价值的那些部分。在接受一项服务之前,问问自己这样做有什么好处。例如,提取实现关键业务和不断发展的功能的服务是值得的。如果提取服务没有太多好处,那么在提取服务上投入精力就没有价值。在本节的后面,我将描述一些用于确定服务摄取范围和时间的策略。但首先让我们更详细地了解您在摄取服务时将面临的一些挑战以及解决这些挑战的方法。提取服务时遇到的一些挑战是:■拆解领域模型。■重构数据库。拆解领域模型为了提取服务,您需要从单体领域模型中提取与服务相关的领域模型。您需要大刀阔斧地拆分领域模型。您将遇到的一项挑战是消除跨服务边界的对象引用。保留在整体中的类可能会引用已移动到服务中的类,反之亦然。例如,想象一下,如图4所示,您提取订单服务,其订单类引用单体餐厅类。由于服务实例通常是一个进程,因此对象引用跨服务边界没有意义。您需要消除这种类型的对象引用。解决这个问题的一个好方法是从DDD聚合的角度来思考。聚合使用主键而不是对象引用来相互引用。因此,您可以将Order和Restaurant类视为聚合,如图5所示,并将Order类中对Restaurant的引用替换为存储主键值的restaurantId字段。用主键替换对象引用的一个问题是,虽然它是对类的一个小改动,但它会对期望对象引用的类的客户端产生很大影响。在本节的后面,我将介绍如何通过在服务和单体之间复制数据来减少更改的范围。例如,DeliveryService可以定义Restaurant类,它是单体中Restaurant类的副本。提取服务通常比将整个类移动到服务中要多得多。拆分领域模型的一个更大挑战是提取嵌入在具有其他职责的类中的功能。这个问题经常出现在职责太多的神班。例如,Order类是FTGO应用程序中的神类之一。它实现了各种业务功能,包括订单管理、食品配送管理等。Delivery实体实现了之前与Order类中的其他功能捆绑在一起的配送管理功能。重构数据库拆分域模型涉及的不仅仅是更改代码。域模型中的许多类都保存在数据库中。它们的字段映射到具体的数据库模式。因此,当您从单体中提取服务时,您也移动了数据。您需要将表从单体数据库移动到服务数据库。另外,当你拆分实体时,你需要拆分相应的数据库表并将新表移动到服务中。例如,将交付管理提取到服务中时,需要拆分Order实体并提取一个Delivery实体。在数据库级别,您拆分ORDERS表并定义一个新的DELIVERY表。然后,将DELIVERY表移动到服务中。重复数据以避免更广泛的更改如上所述,提取服务需要您对整体的域模型进行更改。例如,用主键和拆分类替换对象引用。这些类型的更改会影响代码库,并要求您对整体的各个受影响部分进行大量更改。例如,如果您拆分Order实体并提取Delivery实体,则必须更改引用已移动字段的代码的每一部分。进行这些更改可能非常耗时,并且可能成为打破单体应用的巨大障碍。延迟并可能避免进行这些代价高昂的更改的一个好方法是使用类似于本书《数据库重构》中描述的方法。重构数据库的一个主要障碍是更改该数据库的所有客户端以使用新模式。本书提出的解决方案是在过渡期间保留旧的schema,使用触发器在旧schema和新schema之间进行同步。然后,您可以将客户端从旧模式迁移到新模式。从整体中提取服务时,我们可以使用类似的方法。例如,在提取Delivery实体时,我们让Order实体在过渡期间基本保持不变。如图6所示,我们将与交付相关的字段设置为只读,并通过将数据从交付服务复制回整体来保持私有。所以,我们只需要在单例的代码中找到更新这些字段的地方,改成调用新的DeliveryService即可。通过从DeliveryService复制数据来保留Order实体的结构显着减少了我们需要立即完成的工作量。随着时间的推移,我们可以将使用与交付相关的订单实体字段或订单表列的代码迁移到交付服务。更重要的是,我们可能永远不需要在Monolith中进行更改。如果该代码随后被提取到服务中,则该服务可以访问DeliveryService。确定提取哪些服务以及何时提取正如我提到的,拆除单体非常耗时。它分散了人力资源实施新功能的注意力。因此,您必须仔细确定获取服务的顺序。您需要专注于提取能为您带来最大利益的服务。更重要的是,您希望不断向企业展示迁移到微服务架构的价值。知道你要去哪里对任何旅程都至关重要。开始迁移到微服务的一个好方法是使用时间范围来定义工作。你应该花很短的时间,比如几周,集思广益,想出理想的架构并定义一组服务。这会给你一个目标。但是,请务必记住,此架构并非一成不变。当你分解单体并获得经验时,你应该应用你获得的经验来及时调整重构计划。一旦确定了目标,下一步就是开始拆除整体结构。可以使用几种不同的策略来确定获取服务的顺序。一种策略是有效地冻结单体应用的开发并按需提取服务。您可以提取必要的服务并进行更改,而不是在单体中实现功能或修复错误。这种方法的一个好处是它迫使您打破整体。一个缺点是服务的提取是由短期需求而不是长期需求驱动的。例如,即使你对系统中相对稳定的部分做了一个小改动,它也会要求你拉取服务。因此,您可能会为了一点点收获做很多工作。另一种策略是更有计划的方法,您可以根据提取模块的预期收益对应用程序的模块进行排名。提取服务的好处有几个原因:■加速开发:如果您的应用程序的路线图表明您的应用程序的特定部分将在明年大量开发,那么将其转换为服务可以加速开发。■解决性能、可扩展性或可靠性问题:如果应用程序的特定部分存在性能、可扩展性问题或不可靠,则将其转换为服务很有价值。■允许提取一些其他服务:由于模块之间的依赖关系,提取一项服务有时会简化另一项服务的提取。您可以使用这些标准将重构任务添加到应用程序的“待办事项列表”中,并按预期收益对它们进行排序。这种方式的好处是更具战略性,更符合业务需求。在规划冲刺时,您可以确定是实施功能更有价值还是提取服务更有价值。
