微服务入门系列:单体应用的微服务重构

本文是下面七篇系列文章的最后一篇:

  1. 微服务入门系列:微服务介绍
  2. 微服务入门系列:使用 API 网关
  3. 微服务入门系列:微服务架构的进程间通信
  4. 微服务入门系列:微服务架构的服务发现
  5. 微服务入门系列:基于事件驱动的数据管理
  6. 微服务入门系列:微服务的部署策略选择
  7. 微服务入门系列:单体应用的微服务重构(本文)

出自微服务部落:https://blog.idevfun.io/refactoring-a-monolith-into-microservices/
原文链接:https://www.nginx.com/blog/refactoring-a-monolith-into-microservices/
作者:Chris Richardson
翻译:董干

这 7 篇文章是很不错的微服务入门系列文章,介绍了微服务所需要的主要技术,是当年(本系列文章写于2015年,但现在仍然不过时)带领译者入门微服务的文章,强烈建议深入微服务架构之前先了解本系列。


本文是我关于构建微服务应用系列的第七篇也是最后一篇文章。第一篇介绍了微服务架构模式,并讨论了微服务的优势和缺点。接下来的几篇分别从不同角度描述了微服务架构的各个方面:使用 API 网关进程间通信服务发现基于事件驱动的数据管理 以及 微服务的部署策略。这篇文章我们将讨论单体应用迁移为微服务的一些策略。

希望这个系列的文章能帮助你更好理解微服务架构、它的优点和缺点以及什么时候适合使用它。也许微服务架构对你的组织来说是合适的。

然而,相当大的可能性是你正在一个巨大的、复杂的单体应用上工作。每天的开发和应用部署体验既慢又痛苦。微服务看起来似乎像是远处的理想图景。幸运的是,从单体应用地狱逃脱还是有一些策略的。在这篇文章里,我将介绍如何从单体应用增量地重构为一组微服务。

微服务重构概述

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

一种应该避免的策略是使用 “大爆炸” 式的重写。这种方式是指将所有的开发力量聚焦到从零开始构建一个全新的基于微服务的应用。尽管听起来很有吸引力,这种方式风险非常大,很大可能会以失败告终。正如 Martin Fowler(马丁·福勒) 据称所说,“大爆炸式重写唯一能保证的就是大爆炸!”。

你应该增量地重构单体应用,而非大爆炸式地重写。也就是说逐渐地构建由一些微服务组成的新的应用,然后连同单体应用一起运行。随着时间发展,单体应用所实现的功能不断缩小至完全消失,或者成为另一个普通微服务。这个策略类似于在高速公路上以 70 迈的速度一边开着车一边检修车辆 —— 很有挑战,但比尝试大爆炸式重写风险要小得多。

