一、单体已死,单体永存
在微服务设计之初,我们对业务进行功能划分,用一个个微服务支撑起相应的业务,仿佛焦泥坑中的上古巨兽铮开了桎梏身形的枷锁,呼喊着:"单体已死,微服务起飞",一切似乎都在朝着预期的方向进行。
而随着业务快速迭代,单个微服务项目的体量也在悄然膨胀,渐渐地新功能迭代速度放缓,潜在问题频发,就像千年前的埃及人,扛着巨大的木头工具,推着数吨重的石块,不断搬运累积到未完工的金字塔上,口中呼喊着:"单体永存"。
二、拆掉金字塔
现在"大单体微服务"已成为阻碍完工的主要因素,要做的就是将其拆成多个小的,那我们要如何做呢?
历史的经验告诉我们人多力量大,既然对方很强大,那就先找1000个身强力壮的人来,夜以继日地搬运石块,直到完成目标。
而辩证法又告诉我们,人多真的力量大吗?
阿基米德甚至说给他一个支点就可以撬动整个地球,那我们是否也存在一个支点也可以撬动整个庞然大物呢?
答案显然是有的!
计算机发展的经验告诉我们,重复且简单的工作最终都会被计算机取代,被自动化取代。我们的目标就是实现一套自动化工具,能够自动理解和拆分这个"金字塔"。
三、语义理解就是支点
人工拆分单体项目屡见不鲜,应该说是绝大多数都会选择这样做,因为人工可以去理解拆分规则,执行拆分动作,修复拆分产生的引用问题,同时最重要的是人工可以做到方法级拆分。下面我们就从上述三个点来看看,如何基于语义来达成这个目标。
1. 拆分目标规则
我们以最简单的拆分为例,即将一个单体服务拆分成两个微服务,一个后台服务和一个运行服务。两个服务有不同的API接口,RPC接口。具体如下:
后台服务 | 前端服务 | |
---|---|---|
API接口 | /api/bs/order/ | /api/order/ |
RPC接口 | UserManageService | UserDataService |
2. 方法级拆分
根据拆分规则,我们首先可以找到一些入口类文件,然后再根据每个方法调用的堆栈去判定其他方法如何拆分。一个方法应该属于哪里会存在3种情况:
(1)后台服务:该方法被后台入口类文件直接或间接引用,且没有被运行服务方法直接或间接引用。
(2)运行服务:该方法被运行入口类文件直接或间接引用,且没有被后台服务方法直接或间接引用。
(3)公共包:该方法同时被后台接口和运行接口直接或间接引用。
如此来看,我们只要根据每个入口方法,逐层逐一地对方法进行确认,以我们的目标服务来计算,有2400多个类文件,超过40000个方法要识别。
3. 引用修复
当我们执行拆分后,类名会增加前缀进行重命名以此避免编译冲突。如此一来以前引用是UserService,现在部分方法拆分到了公共服务变成了CommonUserService,编译就无法通过,需要取将之前对foo()方法的注入对象由UserService改成CommonUserService。
综合来看,步骤2和步骤3属于重复性高,规则固定的场景,非常适合用自动化实现。
四、如何做项目的语义理解
4.1 项目结构
以springcloud为例,一个常规的maven多模块项目,由以下几个部分组成:
- 启动类
- properties配置文件
- dubbo接口配置文件
- pom.xml依赖文件
- 子模块
-
- java文件
- mybatis的xml文件
- pom.xml子模块依赖
其中子模块数量不固定,可能存在多个。
4.2 解析项目
我们使用一些符号来标识项目解析产生的结果。
- 对于拆分,我们可以理解为分组,总共有3个分组:bs(后台分组)、runtime(运行分组)、common(公共分组)。
- 每个分组内是方法的集合,我们使用m1(后台方法集合)、m2(运行方法集合)和m3(公共方法集合)来标识。
- 每个分组内的文件集合:f1(后台文件集合)、f2(运行文件集合)和f3(公共方法集合)。
- 全局方法调用栈集合:callStack,本质上是哈希表结构,第一层是入口文件的方法签名,下级是相应的调用堆栈方法签名,层级不固定。
- 全局接口和实现类关系:使用哈希表结构,将接口签名和实现类签名对应起来。
- 全局子类与继承类关系集合:使用哈希表结构。
- 全局Bean类集合:Bean是个很特殊的类,他基本不需要拆分,所以需要单独保存。
- 类与方法关系集合:每个类中含有的方法集合,Key是类全路径,Value是方法签名集合。这里需要注意,我们没有区分方法重载,多个重载方法会被认定为同一个签名。
- 入口方法集合:使用哈希表来记录,每个分组值作为Key,方法集合作为Value。
- 全局静态类集合
- 方法使用短签名:方法在代码中的文本形式,比如user.get(id),那么user.get就是使用处的短签名。后续自动引用修复时会用到。
暂时写到这,后面实际拆分放到下一篇中。