2.2.2 绞杀者模式

绞杀者模式(Stranger Pattern)是一种系统重构方式,它的名称来源于马丁福勒在澳大利亚雨林看到的一种名为绞杀无花果(stranger fig)的植物。这种植物会缠绕在宿主树上吸收养料,然后慢慢地向下生长,直到在土壤中生根,最终杀死宿主树使其成为一个空壳,如图2-8所示。

图2-8

在软件开发行业,这成了一种重写系统的方式,即围绕旧系统逐步创建一个新系统,让它慢慢成长,直到旧系统被完全替代。绞杀者模式的优势就在于,它是一个渐进的过程,容许新旧系统共存,给予新系统成长的时间。它的另一个优点是能够降低风险。一旦新系统无法工作,你可以迅速将流量切换回旧系统。这听上去和蓝绿部署很相似。我们团队基于绞杀者模式逐步将单体应用改造成了微服务,平滑地迁移到了新技术栈。

1.绞杀者模式的开发过程

和绞杀无花果一样,绞杀者模式的开发过程也有三个阶段。

● 转换:创建一个新系统。

● 共存:逐渐从旧系统中剥离功能并由新系统实现,使用反向代理或其他路由技术将现有的请求重定向到新系统中。

● 删除:当流量被转移到新系统后,逐步删除旧系统的功能模块,或者停止维护。

我们对旧系统的改造工作涉及前端和后端两部分,改造后端部分是指将原来的业务逻辑逐一拆解成微服务;改造前端部分是将原来的Rails应用重构为基于React的UI界面并调用微服务提供的新接口。

后端部分的改造步骤和策略具体如下。

(1)识别业务边界

业务边界在领域驱动设计(DDD)中称为界限上下文,它根据业务特性将应用分成相对独立的业务模型,比如订单业务、库存业务等。识别出业务边界是合理设计微服务的基础,可以实现高内聚低耦合。不合理的边界划分会导致服务在开发过程中因为依赖而受到制约。

典型的识别业务边界的方法是使用领域驱动设计,但需要由领域专家和熟悉DDD的开发者一起完成。还有一种简单的方法是通过已有的UI界面进行识别,因为通常情况下不同的业务都会有自己的界面。不过这种方法只能比较粗略地拆分出业务模块,细节部分还需要进一步分析。很可能某个业务模块的页面中也会存在与其他业务交互的部分,比如我们的订单模块中有一个功能是“执行预测”,它显然属于预测服务而不是订单服务。还有一种方式就是基于业务模型并结合旧系统的代码进行梳理,从而识别出业务边界。这种方式比较适合业务清晰且团队在之前已经有很明确的职能分工的组织,我们团队就是使用这种方式完成微服务边界识别和服务拆分的。

(2)基于API进行改造

一个包含前后端的Web应用通常都是围绕着API接口进行开发的,因此,我们的改造过程也是围绕API进行的。首先对前端页面调用的后端API做一个统计,根据重要性、紧急程度、影响范围等设置优先级,然后在新系统中挨个实现原来的接口。要注意的一点是,如果新接口要被旧页面调用,那么就要和原接口保持一致;如果新接口只是内部接口或只被新前端页面调用,则可以基于新技术栈的特性进行一定的调整,只需要注意暴露给客户的页面使用方式和数据格式兼容即可。

(3)选择代价最小的部分进行重构

识别出业务边界也就意味着服务拆分基本完成,可以进行新系统的开发工作。除了微服务架构这种基础设施,业务逻辑的迁移应该依据最小代价原则进行,也就是说先选择不太重要的业务进行改造,比如客户使用率比较低的业务、查询相关的业务、逻辑简单的业务等。使用这种策略可以降低风险,即便出现问题也不会对客户造成很大的影响。图2-9展示了从旧系统中拆分出功能并在新系统中实现的过程。

图2-9

(4)将迁移出来的功能实现为服务,减少对旧系统的修改

理想情况下,除了将已有的业务迁移到新系统,新来的需求也要尽可能地在新系统中实现,这样才能尽快让服务价值被体现,并且阻止单体应用继续变大。

不过现实情况往往不那么完美。第一个棘手的问题是时效性问题,即客户提了一个新需求,并要求在某个时间点以前尽快上线。开发者需要仔细评估实现它的时间成本,如果在新系统中实现要花费更多的时间,那么就不得不在旧系统中实现它,然后再选择合适的时机迁移到新系统。这种重复的无奈之举是开发者最不愿意看到的,但也不得不为最后期限做妥协。另外一种情况是,新功能和旧系统耦合较重,或者暂时不适合单独实现为新的服务。比如,我们需要在原来的接口中添加一两个字段,或者是组合旧代码实现一段新逻辑等,很明显在旧系统中实现这些功能的成本要低得多,因此遇到这样的场景选择修改旧系统相对更合理。但不管怎样,在实施绞杀者模式的时候,开发者还是要明确这一原则:尽可能减少对单体应用的修改。

