前言
19年的时候,次要负责微服务治理平台BOMS交付工作,期间客户提到在微服务模块拆分方面须要征询。
过后我还只是技术负责人,没有深刻理解这方面的需要,只是网上找了点材料发给客户,再联合资料简略讲一下“正交合成”等准则。
起初据说客户找到埃森哲做培训,每天的培训费用大略是好几千,前面领导打算让我当架构师,惋惜我回绝了,当初想想十分惋惜,但我并不是想做培训老师, 而是想能不能制作一款可视化的剖析工具,给微服务模块拆分提供可量化的拆分根据,以及评估拆分前后的性能指标变动。
Spring依赖剖析
我首先想到基于代码扫描的动态剖析,得益于 Spring 的架构劣势,做起来并不难,只须要把 Spring IoC
容器保留的 Bean
信息导出来即可,而正好 Spring Boot Actuator
模块刚好提供 /beans
接口用于导出Bean信息,Bean信息结构大略如下:
{ "contexts": { "school-service": { "beans": { "spring.jpa-org.springframework.boot.autoconfigure.orm.jpa.JpaProperties": { "aliases": [], "scope": "singleton", "type": "org.springframework.boot.autoconfigure.orm.jpa.JpaProperties", "resource": null, "dependencies": [] } } } }}
对于没有应用 Spring Boot
的老旧我的项目,以下就是参考 Actuator
源码实现的代替实现:
package com.springcloud.school;import com.fasterxml.jackson.annotation.JsonProperty;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.config.BeanDefinition;import org.springframework.beans.factory.config.ConfigurableBeanFactory;import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;import org.springframework.context.ApplicationContext;import org.springframework.context.ConfigurableApplicationContext;import org.springframework.stereotype.Component;import org.springframework.util.StringUtils;import java.util.HashMap;import java.util.Map;@Component()public class CustomBeansEndpoint {@AutowiredConfigurableApplicationContext context;public ApplicationBeans beans() { Map contexts = new HashMap<>(); for(ConfigurableApplicationContext context = this.context; context != null; context = getConfigurableParent(context)) { contexts.put(context.getId(), ContextBeans.describing(context)); } return new ApplicationBeans(contexts);}static ConfigurableApplicationContext getConfigurableParent(ConfigurableApplicationContext context) { ApplicationContext parent = context.getParent(); return parent instanceof ConfigurableApplicationContext ? (ConfigurableApplicationContext)parent : null;}public static class BeanDescriptor { @JsonProperty("aliases") String[] aliases; @JsonProperty("scope") String scope; @JsonProperty("type") Class type; @JsonProperty("resource") String resource; @JsonProperty("dependencies") String[] dependencies; private BeanDescriptor(String[] aliases, String scope, Class type, String resource, String[] dependencies) { this.aliases = aliases; this.scope = StringUtils.hasText(scope) ? scope : "singleton"; this.type = type; this.resource = resource; this.dependencies = dependencies; }}public static class ContextBeans { @JsonProperty("beans") Map beans; @JsonProperty("parentId") String parentId; ContextBeans(Map beans, String parentId) { this.beans = beans; this.parentId = parentId; } static ContextBeans describing(ConfigurableApplicationContext context) { if (context == null) { return null; } else { ConfigurableApplicationContext parent = getConfigurableParent(context); return new ContextBeans(describeBeans(context.getBeanFactory()), parent != null ? parent.getId() : null); } } static Map describeBeans(ConfigurableListableBeanFactory beanFactory) { Map beans = new HashMap<>(); String[] var2 = beanFactory.getBeanDefinitionNames(); int var3 = var2.length; for(int var4 = 0; var4 < var3; ++var4) { String beanName = var2[var4]; BeanDefinition definition = beanFactory.getBeanDefinition(beanName); if (isBeanEligible(beanName, definition, beanFactory)) { beans.put(beanName, describeBean(beanName, definition, beanFactory)); } } return beans; } static BeanDescriptor describeBean(String name, BeanDefinition definition, ConfigurableListableBeanFactory factory) { return new BeanDescriptor(factory.getAliases(name), definition.getScope(), factory.getType(name), definition.getResourceDescription(), factory.getDependenciesForBean(name)); } static boolean isBeanEligible(String beanName, BeanDefinition bd, ConfigurableBeanFactory bf) { return bd.getRole() != 2 && (!bd.isLazyInit() || bf.containsSingleton(beanName)); }}public static class ApplicationBeans { @JsonProperty("contexts") Map contexts; ApplicationBeans(Map contexts) { this.contexts = contexts; } public Map getContexts() { return this.contexts; }}}
新建 CustomBeansEndpoint.java
文件,而后把以上Java代码复制粘贴进文件中,最初新增 BeanController
package com.springcloud.school.controller;import com.springcloud.school.CustomBeansEndpoint;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/beans")public class BeanController { @Autowired() CustomBeansEndpoint beansEndpoint; @GetMapping("/") public Object beans() { return beansEndpoint.beans(); }}
把代码批改完再运行起来,在浏览器关上 https://localhost:8080/beans
就会返回JSON内容,而后复制JSON字符串保留到 beans.json
文件。
切记:为了稳当起见,倡议只在开发环境运行,不要提交或部署到生产/预生产环境,免得造成不可挽回的损失。
可视化剖析
有了JSON数据,就能够进行可视化剖析,首先去下载安装 Spring Module Analyzer 工具。
这是一款基于Qwik
+ Echarts
打造,专门用于Spring模块可视化剖析的工具。目前该工具应用 tauri
打包成Web App,反对 Windows
Mac
Linux
等平台(暂不反对win7、xp),能够依据理论需要下载安装后进行应用。
关上工具后,在首页点击两头的 开始
按钮,选中上一步导出的 beans.json
文件,而后就主动跳转到 依赖剖析
页面
看到满屏稀稀拉拉的文字先别慌,因为actuator导出的bean很多是spring框架自带的,能够在左上方 类名 右侧的输入框,输出你的 package
包名,而后点击最右侧的 过滤
按钮就能够把多余的bean过滤掉。比方输出 com.springclcoud
再点击 过滤
按钮:
当初能够看到不相干的bean曾经被过滤掉,还能够通过鼠标滚轮来缩放画面。
如果你的我的项目蕴含的bean数量十分多,能够追加子包名进行过滤,比如说 com.springcloud.school
,
随着包名的一直变长,原来连成一片的点集,会宰割成几个互相独立的小点集,这些小点集就相当于划分好的微服务模块。
以上基于代码的动态剖析,实际上就是基于包名进行微服务模块划分,有一个十分重要的前提就是先前的我的项目构造须要十分正当,而理论状况却是大部分我的项目构造十分凌乱,因而以包名作为微服务模块划分的根据并不合理。
然而有这么一个工具,能够十分直观理解现有的我的项目构造,评估微服务模块拆分之前的代码品质还是很有帮忙的,尤其是当你刚接手一个齐全生疏的我的项目,spring 依赖剖析是疾速理解我的项目构造的无力工具。
调用链分析
后面说到的动态剖析尽管十分直观,但显著不足量化指标。
比方说 A模块
同时依赖于 B模块
和 C模块
,A模块
每分钟会调用 B模块
上百次,但只会调用 C模块
一次,这种状况动态剖析是无奈进行无效的模块划分,因而咱们须要在运行时收集数据能力正确划分。
简略来说,就是通过 Spring AOP
给特定包下的所有办法织入一段代码逻辑,用于统计各模块之间互相调用的次数以及耗时,具体做法就是新建 Profile.java
文件,而后写入以下代码:
package com.springcloud.school;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.springframework.stereotype.Component;import java.util.Stack;@Aspect@Componentpublic class Profile { // ThreadLocal用于解决线程平安问题,每个线程的办法栈是独占的,因而能够防止抵触 ThreadLocal> methodStack = new ThreadLocal<>() { @Override protected Stack initialValue() { return new Stack<>(); } }; @Around("execution(* com.springcloud.school..*.*(..)) && !bean(profile)") public void count(ProceedingJoinPoint pj) { Stack stack = methodStack.get(); String peak = stack.empty() ? "root" : stack.peek(); String className = pj.getSignature().getDeclaringTypeName(); stack.push(className); long start = System.currentTimeMillis(); try { pj.proceed(); } catch (Throwable e) { throw new RuntimeException(e); } finally { long end = System.currentTimeMillis(); long gap = end - start; // 这里抉择间接把调用记录输入到日志,有条件的能够选发送到音讯队列进一步解决 System.out.println("trace: " + peak + " -> " + className + " : " + gap); stack.pop(); } }}
解释一下统计调用次数以及耗时的思路:
首先初始化一个栈,而后每当办法被调用之前,就把办法名称入栈,办法调用完结后就出栈,而后把后果输入到日志文件或者音讯队列,这里为了不便演示间接用 System.out.println。
这里记得把 @Around("execution(* com.springcloud.school..*.*(..)) && !bean(profile)")
的 com.springcloud.school
换成你我的项目所用的包名,至于最左边的 !bean(profile)
就是把 Profile
类给排除,因为我把 Profile.java 也保留到 com.springcloud.school
下。
接着把批改后的代码放到环境上运行,而后跑一下全量接口测试,测试尽可能笼罩所有的业务模块,依据理论状况调整收集时长,等积攒足够多的调用记录,就能够进行下一步:解决收集后果。
切记:为了平安起见,倡议只在开发环境运行,不要提交或部署到生产/预生产环境,免得料
这里只说输入到日志的解决办法,首先依据前缀 trace:
过滤失去相干的调用记录,而后把每行的箭头 ->
和 冒号 :
替换成逗号 ,
,而后保留到 trace.csv
文件,最终的文件构造如下:
com.springcloud.school.controller.BeanController,com.springcloud.school.CustomBeansEndpoint,10root,com.springcloud.school.controller.BeanController,39com.springcloud.school.service.BaseServiceImpl,org.springframework.data.repository.PagingAndSortingRepository,323
第一列是调用方,第二列是被调用方,第三列是耗时,三者通过逗号宰割,只有合乎这个规定的csv文件就能够被用于可视化剖析。
因而微服务模块划分的粒度最小也是类,很少到办法级别,所以调用方和被调用方其实是办法所属的类,而不是办法名。
可视化剖析
咱们从新关上之前的 Spring Module Analyzer 工具,而后点击左边 链路剖析
按钮,再点击下方 开始
按钮,抉择之前保留 trace.csv
文件,而后点击 确定
按钮
进入 链路剖析
页面,左上角 终点
和 起点
两个输入框,能够依据包名进行过滤,和之前的依赖剖析大同小异
和后面不同的是多了 求值
下拉框,以及 范畴
输入框。
“求值” 有五个选项:计数
、平均值
、求和
、最小值
和 最大值
。以下是各选项实用的场景:
计数
:就是累计两个模块之间的调用次数,能够配合调高范畴的下界,来过滤掉某些呈现次数很少的调用记录,从而拆散出更多的点集,主动实现模块划分;也能够调低范畴的下限,过滤某些因为循环或递归调用频繁呈现的记录;平均值
:就是用总的耗时除以总的调用次数,得出每次调用办法的均匀耗时,可用于评估微服务模块划分前的理论提早,个别均匀耗时低于100毫秒的模块不宜拆分,因为微服务模块之间RESTful接口调用基本上都大于100毫秒,把本来均匀耗时大于100毫秒的模块拆分到不同微服务并不会显著减少零碎提早;求和
:两个模块之间互相调用所须要的总耗时最小值
:两个模块之间互相调用耗时的最小值最大值
:两个模块之间互相调用耗时的最大值
总结
基于Spring依赖的可视化剖析,能够疾速大略把握我的项目构造的根本状况,同时也能直观地感触代码架构是否合乎 低耦合高内聚
的要求,对后续的代码重构优化也有肯定的指导作用;在对我的项目构造有根本的理解后,就能够联合调用链分析,在微服务模块拆分之前,对系统整体的性能指标有确切的数据,也能通过和图表的交互,大抵预估微服务模块拆分后的我的项目构造以及零碎性能的变动。
我做这个工具的初衷,是因为职业生涯中,常常遇到空降的大厂架构师之类的人物,在没有充沛理解我的项目现况的状况下,强行推广从别的企业生吞活剥的软件架构,后果往往导致我的项目停顿碰壁的状况;而相熟我的项目的低级开发人员却没有任何发言权,凡是提出质疑总会被大厂光环的title压抑。
这个可视化剖析工具能够提供可量化的指标,能够突破领导对大厂教训的科学,刹住行业的歪风邪气,让行业风尚从新回到纯正的感性探讨技术气氛。