07-单体应用重构为微服务

原文链接

这是我的系列文章中关于使用微服务构建应用程序的第七篇也是最后一篇文章。第一篇文章介绍了微服务架构模式,并讨论了使用微服务的优缺点。以下文章讨论了微服务架构的不同方面:使用API​​网关,进程间通信,服务发现,事件驱动的数据管理和部署微服务。

在本文中,我们将介绍将单体应用迁移到微服务的策略。我希望这一系列文章能让单主机对微服务架构,它的优点和缺点以及何时使用它有一个很好的理解。也许微服务架构非常适合单主机的组织。也许单主机正在处理大型复杂的单体应用,单主机每天开发和部署应用程序都很缓慢而且很痛苦。微服务似乎是一个遥远的必杀技,有一些策略可以用来摆脱单体应用的地狱。在本文中,我将介绍如何将单体应用逐步重构为一组微服务。

重构到微服务概述

将单体应用转换为微服务的过程是应用程序现代化的一种形式。这是开发人员几十年来一直在做的事情。因此,在将应用程序重构为微服务时,我们可以重用一些想法。

一种不推荐的的策略是推翻重写。这就是单主机将所有开发工作集中在从头开始构建基于微服务的新应用程序的时候。虽然听起来很吸引人,但它风险极大,很可能以失败告终。正如Martin Fowler所说的那样,“Big Bang重写的唯一保证是Big Bang!”

更合理的方式是,你应该逐步重构你的单体应用。单主机逐步构建一个由微服务组成的新应用程序,并将其与单主机的单体应用一起运行。随着时间的推移,单体应用程序实现的功能量会缩小,直到它完全消失或者变成另一个微服务。这种策略类似于以70英里/小时的速度在高速公路上行驶时为单主机的汽车提供服务,具有挑战性,但风险远低于尝试推翻重写。

Martin Fowler将此应用程序现代化策略称为Strangler应用程序。这个名字来自在热带雨林中发现的扼杀藤蔓(a.k.a. strangler fig)。为了到达森林树冠上方的阳光,荆棘藤生长在树周围。有时,树死了,留下了一棵树状的藤蔓。应用程序现代化遵循相同的模式。我们将构建一个新的应用程序,包括遗留应用程序周围的微服务,最终将会死亡。

让我们来看看这样做的不同策略。

策略1-停止挖掘

Holes法则认为每当你在一个洞里,你应该停止挖掘。当单主机的整体应用程序变得无法管理时,这是一个很好的建议。换句话说,你应该停止使整体更大。这意味着在实现新功能时,不应向单体应用添加更多代码。相反,这个策略的主要思想是将新代码放在一个独立的微服务中。下图显示了应用此方法后的系统架构。

image

除了新服务和传统单体应用之外,还有另外两个组件。

  • 请求路由器,它处理传入(HTTP)请求。它类似于前面文章中描述的API网关。路由器将对应于新功能的请求发送到新服务。它将遗留请求路由到单体应用。
  • 胶水代码,它将新服务与单体应用结合在一起。服务很少孤立存在,并且通常需要访问单体应用所拥有的数据。粘合代码驻留在单体应用程序,新服务或两者中,负责数据集成。该服务使用粘合代码来读取和写入单体应用所拥有的数据。

服务可以使用三种策略来访问单体应用的数据:

  1. 调用单体应用程序提供的远程API
  2. 直接访问单体应用的数据库
  3. 维护自己的数据副本,该数据与单体应用的数据库同步

胶水代码(也称为反腐败层),这是因为粘合代码阻止了具有其自己的原始域模型的服务被传统单体应用的域模型中的概念污染。胶水代码在两种不同的模型之间进行转换。

反腐败一词首先出现在Eric Evans的必读书《领域驱动设计》中,然后在白皮书中进行了细化。制定反腐败层可能是一件非常重要的事情。但是如果你想要从单体应用程序的地狱中走出来,那么创造一个是至关重要的。

将新功能实现为轻量级服务的好处:

  1. 可以防止整体变得更难以管理
  2. 该服务可以独立于整体进行开发,部署和扩展
  3. 可以为创建的每项新服务体验微服务架构的优势

但是,这种方法无法解决整体问题,要解决这些问题,需要打破单体应用。让我们来看看这样做的策略。

策略2-拆分前端和后端

缩小整体应用程序的策略是将表示层业务逻辑层数据访问层分开。典型的企业应用程序至少包含三种不同类型的组件:

  • 表示层:处理HTTP请求并实现(REST)API或基于HTML的Web UI的组件。在具有复杂用户界面的应用程序中,表示层通常是大量代码。
  • 业务逻辑层:作为应用程序核心并实现业务规则的组件。
  • 数据访问层:访问基础架构组件(如数据库和消息代理)的组件。

