在博主工作的经历中曾经接受过一个历史超过15年的单体应用程序,其代码行数超过四百万,使用的技术五花八门。并且由于项目太大,也带来了很多问题:
项目的开发者管理人员当然也意识到了这个问题,但是由于项目已经有很多客户在使用,因此要进行微服务转型必须考虑客户不受到影响,因此进行微服务转型并不是一件容易的事情,但是在项目内部的所有人都意识到转型到微服务的架构是必须完成的事情,因为项目过于庞大带来的问题已经对项目的新功能的开发,测试和交付带来了巨大的障碍。
这样的问题存在于软件行业许多的系统当中。毕竟软件先于微服务的概念诞生。
一种不推荐使用的策略是进行“大爆炸”重写。这种方式是,将所有开发工作都集中在从头开始构建新的基于微服务的应用程序上。尽管听起来很吸引人,但它极具风险,并且很可能会以失败告终。Martin Fowler说,“大爆炸重写唯一保证的就是大爆炸!”
代替Big Bang重写的方案是,我们应该逐步重构整体应用程序。逐步构建一个由微服务组成的新应用程序,并与整体应用程序一起运行。随着时间的流逝,单片应用程序实现的功能量会不断缩小,直到它完全消失或变成另一个微服务为止。此策略类似于以70 mph的速度在高速公路上行驶时维修汽车-它机具挑战性,但风险远小于尝试重写Big Bang。
Martin Fowler将此应用程序现代化策略称为Strangler应用程序。这个名字来自雨林中的扼杀藤蔓(又称扼杀无花果)。为了达到林冠上方的阳光,缠结藤蔓生长在树木周围。有时,树死了,留下了树形的藤蔓。应用程序现代化遵循相同的模式。我们将围绕旧应用程序构建一个由微服务组成的新应用程序,该应用程序最终将消失。
为了达到单体应用的微服务转型,我们有下面这些策略可以参照
打孔法则说,每当处在在孔中时,都应停止挖掘。 当整体应用程序变得难以管理时,这是值得遵循的好建议。 换句话说,我们应该停止将整体增大。 这意味着在实现新功能时,不应将更多代码添加到整体中。 相反,此策略的主要思想是将新代码放入独立的微服务中。 下图显示了应用此方法后的系统架构。
除了新服务和旧的单体应用之外,还有两个其他组件。 第一个是请求路由器,它的功能是处理传入的(HTTP)请求。 它类似于我们在另外的博客中提到的API gateway。 路由器将与新功能相对应的请求发送到新服务。 同时将老的请求路由到单体应用当中。
另一个组件是粘合代码,它将服务与整体集成在一起。 服务很少单独存在,经常需要访问整体所拥有的数据。 驻留在整体,服务或两者中的粘合代码负责数据集成。 该服务使用胶水代码读取和写入整体拥有的数据。
服务可以使用三种策略来访问单体应用中的数据:
胶水代码有时称为反腐败层。这是因为粘合代码可防止具有原始域模型的服务被遗留的整体域模型中的概念所污染。粘合代码在两个不同的模型之间转换。 “反腐败层”一词首先出现在必读的埃里克·埃文斯(Eric Evans)的《域驱动设计》(Domain Driven Design)中,然后在白皮书中进行了完善。建立反腐败层可能是一项艰巨的任务。但是,如果您想摆脱整体困境,那么创建一个是至关重要的。
将新功能实现为轻量级服务有很多好处。它可以防止整体变得更加难以管理。可以独立于整体而开发,部署和扩展服务。所创建的每个新服务可以得到微服务体系结构的好处。
但是,这种方法没有解决整体结构的问题。要解决这些问题,我们需要分解整体。下面让我们看看相关的策略。
缩小单体应用程序的一种策略是将表示层与业务逻辑和数据访问层分开。典型的企业应用程序至少包含三种不同类型的组件:
通常,即便是在单体应用程序里,表示层的逻辑与业务和数据访问逻辑之间是完全分开。业务层具有由一个或多个组件组成的粗粒度API,这些API封装了业务逻辑组件。沿着该API将单体应用分割成两个较小的应用程序。一个应用程序包含表示层。另一个应用程序包含业务和数据访问逻辑。拆分之后,表示层的应用程序对业务逻辑应用程序进行远程调用。下图显示了重构前后的体系结构。
以这种方式拆分单体应用程序有两个主要好处。 它使我们可以彼此独立地开发,部署和扩展两个应用程序。 尤其是它允许表示层开发人员在用户界面上快速迭代并轻松执行A / B测试。 这种方法的另一个好处是,它公开 暴露了可以由我们进行开发的微服务调用的远程API。
但是,此策略只是部分解决方案。 一个或两个应用程序很可能也是无法管理的单体应用程序。 因此我们还需要使用第三种策略来消除剩余的一个或多个整体
第三种重构策略是将整体中的现有模块转变为独立的微服务。 每次提取模块并将其转换为服务时,整体都会收缩。 一旦转换了足够多的模块,单体应用程序将不再是问题。 它要么完全消失,要么变得足够小成为另一个微服务。
对将哪些模块转换为微服务进行优先级排序
大型,复杂的整体应用程序往往由数十个或数百个模块组成,所有这些模块都是进行抽取的候选对象。找出首先要转换的模块通常很困难。一个好的方法是从几个容易提取的模块开始。这将为我们提供一些通用的微服务经验,尤其是提取过程的经验。之后,我们应该提取那些将为项目带来最大利益的模块。
将模块转换为服务通常很耗时。您想按获得的收益对模块进行排名。提取经常更改的模块通常是有益的。将模块转换为服务后,就可以独立于整体而独立开发和部署它,这将加快开发速度。
提取对资源需求与其余单体程序资源明显不同的模块也是有益的。例如,将具有内存数据库的模块转换为服务非常有用,然后可以将该服务部署在具有大量内存的主机上。同样,提取实现计算成本高昂的算法的模块可能是值得的,因为随后可以将该服务部署在具有大量CPU的主机上。通过将具有特定资源需求的模块转换为服务,可以使应用程序更易于扩展。
在找出要提取的模块时,寻找现有的粗粒度边界很有用。它们使将模块转变为微服务变得更加容易。这种边界的一个示例是仅与应用程序其余部分通信的模块。
如何提取一个模块
提取模块的第一步是定义模块和整体之间的粗粒度界面。它很有可能是双向API,因为单体应用将需要微服务拥有的数据,反之微服务也需要单体应用的数据。由于模块和应用程序其余部分之间存在复杂的依赖关系和细粒度的交互模式,因此实现这样的API通常具有挑战性。由于域模型类domain model class之间存在大量关联,因此使用域模型模式实现的业务逻辑尤其难以重构。通常需要进行重大的代码更改才能打破这些依赖性。
一旦实现了粗粒度的接口,就可以将模块变成独立的服务。为此,我们必须编写代码以使整体组件和服务能够通过使用进程间通信(IPC)机制的API进行通信。下图显示了重构之前,期间和之后的体系结构。
在此示例中,模块Z是要提取的候选模块。模块X使用其组件,然后它又使用了模块Y的组件。第一步是要定义一对粗粒度的API。第一个接口是模块X用来调用模块Z的入站接口。第二个接口是模块Z用来调用模块Y的出站接口。
第二个重构步骤将模块转换为独立服务。入站和出站接口由使用IPC机制的代码实现。我们很可能需要通过将模块Z与处理诸如服务发现之类的跨领域问题的微服务机箱框架相结合来构建服务。
提取模块后,我们将拥有另一个可以独立于整体和任何其他服务开发,部署和扩展的服务。甚至可以从头开始重写服务。在这种情况下,将服务与整体集成在一起的API代码将成为在两个域模型之间转换的反腐败层。每次提取服务时,就朝着微服务的方向迈出了又一步。随着时间的流逝,整体将缩小,我们就将拥有越来越多的微服务
将现有应用程序迁移到微服务的过程是应用程序现代化的一种形式。 我们不应通过从头开始重写应用程序来转向微服务。 相反,应该将应用程序逐步重构为一组微服务。 我们可以使用三种策略:将新功能实现为微服务; 将表示组件与业务和数据访问组件分开; 并将整体中的现有模转换为服务。 随着时间的流逝,微服务的数量将会增加,而开发团队的敏捷性和速度也会增加。