前序

额,十分遗憾,这次并不是分享BUG了,所以不能让大家看到我出糗的样子了,而且,这次也没有太多技术性的内容,多少会显得有些枯燥乏味。不过呢,可能本次所涉及到的项目迁移拆分方案,在诸位看来也并非完美,所以各位还是有机会批评一波,娱乐一波。

背景

话不多说,我们先来谈谈这次这次项目迁移拆分的背景。

经典模型

我们先来看看目前大多数微服务框架的系统架构,这里以Dubbo为RPC服务基础,并且用传统的电商业务模型(熟知我的观众肯定知道,我向来是保护公司业务代码的,所以这里使用大家广泛熟知的业务模型为例)。

微服务架构服务拆分原则_人工智能

简单讲解下:

  1. 这是一件简单的电商业务模型,在微服务业务细分下,分为用户服务、商品服务、交易服务、仓储服务、售后物流服务、财务服务。(要是具体细分以及其他服务的就不在这里多说)
  2. 微服务架构采用Dubbo作为服务治理框架,各个服务通过Dubbo进行RPC通信。
  3. 各个服务当中涉及到的实体类、工具类、Dubbo接口等放在公共接口服务当中(我也不知道该怎么说,其实就是一个公共的jar包,不能说是一个服务,但现在姑且这么称呼吧),由各自业务服务web工程进行引用,其实就是在maven的pom文件当中引用。

以上可能是最简单的一个模型,当然在业务、架构上各个公司都有自己的方案以及扩展之类的,这就不再考虑之内,但是其实核心还是围绕着这个最原始的模型。

当前现状

那么现在遇到了什么问题?我们再来看看这样一个模型。各自的业务服务web工程都会对这个公共服务进行依赖,这本身也没什么问题,毕竟这里面放的都是一些Dubbo接口等,具体的实现类还是在各自的服务工程当中。但是问题来了,不知道从什么时候开始,这个模型发生了变化,就是所有的实现类都写在了这个公共接口服务里面。

由于这本身就是多模块的maven工程,于是各自的web工程引用自己所需要的那一部分module。于是所有的web工程只是一些不同环境profile下的配置文件。这个就与上面的模型的初衷背道而驰。如图所示:

微服务架构服务拆分原则_java_02

产生影响

那么这样会有什么问题:

  1. 项目结构的问题。所有人的代码都在这个公共接口服务当中,使得结构变得很混乱,对于web工程甚至会引用不属于自己的module,例如商品服务当中引用了财务的实现类,本质上走的是dubbo协议,web工程反而引入了一些多余的jar,甚至还会引起maven的依赖优先的问题,造成事故的风险点。(maven的依赖优先指的是引用不同module但是包含了同一个jar的不同版本的情况,maven采用依赖最短路径优先原则,这个有机会再之后的文章中细说)
  2. 代码管理的问题。由于不同模块的代码都在这个公共服务当中,在合并代码的时候会产生大量冲突。例如商品服务的同学提交代码进行Merge的时候会发生产生大量的冲突,这些冲突基本上是非自己业务模块,或交易、或仓储、或其他业务模块等。在理想模型下,也就是在我第一个图片当中,那么进行merge产生的冲突也应该是各自业务模块的代码冲突,这个是免不了。(导致合并代码最后演化成copy code的模式,或者使用Git提供的cherry-pick,但是本质上其实也是高级版的copy code,而无法使用正常的merge)
  3. 部署管理的问题。既然大家都把代码合并在这个公共服务当中,那么在CI部署的时候(不管是gitlab上还是Jenkins),编译打包这个过程就十分缓慢。原本这些编译打包的负担可以落在各自业务web服务中,现在反而都集中在这一个服务当中。随着日后业务代码的不断增多,这个时耗将会变得越来越长,甚至一旦有代码出错导致整个部署发布都失败。

这个是目前观察下来最为头疼的几个痛点。这个事情我从去年一直不断反馈,这个公共服务需要进行拆分。但是一是由于一些部门层面上的原因,二是业务发展吃紧,也没有人也没有这个时间去主导或者去推动跟进这个事情,导致这个事情一拖再拖,于是就陷入了一种恶性循环的地步。
直到这次双十一,因为大促期间无法发布,所以也就没有需求迭代。于是我再次提出这个事情,这次总算是有时间去推进这个事情。(我还是认为,既然已经发现诸多的痛点,还是应该想着提前去根治,否则这迟早是一把达摩克利斯之剑,要么在一开始就应该防止这种事情的发生)

微服务架构服务拆分原则_编程语言_03

实施前