Martin Fowler 称这种应用程序现代化改造的策略为 绞杀者应用 (译者注:绞杀者模式,Strangler Pattern,参见 https://docs.microsoft.com/zh-cn/azure/architecture/patterns/strangler )。这个名称来自热带雨林里发现的绞杀藤(见下图)。绞杀藤围绕大树向上生长,以获取森林顶部以上的阳光。有时,被围绕的树死亡,只留下树形的蔓藤。应用的现代化改造遵循同样的模式。我们将围绕老应用构建由一组微服务组成的新应用,并使老应用最终逐渐消亡。

Richardson-microservices-part7-fig

我们接下来继续讨论完成这项工作的不同策略。

策略 1 —— 停止挖掘

第一洞窟定律 说的是不论何时当你发现自己处于一个洞窟中,你首先应该停止挖掘。这是当你的单体应用变得不可管理时应该遵循的一条很好的建议。换句话说,不应该使得单体应用变得更大。这意味着当你在实现新的功能时,不该继续往单体应用中添加新的代码。相反,这种策略的主要思想主张将新的代码放入单独的微服务。下图表示了应用这种方法后的系统架构。

Adding_a_secure_microservice_alongside_a_monolithic_application-1024x865

除了新服务和遗留的单体应用,图中还有两个组件。一个是请求路由器,它负责处理进入系统的 HTTP 请求,和之前的文章中描述的 API 网关 类似。这个路由器将新功能对应的请求发送到新服务中,而将老的请求路由到遗留的单体应用中。

另外一个组件是胶水代码,它将服务和单体应用集成起来。服务很少能够隔离存在,它通常需要访问单体应用所需要的数据。胶水代码可能存在于单体应用中,也可能存在与服务中,也可能并存,它负责数据层面的集成。服务使用胶水代码读写单体应用的数据。

服务访问单体应用的数据有以下三种方式:

  • 请求单体应用提供的远程 API
  • 直接访问单体应用的数据库
  • 维护自己的一份数据,并和单体应用的数据库同步

胶水代码有时也被称为反腐层 (anti-corruption layer) 。原因是胶水代码阻止拥有崭新领域模型的服务被遗留单体应用的领域模型污染。胶水代码在两种模型之间进行翻译。反腐层这个术语首次出现在 Eric Evans 所著的必读书籍《领域驱动设计(Domain Driven Design)》中,后在一篇白皮书中进行了进一步细化。开发反腐层并非一项容易的工作,然而如果你想逃离单体地狱,它却至关重要。

将新功能实现为轻量的服务有很多好处。它阻止单体应用变得更加难以维护和管理。服务可以独立于单体应用进行开发、部署和扩缩容。你可以在新创建的每个服务中体验微服务架构的好处。

但是,这种方式并不能解决单体应用自身的问题。为了解决这些问题,你需要将单体应用肢解。接下来我们看一下这么做具体的策略。

策略 2 —— 前后端分离

使单体应用逐渐缩小的一个策略是将表示层同业务逻辑层和数据访问层分离开。一个典型的企业级应用包括至少以下三种不同类型的组件:

  • 表示层 —— 这类组件处理 HTTP 请求,实现(REST)API 或基于 HTML 页面的 UI。在拥有复杂用户接口的应用中,表示层通常具有很大体量的代码。
  • 业务逻辑层 —— 应用程序的核心组件,实现业务规则。
  • 数据访问层 —— 这类组件访问基础设施组件,例如数据库和消息队列等。

表示层逻辑作为一侧,同业务逻辑和数据访问层作为另一侧,两者之间通常具有明显的分隔。业务逻辑层有一个或多个门面组成的粗粒度 API,这些门面将业务逻辑组件封装起来。这些 API 很自然地可以将单体应用拆分为两个小些的应用。其中一个应用包含表示层,另一个包含业务逻辑和数据访问层。拆分以后表示层逻辑发起远程调用到业务逻辑应用。下面的图表示了重构之前和之后架构的变化。

Richardson-microservices-part7-refactoring

照这种方式拆分单体应用主要有两个好处。它使得独立开发、部署和扩缩容两个应用成为可能。尤其地,它允许表示层开发者们能够快速迭代用户界面并很容易地实施 A/B 测试。这种方式的另一个好处是它暴露了可以被你所开发的其它微服务调用的远程接口。

不过,这个策略只是解决方案的一部分。很有可能这其中的一个或者两个应用全部都成为无法管理的巨型单体应用。你需要使用第三个策略来消除残余的单体应用。

策略 3 —— 抽取服务

第三个重构策略是将单体应用中现有的模块变为独立的微服务。每当抽取一个模块并转变为微服务时,单体应用就缩减一些。当转变了足够多的模块后,单体应用原先的问题逐渐就消失了。或者它完全消失,或者足够小到成为另一个微服务。

确定转变为服务的模块的优先级

复杂的大型单体应用由数十甚至数百模块组成,它们都是即将被抽取的候选。确定哪些模块应该先转化通常比较难。比较好的方式是从容易抽取的一些模块开始,它们将带来一些微服务的主要体验,尤其是抽取过程。之后,应该抽取效果收益最大的那些模块。

将模块转化为服务通常都比较耗时,因此你会希望将模块按照得到的效果收益排序。通常来说抽取经常变化的模块取得的收益较好。一旦当你将模块转化为服务,就可以独立于单体应用开发和部署它,进而提升开发效率。

另外,将有着同单体应用其它部分有着完全不同的特殊资源要求的模块抽取出来也会取得较大收益。例如,将使用嵌入式内存数据库的模块转变为服务比较有用,因为这样就可以部署在拥有较大内存的主机上。类似地,将实现了计算复杂度很高的 CPU 密集型模块抽取成为服务可能是值得的,因为这样服务就可以部署在 CPU 较多的主机上。将有着特殊资源要求的模块转化为服务,可以使得应用更容易扩容。

当研究抽取哪个模块时,找寻现存的粗粒度边界(即接缝)会比较有用,它们能使模块更容易且代价更小地转化为服务。这种边界的一个例子是只通过异步消息同应用程序的其它部分通信的模块。它可以相对代价小且简单地将模块转变为微服务。

如何抽取模块

抽取模块的第一步是粗略定义模块和单体应用之间的接口。很可能是双向的 API,因为单体应用会需要服务的数据,反之亦然。因为互相缠绕的依赖以及模块与应用程序其它部分细粒度的交互模型,实现这样的 API 通常都很难。尤其是使用了 领域模型模式 实现的业务逻辑由于领域模型之间繁多的关联,导致重构起来非常有挑战。你通常要做很大的代码改动才能打破这些依赖关系。后面的图示意了重构的过程。

一旦实现了粗粒度的接口,你就可以将模块转化为自由存在的服务。为了实现它你需要使用某种 进程间通信(IPC)机制编写代码使得单体应用和服务之间可以通过 API 进行通信。下图示意了重构前、重构进行中以及重构后的架构。

Richardson-microservices-part7-extract-module

在上图的例子中,模块 Z 是即将被抽取的模块。它的组件被模块 X 使用,它本身使用模块 Y。重构的第一步是定义一对粗粒度的 API。第一个是是用于模块 X 调用模块 Z 的向外接口。第二个是用于模块 Z 调用模块 Y 的向内接口。

重构的第二步将模块转变为独立的服务。向外和向内的接口使用 IPC 机制实现。通常来说模块 Z 需要联合 微服务底盘框架 处理像服务发现这样的业务逻辑以外的问题。

一旦当抽取完模块,你就完成了另一个可以独立于单体应用和其它任何服务进行开发、部署和扩容的服务。你甚至可以从头重写这个服务,在这种场景下,将服务集成到单体应用的 API 代码则成为了能够在两种领域模型之间互相翻译的反腐层。每次抽取完一个服务,你就朝微服务的方向更迈进了一步。假以时日,单体应用逐渐缩小,最终你将得到数量逐渐增多的微服务。

总结

将现存的应用迁移为微服务的过程实际是应用现代化的过程。你不应该用重写整个应用的方式迁移到微服务,而应该逐渐增量地将应用程序重构为微服务。有三个策略可以使用:将新增功能使用微服务实现;将表示层组件同业务逻辑和数据访问组件分离;将单体应用的现有模块转化为服务。微服务的数量随着时间逐渐增长,而开发团队的敏捷性和速度则会逐渐增加。


本文是下面七篇系列文章的最后一篇:

  1. 微服务入门系列:微服务介绍
  2. 微服务入门系列:使用 API 网关
  3. 微服务入门系列:微服务架构的进程间通信
  4. 微服务入门系列:微服务架构的服务发现
  5. 微服务入门系列:基于事件驱动的数据管理
  6. 微服务入门系列:微服务的部署策略选择
  7. 微服务入门系列:单体应用的微服务重构(本文)

出自微服务部落:https://blog.idevfun.io/refactoring-a-monolith-into-microservices/
原文链接:https://www.nginx.com/blog/refactoring-a-monolith-into-microservices/
作者:Chris Richardson
翻译:董干

这 7 篇文章是很不错的微服务入门系列文章,介绍了微服务所需要的主要技术,是当年(本系列文章写于2015年,但现在仍然不过时)带领译者入门微服务的文章,强烈建议深入微服务架构之前先了解本系列。

Show Comments

Get the latest posts delivered right to your inbox.