前言

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压抑。

这个可视化剖析工具能够提供可量化的指标,能够突破领导对大厂教训的科学,刹住行业的歪风邪气,让行业风尚从新回到纯正的感性探讨技术气氛。