7-从单体应用迁移到微服务
本文出自Nginx官网,是微服务介绍系列文章的第七篇,也是最后一篇。原文地址:https://www.nginx.com/blog/refactoring-a-monolith-into-microservices/
1.介绍
这是微服务系列文章的最后一篇,在第一篇文章中我们比较了微服务架构应用和单体应用的差异,讨论了微服务架构的优点与缺点;随后又讨论了使用API网关、进程间通信、服务发现、处理分布式数据管理、微服务部署等内容;在本篇文章中我们讨论将单体应用迁移到微服务的策略。
希望这个系列能让你了解微服务的架构、微服务的好处和缺点、什么时候选用微服务;微服务架构在很多情况下是更好的选择,可你很可能正工作于巨大、复杂的单体应用,每天的开发、维护、部署工作,效率低下、充满痛苦;而微服务看起来像是遥不可及的美梦。所幸,从单体应用迁移到微服务架构,有可以遵循的策略,这篇文章中,我们就讨论这些策略。
2.迁移工作概述
将单体应用迁移到微服务的过程,是应用现代化的过程,这是全世界开发人员十几年来一直努力在做的事情,有一些好的思想可以借鉴。
不推荐使用“大爆炸”式的将原单体应用推倒重写的策略,“大爆炸”听起来很美好,实际上风险很大,最终很可能失败。就像Martin Fowler所说的,“大爆炸式的推倒重来,唯一能保证的就是大爆炸式的灾难后果”。
应该采用渐进式重构的方式进行迁移,逐步构建由微服务构成的新应用,让它跟单体应用协同工作;慢慢的,单体应用实现的功能越来越少,直至完全消失或者变成另外一个微服务。这种策略类似于向在高速公路上以70英里时速行驶的汽车提供服务,具有挑战性,但风险远低于大爆炸式的重写。
Martin Fowler将这种应用现代化的策略称为“Strangler Application”,这个名字来自于热带雨林的一种植物“strangler vine”。strangler vine沿着树干向上生长,以获取阳光;最后大树死去,只剩下树形的strangler vine。应用现代化的过程类似于这种模式,我们在单体应用的周围构建由微服务组成的新应用,直至原有应用死去。接下来,我们讨论不同的迁移策略。
3.策略1-停止挖坑
深坑法则告诉我们,掉进深坑后第一件要做的事情,就是别再继续挖坑。当单体应用已经大到不可维护时,这个建议非常有价值;换句话说,别让单体应用更大了。这意味着不要向单体应用添加新的代码,应该将新代码放到独立的微服务中,下图展示了这种结构:
在上图中,除了遗留的单体应用和新构建的服务之外,还有两部分内容:第一部分是处理HTTP请求的请求路由器,有点儿类似于之前描述的API网关;路由器将新服务的请求转发给服务,将老请求转发给遗留的单体应用。另外一部分是将服务和单体应用组合在一起的胶水代码,通常新服务不会独立存在,它依赖单体应用的数据。胶水代码可能存在于微服务、单体应用,或者两者都有,它负责组合数据;微服务使用胶水代码读写单体应用拥有的数据。
服务使用三种策略访问单体应用的数据:使用单体应用的API远程调用;直接访问单体应用的数据库;维护单体应用数据的一份拷贝。
胶水代码也被称作反腐败层,它防止服务自身领域模型受到单体应用领域模型的污染;胶水代码在两个不同的模型之间进行翻译。如果你想从单体应用的地狱中走出,胶水代码是至关重要的工作。
将新功能构建成服务有很多好处:它阻止单体应用变得更大;服务可以独立于单体应用进行开发、部署和扩展。你能体验到使用微服务架构创建的每一个服务带来的好处。
停止挖坑的做法没有让事情变得更坏,但它没有解决原有单体应用的任何问题,为了解决问题,需要拆分单体应用,接下来将描述单体应用拆分的策略。
4.策略2-拆分前端和后端
这种策略通过将表示层从业务逻辑层和数据层中拆分,实现单体应用规模的缩减。通常企业应用至少包括三层:
表示层:处理HTTP请求,实现REST风格API或者Web UI。如果应用有比较复杂的用户界面,表示层一般会有很多代码。
业务逻辑层:是应用的核心,实现业务逻辑。
数据访问层:访问类似数据库和消息中间件等基础实施。
一般情况下,表示层和另外两层有比较明显的区分。业务逻辑层会使用一个或者几个façade模式封装业务逻辑,对外提供粗粒度的API;这些API就是拆分应用的天然接缝。你可以沿着API将应用拆分,一个应用包含表示层代码,另外一个包含业务逻辑和数据访问代码,表示层应用远程调用业务逻辑应用提供的服务。以下图示展示了拆分前和拆分后的架构:
通过上述策略拆分单体应用有两个好处:两个应用可以独立的开发、部署、扩展,允许开发人员在用户界面上快速迭代并轻松执行A/B测试;应用暴露了远程访问的接口,可以供后续开发的其他服务调用。
当然,经过上述拆分之后仍然没有从根本上解决问题,原应用很可能变成两个同样无法维护的单体应用,那就需要使用第三个策略进行进一步拆分。
5.策略3-服务抽取
这个策略是将单体应用的模块抽取成服务,每抽取出一个服务,单体应用就缩减一点儿;当你抽取出足够多的服务时,单体应用就会完全消失或者转化为另外一个服务,问题就会完全解决。
确定服务抽取的优先级
大型应用通常有数十个甚至数百个模块,所有模块都可以进行服务抽取,如何确定服务抽取的优先级是个有挑战的工作。一个好的办法是从容易进行服务抽取的模块开始,这能让你积累微服务构建和服务抽取的经验,接下来应该抽取那些能带来最大好处的模块。
将应用模块转化成服务是耗费时间的工作,应该根据转换工作能带来的好处确定优先级。通常来说,抽取经常变化的模块能带来较大的好处,一旦你将模块抽取成服务,它就能独立的开发和部署,从而提高开发和部署效率。
将与其他模块资源需求明显不同的模块抽取成服务,也能带来很大好处。比如,将使用内存数据库的模块抽取成服务,它就能部署在有大内存的主机上;同样,将需要大量计算资源的模块抽取成服务,就能将它部署到有优异计算性能的主机上;通过将不同资源需求的模块抽取成服务,可以让应用更容易扩展。
当确定要抽取哪些模块时,查找粗粒度的模块边界很有用,能简化服务抽取工作。比如,如果有个模块的边界是仅通过异步消息跟其他模块通信,那将该模块抽取成服务就相对简单。
如何进行服务抽取
第一步是抽取出候选模块和单体应用的接口,既然单体应用和候选模块都需要对方的数据,这个接口很可能是双向依赖的;由于候选模块和单体应用之间复杂的依赖关系,且往往存在细粒度的交互,抽取接口通常是困难的。重构使用领域模型实现的业务逻辑模块更加困难,因为在领域模型的不同类之间往往存在大量复杂的交互,需要改动很多代码来减少相互依赖。
一旦实现了粗粒度的接口,候选模块就变成了独立服务,候选模块和单体应用之间采用进程间通信机制。以下图示展示了重构前、重构中、重构后的架构:
在这个例子中,计划将Z模块抽取成服务,它调用Y模块,又被X模块调用。第一步是定义一对接口,一个接口是入站接口,X模块用来调用Z模块;另外一个接口是出站接口,Z模块用来调用Y模块。
第二步是将Z模块转化为独立的服务:使用进程间通信机制实现出站、入站两个接口;组合微服务基础框架和Z模块,实现服务发现等功能。
一旦你抽取出一个模块,你就可以开发、部署、扩展独立于应用和其他服务的另一个服务;你甚至可以重写服务;在这种情况下,例子中的两个接口起到了防止腐败的作用,它们集成候选模块和单体应用,并在两者的领域模型之间进行翻译。每抽取出一个服务,应用就朝微服务迈出一步,慢慢的,微服务会逐渐壮大,单体应用会逐渐缩减。
6.总结
将单体应用迁移到微服务是应用现代化的过程,不要从头重写应用,应该逐步重构。有三种策略可以使用:将新功能构建成服务,将表示层构建成服务,将已有模块抽取成服务,随着微服务数量的不断增加,开发团队的敏捷性和开发效率会不断提升。