那么现在想想,应该怎么去入手这个事情。(是不是觉得这个事情看上去好像没啥技术含量,但是突然就不知道怎么做,我当时还以为是大佬们去做这个事情,但是大佬们有其他优化,所以就变成谁提出,谁解决。当然,做程序员嘛,还是以解决问题为主)

分析目的

我们的目的是将业务具体的实现类(这里主要是service层的实现和controller层的接口)迁移至各自的web工程。那么,首先想到的是是否可以直接从公共服务当中删去那些代码,然后新增到各自的web工程呢,或者通过git操作进行合并。那么我觉得,如果只是从做法上考量,两种答案当然都是可以。但是呢,要是这么简单的话,我今天也就没有必要写这篇文章了。我们想想,要是真的就这么做了,会有哪些问题:

  1. 第一种,如果是删除代码,移动代码的话,这种做法我觉得是效果最差的。因为虽然你代码是实现了转移,但是一并的也失去了git上面的commit记录,其次还无法保留项目当中的各个分支。
  2. 第二种,似乎效果是好了一点,但是由于工程结构的不一样,在通过Git操作之后会有大量的冲突。网上也有很多的类似操作,但是由于网上提供的demo业务场景比较单一过于简单,就像这种git如何合并两个项目?,而且实际上的操作也比这种demo复杂且难实施,风险点过大不可控。

项目分支

可能很多人会奇怪,既然我是迁移项目,为什么还要保留分支。首先,我们要明确,如果是那种几个月前的开发分支,或者只是为了合并代码生成的合并分支,这种分支对于整个工程来说,意义不大。在迁移的时候重要的分支其实就是分为两类:

  1. 现阶段正在开发的开发分支,例如feature/abc;
  2. 具有重要里程碑意义的分值,例如灰度环境: gray/abc,或者是特殊分支: vip/abc,亦或者是预发分支: release/abc,还有就是这种备份分支: remaster/abc等等。

为此这类分支就要毫无保留的进行迁移,保留对代码工程的维护性。

当然我们公司还存在着另外一个特殊性,给这件事情造成极大的困难。由于我们公司是一家To B企业,所从事的业务也是面向企业的,所以对不同类型的公司就会进入到不同的环境,我们称之为“灰度环境”(虽然个人认为称为这里灰度与实际意义上的灰度意思有点不符,我所认为的灰度可能更多还是倾向于小流量用户所处环境,且用于新功能测试阶段的灰度过渡,这个可能就是To B与To C的不同吧)。于是在我们公司就会存在许多“灰度环境”,这个给推进这个项目迁移工作带来极大的阻力。
我之所以在这里介绍这么多篇幅,是因为这个确实是造成了很大问题:

  1. 首先,既然是项目迁移拆分,那么肯定不是一个晚上全部改掉完成,想想有这么多环境的存在;而且就算通宵改造,那么就不怕出现风险嘛!这个锅背不起。
  2. 其次,如果改造的话,那么公共接口服务当中具体的实现类肯定都是要删掉的(git上删除),剩下一个光秃秃的公共接口,那么各位开发同学在开发的时候,肯定会从这个光秃秃的公共接口服务的master切出分支。正常情况下是一个环境一个环境的进行更迭,那么当有紧急bug需要修复的时候,从这个光秃秃的服务中切出的hotfix分支合并到还未改造的环境的时候,就会把对应环境的你那个服务上的代码都删除了,但是那个环境由于还没有进行改造,而此时又发生删除情况,那么这显然就会出事。除非在某一时刻全部改造完成来避免这种情况,例如上面1中所说。如图所示:

微服务架构服务拆分原则_微服务架构服务拆分原则_04

实施方案

那么难道就真的无济于事了吗?后来我仔细考虑了下,既然一步到位的事情做不到,那么是不是就可以绕一下曲线救国。于是我寻思着,既然不能直接改造,那我是不是每一个业务服务可以重新镜像出一个公共接口服务,将依赖从原先的公共接口服务转移到这个公共接口服务镜像当中,由于各自业务都有自己的镜像,那么只需要对各自的镜像进行修改,这样改动与影响就小很多了。如图所示:

微服务架构服务拆分原则_bug_05

这样做有如下好处:

  1. 既然是复制镜像的做法,你那么整个Git工程当中的commit记录和所有的分支都保留。
  2. 每一个业务模块都可以对自己的镜像进行增删改,而不会影响到其他业务模块。
  3. 每个环境迭代发布,只需要将maven引用转移到公共接口服务镜像即可。

我们来看看接下去的环境迭代:

微服务架构服务拆分原则_bug_06

