编者的话 |本文来自 Nginx 官方博客,是「Chris Richardson 微服务」系列的最后一篇。第一篇介绍了微服务架构模块,并且讨论了使用微服务的优缺点。随后的文章讨论了微服务的不同方面,包括使用 API 网关、进程间通讯、服务发现、事件驱动的数据管理,以及部署微服务。本篇将讨论从单体应用迁移到微服务的策略。
作者介绍:Chris Richardson,是世界著名的软件大师,经典技术著作《POJOS IN ACTION》一书的作者,也是 cloudfoundry.com 最初的创始人,Chris Richardson 与 Martin Fowler、Sam Newman、Adrian Cockcroft 等并称为世界十大软件架构师。
Chris Richardson 微服务系列全 7 篇:
1. 微服务架构概念解析
2. 构建微服务架构:使用 API Gateway
3. 深入微服务架构的进程间通信
4. 服务发现的可行方案以及实践案例
5. 微服务的事件驱动数据管理
6. 选择微服务部署策略
7. 将单体应用改造为微服务(本篇文章)
使用微服务重构概述
将单体应用转变为微服务的过程也是将应用现代化的过程,数十年来开发者们一直致力于此。因此,当把应用重构为微服务的时候,我们可以借鉴其中的理念。
首先不要大规模地重写代码。大规模重写代码意味着你需要集中全部开发力量、从头构建全新的基于微服务的应用;听起来吸引人,但是充满风险,有可能以失败告终。正如 Martin Fowler 所言,“大规模重写唯一能够保证的只有大规模!”
相反,应当采取逐步重构单体应用的策略。逐步构建一个由微服务构成的应用,与单体应用并行运行;随着时间推移,原先由单体应用实现的功能不断收缩,最后或者完全消失,或者转变为微服务。这一方法虽然充满挑战,但风险远小于大规模重写代码。
Martin Fowler 将这一应用现代化的策略称为“杀手应用”。这一名称源自热带雨林中的杀手藤。杀手藤附生于树,直达树冠上方;树死后,留下树状的藤蔓。应用的现代化也是遵循这一模式。微服务构成的新应用围绕着遗留应用,后者最终完全不复存在。
接下来了解不同的实现策略。
策略一:停止挖坑
挖坑第一法则指出,如果发现自己掉坑里,马上停止。这一忠告也适用于难以管理的单体应用。换句话说,应该停止让单体应用继续变大,也就是在实现新功能的时候,不应该再增加代码。相反,这一策略的理念在于,把这部分新代码开发称为独立的微服务。下图展示了采用此方法的系统架构。
除了新服务和遗留应用,这个系统还包括另外两个组件。其一是请求路由,处理传入(HTTP)请求,与之前文章中描述的 API 网关类似。路由将与新功能对应的请求发送到新服务,将遗留请求发送到已有的单体应用。
另一组件是胶水代码(glue code),负责集成微服务与单体应用。微服务很少孤立存在,通常需要访问单体应用拥有的数据。胶水代码存在于单体应用或微服务中,或者两者兼有,负责数据集成。微服务使用胶水代码来读取和写入单体应用拥有的数据。
微服务可以通过以下三种方式访问单体应用的数据:
- 调用由单体应用提供的远程 API
- 直接访问单体应用的数据
- 维护一份数据拷贝,与单体应用的数据库保持同步
胶水代码也被称作防崩溃层(anti-corruption layer)。对于拥有自己全新领域模型的微服务,胶水代码能够阻止其受到遗留单体应用的领域模型的污染,并且为这两种模型提供转换。防崩溃层这一术语最早出现于 Eric Evans 撰写的必读书 Domain Driven Design 中,并被提炼成为白皮书。开发防崩溃层是项不平凡的工作,要想远离单体应用的泥淖,创建防崩溃层必不可少。
以轻量级微服务的方式实现新功能有诸多优点。它能够防止单体应用变得不可管理。微服务能够独立于单体服务进行开发、部署和扩展。采用微服务能让开发者切身感受其好处。
然而,这一方法并没有解决单体应用的问题。要想解决这些问题,需要分解单体应用。
策略二:拆分前端和后端
缩小单体应用的策略之一是将表示层(presentation layer)与业务逻辑和数据访问层分离。典型的企业应用包括至少三类组件:
- 表示层:处理 HTTP 请求并实现 (REST)API 或基于 HTML 的 Web UI。对于包含复杂用户接口的应用,表示层往往是代码的实体部分。
- 业务逻辑层:应用的核心,实现业务逻辑
- 数据访问层:访问诸如数据库和消息代理这样的基础架构组件
在表示逻辑与业务和数据访问逻辑之间,有着清晰的间隔。业务层的粗粒度的 API 由若干方面组成,内部封装业务逻辑组件。这个 API 是一道天然分界线,将单体应用分割成两个较小的应用。一个应用包含表示层,另一个应用包含业务和数据访问逻辑。拆分后,表示逻辑应用对业务逻辑应用远程调用。下图展示了重构前后的构架。
以这种方式切割单体应用有两大好处。首先,它使得这两个应用的开发、部署和扩展用各自独立。尤其是,它使得表示层的开发人员能够快速迭代用户接口,轻松进行 A|B 测试。其次,它暴露了远程 API,能够被微服务调用。
这种策略也只是部分解决方案,很有可能其中一个应用或两个应用变成难以管理的单体应用。这时需要使用第三种策略来消除剩余的单体应用。
策略三:提取微服务
第三种重构策略是将单体应用内现有的模块转变为独立的微服务。每当提取模块并将其转化为服务,单体应用就会收缩。一旦转化了足够的模块,单体应用也不再是问题,它或者彻底消失,或者小到成为另一个微服务。
为需要转化为微服务的模块设置优先级
大型、复杂的单体应用由数十甚至数百个模块组成,每个都是提取的对象。要弄清楚哪些模块首先被转化,往往具有挑战性。从易于提取的模块开始是个好方法,它能让开发者熟悉微服务和提取过程。然后就应该转化能从中获益最多的模块。
鉴于把模块转变为微服务非常耗时,一般会根据获益程度来给模块排序。从频繁更改的模块开始会让用户收获不菲。一旦把模块转化为微服务,也就能独立开发和部署,从而加速开发进度。
将资源需求大不相同的模块优先转化,也颇有好处。例如,把内存数据库模块转化为微服务,能够被部署在大内存主机上。同样,将实现计算算法的模块提取出来也是非常值得,这一微服务能够部署在拥有大量 CPU 的主机上。通过将对资源有着特殊需求的模块转变为微服务,应用能够易于扩展。
找出哪些模块需要优先提取后,找出现有粗粒度的边界(即分界线)也大有裨益。这些边界让模块转变为微服务更加简单、省力。例如,通过异步消息与应用的其它部分通信的模块能够相对省力、简便地转化为微服务。
如何提取模块
模块提取的第一步是定义模块和单体应用间的粒度接口。由于单体应用和微服务互相需要对方拥有的数据,因此更像是双向 API。由于模块和应用其它部分之间存在着互相依赖和细粒度的交互模型,因此实现这样的 API 充满挑战。对于重构微服务,通过领域模型实现的业务逻辑尤为挑战,开发人员需要大刀阔斧地修改代码来打破这些依赖。
粗粒度接口一旦完成,模块也就变成了独立的微服务。要做到这一点,开发人员必须编写代码,能够让单体应用和微服务通过 API 通信,API 使用进程间通信(IPC)机制。 下图展示了重构前、重构中和重构后的不同架构。
在图片中,模块 Z 将要被重构,它用到了模块 Y 的组件,同时它的组件被模块 X 使用。重构的第一步就是定义一对粗粒度 API。第一个接口是模块 X 使用的对内接口,用来唤醒模块 Z。第二个接口是模块 Z 使用的对外接口,唤醒模块 Y。
重构的第二步则是把模块转变为独立的微服务。对内和对外接口通过 IPC 机制的代码实现,开发人员可能只需要将模块 Z 与微服务支撑框架(Microservice Chassis framework)组合起来构建微服务。微服务支撑框架处理与割接相关的问题,比如服务发现。
一旦将模块提取完毕,相当于得到一个微服务,能够独立于单体应用和其它微服务进行开发、部署和扩展。如果要从头重写微服务代码,集成微服务和单体应用的 API 代码会成为这两个领域模型之间的防崩溃层。每重构一个组件,就向着微服务的方向又迈进了一步。随着时间推移,单体应用逐渐消失,微服务则越来越多。
总结
将现有单体应用迁移到微服务架构是应用的现代化。实现这一结果并不需要从头重写代码,相反,只需要渐进式地将应用重构为一组微服务。其中有三种策略可以采纳:使用微服务实现新功能、将表示组件与业务和数据访问组件拆分、以及将现有应用内的模块转变为微服务。随着时间推移,大量微服务形成,团队的敏捷和效率也会提升。