表示层与业务逻辑层和数据访问层之间通常存在清晰的分离。业务层具有粗粒度API,它由一个或多个部分组成,其封装业务逻辑组件。此API是一个天然接缝,单主机可以沿着该接缝将整体分割为两个较小的应用程序:

  • 一个应用程序包含表示层。
  • 另一个应用程序包含业务逻辑层和数据访问层。

分割后,表示层应用对业务逻辑层应用进行远程调用。下图显示了重构之前和之后的体系结构。

image

以这种方式拆分整体结构有两个主要好处:

  • 它使单主机能够彼此独立地开发,部署和扩展这两个应用程序。特别是,它允许表示层开发人员在用户界面上快速迭代并轻松执行A/B测试。
  • 它暴露了一个可以由单主机开发的微服务调用的远程API。

然而,这种策略只是部分解决方案,一个或两个应用程序很可能是一个难以管理的巨型应用。单主机需要使用第三种策略来消除剩余的整体块或整体块。

策略3-提取服务

将单体应用中现有模块转换为独立的微服务。每次提取模块并将其转换为服务时,单体应用都会缩小。一旦你转换了足够的模块,单体应用程序将不再是一个问题。它要么完全消失,要么变得足够小以至于它只是另一个微服务。

确定要转换为微服务的模块的优先级

大型复杂的单体应用程序由数十个或数百个模块组成,所有模块都是待转换的候选模块。首先确定转换哪些模块通常具有挑战性,一个好的方法是从一些易于提取的模块开始。这将为单主机提供通用的的微服务转换过程的经验,为之后单主机确定转换哪些带来最大好处的模块提供帮助。

将模块转换为微服务通常很耗时,按照获得的利益最大化对模块进行排名:

  • 转换经常变化的模块通常是有益的。将模块转换为微服务后,可以独立于整体开发和部署它,这将加速开发。
  • 转换具有与其他整体结构明显不同的资源要求的模块也是有益的。例如,将具有内存数据库的模块转换为微服务是有用的,然后可以将该服务部署在具有大量内存的主机上。
  • 转换实现计算成本昂贵的算法的模块是值得的,因为该服务可以部署在具有大量CPU的主机上。通过将具有特定资源需求的模块转换为微服务,可以使应用程序更容易扩展。

在确定要转换哪些模块时,查找现有的粗粒度边界(a.k.a接缝)很有用。它们使模块变成微服务更容易,迁移成本更低。这种边界的一个例子是仅通过异步消息与应用程序的其余部分通信的模块,它可以相对便宜并且易于将该模块转换为微服务。

如何转换模块

转换模块的第一步是在模块和整体结构之间定义粗粒度接口。它很可能是双向API,因为整体将需要服务拥有的数据,反之亦然。由于模块与应用程序其余部分之间存在纠缠的依赖关系和细粒度的交互模式,因此实现此类API通常具有挑战性。

由于域模型类之间存在大量关联,因此使用域模型模式实现的业务逻辑对于重构尤其具有挑战性。经常需要进行重要的代码变更以打破这些依赖关系。实现粗粒度界面后,即可将模块转换为独立服务。为此,必须编写代码以使单体应用和微服务能够通过使用进程间通信(IPC)机制的API进行通信。下图显示了重构之前,期间和之后的体系结构。

image

在此示例中,模块Z是要转换的候选模块。它的组件由模块X使用,它使用模块Y。

  • 第一个重构步骤是定义一对粗粒度的API。

    • 第一个接口是模块X用于调用模块Z的入站接口。
    • 第二个接口是模块Z用于调用模块Y的出站接口。
  • 第二个重构步骤将模块转换为独立服务。

入站和出站接口由使用IPC机制的代码实现。很可能需要通过将Module Z与微服务框架相结合来构建服务,该框架处理诸如服务发现之类的跨领域问题。一旦转换了一个模块,就可以开发,部署和扩展另一个独立于整体和任何其他服务的模块。甚至可以从头开始重写服务;在这种情况下,将服务与整体结构集成的API代码成为一个反腐败层,可在两个域模型之间进行转换。每次提取服务时,都会朝着微服务的方向迈出新的一步。随着时间的推移,整体结构将缩小,将拥有越来越多的微服务。

总结

将现有应用程序迁移到微服务的过程是应用程序现代化的一种形式。不应该通过从头开始重写单体应用来转向微服务。相反,单体应用应该逐步将应用程序重构为一组微服务。

单体应用转换为微服务可以使用三种策略:

  1. 将新功能实现为微服务
  2. 从业务逻辑层和数据访问层中拆分表示层
  3. 将整体中的现有模块转换为服务

随着时间的推移,微服务的数量将会增长,开发团队的敏捷性和速度将会提高。

上次修改: 14 April 2020