如果你的工作围绕一个大型的,复杂的单体应用,可能你每天开发和部署应用的工作都是进展缓慢而痛苦的。微服务看起来像是一个遥不可及的天堂,幸运的是,有方法可以帮助你逃离单体架构的地狱。本文将会介绍如何逐步地将单体应用改造为一系列的微服务。
概述
将单体应用改造为微服务实际上是应用现代化的过程,这是开发者们在过去十年来一直在做的事情,所以已经有一些可以复用的经验。
全部重写是绝对不能用的策略,除非你要集中精力从头构建一个基于微服务的应用。虽然听起来很有吸引力,但是风险很大,很有可能会失败。就像MartinFowler所说的:『The only thing a Big Bang rewrite guarantees is a Big Bang!』
你应该循序渐进地重构你的单体应用。你可以逐步地构建一个部分微服务化的应用,然后和你的单体应用集成起来。单体应用的实现的功能会逐渐变少,最终消失或变成一个新的微服务组件。
Martin Fowler称这种应用现代化的策略为Strangler Application。这个名字来源于在热带雨林发现的一种植物Strangler vine,为了够到充足的阳光,它们绕树生长,一直向上。当树木死后,只会留下一个树形的藤蔓。应用的现代化就是类似的模式,我们会在旧有的应用上,构建一个新的包含微服务的应用,慢慢取代旧的应用。下面一起来看下这些策略:
策略1:止损
Law of Holes告诉我们,如果你正在一个洞里,就不要继续再挖了。当你的单体应用已经变得无法管理的时候,就不要再继续扩大它的规模了。比如你想添加新功能,不要在单体应用中添加代码,而要将新的代码放在另一个单独的微服务中。下图展示了使用这种方法后的系统架构:
除了新服务和旧的单体应用,还有两个组件。一个是请求路由(request router),用来处理过来的(比如HTTP)请求,类似于API网关。这个路由发送与新功能相应的请求到新的服务上,将旧服务相关的请求路由到单体应用上。
另一个组件是胶水代码(glue code),用来将服务与单体应用集成起来。一个服务很少是隔离存在的,需要访问单体应用的数据。胶水代码就负责这些数据集成。微服务组件可以通过它来读写单体应用中的数据。
一个服务可以通过三种方式访问单体应用中的数据:
- 通过调用单体应用提供的远程API
- 直接访问单体的数据库
- 保存一份数据的副本,和单体数据库保持同步
胶水代码有时被称为防腐层(anti-corruption layer),可以防止拥有自己原始领域模型的服务,被来自单体领域模型的概念所影响。
胶水代码可以在两个不同的模型间充当翻译官,防腐层这个词最初出现在Eric Evans写的《Domain DrivenDesign》一书中。开发一个防腐层不是一个小工程,但如果你想从单体地狱中走出来,这是很重要的。
用轻量级的服务实现一个新功能,有很多好处。首先,可以防止单体应用变得更难以管理;其次,这个应用可以被独立地开发,部署和扩展。
然而,这个方法并不能解决在旧有的单体部分遇到的问题,你还需要破坏原有的单体部分。
策略2:前后端分离
缩减单体应用的一个策略是将表现层从业务逻辑和数据访问层中分离出来,一个典型的企业应用至少包括三种组件:
- 表现层:这层组件用来处理HTTP请求,实现(REST)API或者基于HTML的Web
UI。在一个有着复杂的用户接口的应用中,表现层通常有大量的代码; - 业务逻辑层:应用的核心代码,用来实现业务规则;
- 数据访问层:访问数据库或信息中介的组件。
在表现逻辑与业务和数据访问逻辑之间通常有着明显的区分。业务层有一个粗粒度的API,包含一个或多个外立面组成,外立面封装了业务逻辑组件。这个API是自然的『缝合』,所以可以将单体分割为更小的应用,一个应用包含了表现层,另一个应用包含了业务和数据访问层。分隔后,表现逻辑层应用可以远程调用业务逻辑层应用,下图展示了改造前后的架构:
这样分隔单体应用有两个主要好处。首先你可以独立地开发,部署和扩展两个应用,比如对于表现层开发者来说,他们可以实现用户界面的快速迭代,A/B测试也很容易实现;其次,这样做会向外开放一个微服务也可以调用的远程API。
但是这个策略只是部分解决方案,很有可能会变成两个混乱的单体应用。需要用下面第三个策略去减少单体部分的比重。
策略3:提取服务
第三个策略的目的是将单体中的模块,转变为单独的微服务。每次提取一个模块,就改造为微服务,单体部分就缩减了。一旦你转化了足够的模块,最后不管单体部分是完全消失了,还是变小成了另一个微服务,都不是问题了。
优先改造哪个模块?
一个大型的复杂的单体应用,通常包含数十甚至上百个模块,都可以被提取出来,选择先提取哪个是个问题。可以从容易被提取的开始,积累微服务的经验,然后提取那些能给你带来最大好处的模块。
通常提取那些频繁变化的模块很有用,一旦你将这个模块提取出来,就可以独立开发和部署它了,可以加速开发。
另外一个就是提取那些资源需求和其它部分有很大不同的模块。比如将一个有内存数据库的模块转变为服务,就可以把它部署在内存很大的主机上;同样的,提取那些实现复杂算法的模块,就可以把它部署在CPU多的主机上。总之这样做有助于你扩展应用。
当决定了提取哪个模块后,需要看下现有的粗粒度边界,可以帮助你将模块转化为服务。比如一个只会通过异步信息和其它应用交互的模块,就很容易能被改造为微服务。
如何提取一个模块?
第一步是在模块和单体之间定义一个粗粒度的接口。由于单体和微服务的数据互相都有需求,所以它很像一个双向的API。但是在这个模块和应用的剩余部分之间,有着混乱的依赖关系和细粒度的交互模式,所以实现这个API还是很有挑战的。通过域模型实现的业务逻辑,改造起来尤其困难,因为各个域模型间的关系复杂。通常需要进行大量的代码修改,去打破这些依赖。
一旦你实现了细粒度的接口,就可以将模块改造为一个独立的服务。要写代码实现单体和微服务间的通信,通过使用了IPC机制的API。下图展现了一个架构在改造前,改造中和改造后的样子:
在这个例子中,模块Z是待提取的模块。模块X使用了Z的组件,Z又使用了模块Y。
改造的第一步是定义一对粗粒度的API,第一个接口是模块X调用模块Z的入站接口,第二个是模块Z调用模块Y的出站接口;
第二步是将这个模块改造为独立的服务。入站接口和出站接口都通过IPC机制的代码实现。可能你需要通过结合模块Z到微服务底盘框架(用来处理横切关注点,比如服务发现)上,来构建这个服务。
一旦你提取了这个模块,就可以独立地开发,部署和扩展它了。你甚至可以从头重写这个服务,将这个服务和单体结合起来的API代码就成了防腐层,相当于两个域模型之间的翻译官。每次提取一个服务,都是向着微服务又进了一步,单体的比重会逐步缩减。
总结
将单体架构改造为微服务的过程是一种应用现代化的形式,不应该从头重写来实现。而是应该循序渐进地改造你的应用成为一系列微服务。有三种策略可以应用:用微服务实现新的功能;将表现层组件从业务和数据访问组件中分离出来;改造单体中的模块为微服务。随着时间的推移,你的微服务比重会增大,你的开发团队的灵活性和速度也会提高。