前言
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 {
@Autowired
ConfigurableApplicationContext 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
@Component
public 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,10
root,com.springcloud.school.controller.BeanController,39
com.springcloud.school.service.BaseServiceImpl,org.springframework.data.repository.PagingAndSortingRepository,323
第一列是调用方,第二列是被调用方,第三列是耗时,三者通过逗号宰割,只有合乎这个规定的csv文件就能够被用于可视化剖析。
因而微服务模块划分的粒度最小也是类,很少到办法级别,所以调用方和被调用方其实是办法所属的类,而不是办法名。
可视化剖析
咱们从新关上之前的 Spring Module Analyzer 工具,而后点击左边 链路剖析
按钮,再点击下方 开始
按钮,抉择之前保留 trace.csv
文件,而后点击 确定
按钮
进入 链路剖析
页面,左上角 终点
和 起点
两个输入框,能够依据包名进行过滤,和之前的依赖剖析大同小异
和后面不同的是多了 求值
下拉框,以及 范畴
输入框。
“求值” 有五个选项:计数
、平均值
、求和
、最小值
和 最大值
。以下是各选项实用的场景:
计数
:就是累计两个模块之间的调用次数,能够配合调高范畴的下界,来过滤掉某些呈现次数很少的调用记录,从而拆散出更多的点集,主动实现模块划分;也能够调低范畴的下限,过滤某些因为循环或递归调用频繁呈现的记录;平均值
:就是用总的耗时除以总的调用次数,得出每次调用办法的均匀耗时,可用于评估微服务模块划分前的理论提早,个别均匀耗时低于100毫秒的模块不宜拆分,因为微服务模块之间RESTful接口调用基本上都大于100毫秒,把本来均匀耗时大于100毫秒的模块拆分到不同微服务并不会显著减少零碎提早;求和
:两个模块之间互相调用所须要的总耗时最小值
:两个模块之间互相调用耗时的最小值最大值
:两个模块之间互相调用耗时的最大值
总结
基于Spring依赖的可视化剖析,能够疾速大略把握我的项目构造的根本状况,同时也能直观地感触代码架构是否合乎 低耦合高内聚
的要求,对后续的代码重构优化也有肯定的指导作用;在对我的项目构造有根本的理解后,就能够联合调用链分析,在微服务模块拆分之前,对系统整体的性能指标有确切的数据,也能通过和图表的交互,大抵预估微服务模块拆分后的我的项目构造以及零碎性能的变动。
我做这个工具的初衷,是因为职业生涯中,常常遇到空降的大厂架构师之类的人物,在没有充沛理解我的项目现况的状况下,强行推广从别的企业生吞活剥的软件架构,后果往往导致我的项目停顿碰壁的状况;而相熟我的项目的低级开发人员却没有任何发言权,凡是提出质疑总会被大厂光环的title压抑。
这个可视化剖析工具能够提供可量化的指标,能够突破领导对大厂教训的科学,刹住行业的歪风邪气,让行业风尚从新回到纯正的感性探讨技术气氛。
发表回复