这样就可以避免上面的问题,对于未迁移的环境和已经迁移的环境,都可以很好的兼容。

最后的问题

但是这里还有一个问题,不知道各位观众有没有留意到,那么原先的公共接口服务,该怎么处理呢。这里也会面临是否删除代码的问题:

  1. 如果删除代码,但是这样又跟刚才说的第2点的问题是一样了;
  2. 如果不删除代码,那么原先的公共接口服务当中和镜像当中的实现类就会重复,这样两者进行编译的时候,后一个编译打包的就会覆盖前者,也会存在问题。

那么怎么办呢?这个还是主管给我提供了思路,由于原先公共接口服务的maven坐标和镜像的maven坐标是一样的,那么就需要将镜像的maven坐标进行改名,然后让web工程引用镜像的maven坐标,这样,即便同一个实现类在公共接口服务和镜像中都存在,但是web工程实际上加载的只会是镜像当中的class,这样就能解决这种问题。

实施中

既然上面已经将思路与方案都已经阐述清楚,那么接下来进行实施,这个过程就相对容易点了,就好像高楼大厦,架子已经搭建好了,只需要搬砖即可。
由于各个业务都是由各个业务小组进行推进,业务迁移拆分也有先后顺序,那么就以商品服务的拆分为例来展开:

  1. 通知各个开发,主要是商品服务开发人员,在统一时刻进行分支提交,因为需要将公共接口服务进行镜像复制。这是很关键一点,不然当复制出镜像之后,尚有分支还在原先公共接口服务当中,那么这个分支上的功能代码的迁移就会十分鸡肋。
  2. 在镜像当中只保留商品服务的实现类,包括controller、service实现类、business,dao等,其余非商品服务代码都进行删除,commit并且push。
  3. 修改maven坐标,进行改名,这里的改名最快的方法就是修改groupId,例如原先叫com.abc.abc,那么现在修改为com.abc.xyz,而artifactId不变,依旧保持语义,例如abc-item。
  4. version:set。这个就是在对应环境中进行部署打包,正式环境肯定是deploy。
  5. 在商品服务的web工程中,将涉及到依赖商品实现类的maven坐标进行修改,其实就是修改groupId就好,这样web工程就引用到镜像而不是原先旧的公共接口服务。
  6. 发布,检查启动是否报错。当然,我是在镜像当中加了一个心跳请求controller,来验证请求是否走到了镜像当中的代码。

那么对于其他开发同学的情况,我们这边也需要进行考虑一下:

  1. 非商品业务开发: 原则上,这些开发的改动是不能够触及商品业务的代码。但是由于之前都是在一个公共接口服务当中,这一点就很难保证。这种情况的话,要么予以驳回,要么就是配合copy code将这些代码复制到进行当中;
  2. 商品业务开发: 在切出镜像之前,有些同学已经有了业务开发的分支,那么这种情况下,只需要将镜像的master分支进行反向merge即可,因为master分支是只保留了商品的业务代码,其他代码都已经进行了delete,在反向merge之后,也一并会将那些代码进行删除;而在镜像已经切出来之后,那么如果需要开发新功能或者进行hotfix的话,那么只需从镜像checkout出新的分支即可。

实施后

在迁移拆分之后,需要进行简单验证。这里我以商品服务为例,在镜像当中的Controller层写一个心跳请求:

@RequestMapping(value = "/healthCheck", method = RequestMethod.GET)
@ResponseBody
public Object healthCheck() {
    return successResponse();
}

那么如果可以请求到这个接口,说明商品web服务工程已经成功引用了镜像里面的module,其余业务模块也都可以用这个方法监测。

小结

总的来说,可能本次项目迁移拆分的工程,难度更多的还是偏向管理方面的考量。虽然我不太清楚现在其他公司的项目架构是否有遇到这样的瓶颈,但就我身边朋友所处小公司的项目,也多少可以窥见接下来的趋势也会遇到相同的问题。其实也不难理解,在前期还是以发展业务为主(肯定是赚钱要紧,不然谁来发工资),只是我觉得,任何前期小型项目演化成一个巨无霸的时候,一定要意识到项目对于现在团队建设所影响到的方方面面,因为细节决定成败不只是书上说说的道理。
另一方面,在我们不能一蹴而就的情况下,需要进行一定程度上的曲线救国,通过引入中间环节来解决问题。正如上面所述的解决方案中,通过引入镜像以及更改maven坐标的方式,来间接实现迁移拆分的效果,并且在这种多环境的场景中也能很好的实现兼容,并且在业务中将代码实现了隔离,大大增强了系统的可维护性。