图2-10展示了将已有的单体应用逐渐迁移成新的微服务应用的流程,从图中可以看到,因为上面提到的原因,旧系统并不总是变小的,有可能会变大。在一定时期内,新系统和旧系统是共存的,直到新系统完全实现了旧系统的功能,我们就可以不再维护旧系统并对其做下线处理了。

图2-10

前端部分的改造策略和步骤具体如下。

(1)分析UI界面

一般来讲,一个Web应用是围绕API进行开发的,UI界面的数据是从后端微服务获取的。因此,我们可以通过分析API的从属关系来找到前端和后端的对应关系。

在应用中,UI界面一级导航栏的内容一般都是基于大的业务模块确定的,通常会包括几个隶属于不同微服务的子业务模块。比如广告投放模块中包括广告主、广告活动、广告创意等二级模块,分别属于不同的微服务。这些业务比较清晰的页面调用的API一般也都来自同一个服务。

但是如果出现从其他服务获取数据的情况就需要注意一下,比如有一个页面中的图表是从不同的两个API中获取数据并组合在一起的,而这两个API属于不同的服务。在遇到这种情况时,笔者建议先做设计自省,分析是不是在服务的拆分上有不合理的情况。如果没有设计问题,那么页面由大业务所属的团队负责,另外一个团队负责提供API即可。

(2)旧前端调用新服务

对客户来讲,微服务迁移通常是一个透明的过程,提供给最终用户的URL一般不会变更。另一方面,新服务对应的前端页面还没有开发完成,这就需要我们用旧前端去调用新服务。因此在确保新服务已经能替代某些旧系统的业务之后,我们一般都会修改前端调用后端的API,将数据获取从旧系统转移到新服务。

(3)新前端调用新服务

这种情况相对比较自由,因为前后端都是重新开发的,可以对原有接口的输入和输出做优化和调整,只要保证最终展示给客户的结果和原来的一致即可。我们在迁移过程中单独为新前端定义了一个URL前缀,URL的其他部分和原地址保持一致。两个前端同时存在,可以通过输入对应的URL进行对比测试,分析新页面渲染的数据和旧页面是否一致。我们还专门开发了一个对比工具,可以将页面中不一致的部分高亮显示,方便调试。

(4)OpenAPI替换

对外暴露的API原则上是不能随便改接口签名的,我们的解决方法是发布新版本的OpenAPI。比如之前客户使用的是V3版本的API,现在发布了基于微服务的V4版本API,两个版本并存,并告知客户尽快迁移到新版本,旧版本在某个时间点将停止维护。尽管新版本的API可以使用全新的签名,比如URL、入参,但除非有必要,否则API的返回结果最好和旧版本兼容,因为客户的应用程序一般都要基于返回数据做进一步的处理,如果返回结果变了,客户将不得不修改自己的代码逻辑,这会增加他们迁移到新API的成本。

2.绞杀者模式的使用策略

除了上面提到的改造策略和步骤,在使用过程中还需要关注以下问题。

● 不要一次性用新系统替换旧系统,那不是绞杀者模式,要通过控制替换的范围来降低风险。

● 考虑存储和数据一致性问题,确保新旧系统都可以同时访问数据资源。

● 新构建的系统要容易被迁移和替换。

● 迁移完成后,绞杀者应用要么消失,要么演变为遗留系统的适配器。

● 确保绞杀者应用没有单点和性能问题。

3.绞杀者模式的适用性

绞杀者模式也不是灵丹妙药,并不能适用于所有的系统迁移场景,使用它有一些条件和限制,具体如下。

● 基于Web或API的应用:实施绞杀者模式的一个前提是必须有一种方式可以在新旧系统之间切换。Web应用或基于API构建的应用程序可以通过URL结构来选择系统的哪些部分以何种方式实现。相反,富客户端应用或移动端应用并不适合使用这种方式,因为它们不一定具有分离应用程序的能力。

● 标准化的URL:一般情况下Web应用都使用一些通用的模式来实现,比如MVC、表现层和业务层分离。但有些情况下使用绞杀者模式就不太容易。比如请求层下面有一个中间层,做了API聚合等操作,这就导致切换路由的决定不能在最上层实现,而要在应用的更深层实现,此时使用绞杀者模式的难度就比较大。

● 复杂性和规模较小的小型系统:绞杀者模式不适合用于改造规模较小的系统,因为批量替换的复杂性比较低,还不如直接开发全新的系统。

● 到后端的请求无法被拦截的系统:请求无法拦截意味着没有办法分离前后端,也就没办法将部分请求指向新系统,绞杀者模式无从谈起。