前言
微服务已经在越来越多的企业开花。企业在享受微服务优势的同时,会产生一些问题。如随着企业的业务发展,相依的服务数量不断增加,服务调用关系越来越错综复杂。
以下对话是软件开发人员经常遇到的。
场景一:我的接口都谁在调用呢1
2
3
4
5小张:小王,user服务的获取getById()接口被哪些服务使用呢?
小王:原来业务不多的时候,我还知道。现在业务太复杂了,调用方太多,我已经记不清了。
小张:那么办,我接了一个需求,涉及原接口逻辑的变动,这个接口的调用方都要评估下影响面
小王:那你只能挨个负责服务的同事问下了
小张:太麻烦了,时间都用在寻找调用方上了
RD每天的宝贵时间都浪费在了维护服务和接口的调用关系上,为此失去了专研更有价值的业务的精力
场景二:我的服务都调了谁的服务的哪些接口1
2
3
4
5小姚:小修,我接手了一个product服务,为了尽量熟悉,我需要知道这个服务都依赖哪些服务,调了哪些接口?
小修:这个···,服务有个wiki文档,你看看呢
小姚:我看了,最新的更新时间是一年前
小修:恐怕你只能翻代码了
小姚:一行一行看代码!,我嚎哭
RD每天的宝贵时间都浪费在了维护服务和接口的调用关系上,为此失去了学习新技术的时间
本项目产生的意义就是为了收集和展示服务的调用关系,特别是服务中接口的调用关系。带来的价值为很好的避免了以往只能通过开发人员头脑记忆,要知道记忆是会减退的。所以利于准备评估后续需求开发涉及的影响面。
从而维护项目上线的稳定,增强服务可用性。所以,项目name:微服务梳子
。意在自动化理清微服务体系下的服务间关系调用
业务背景
微服务架构的流行,在业务分隔,服务复用,敏捷开发等方面带来了很大的飞跃。随着业务场景越来越丰富,业务领域越来越广泛,服务数量越来越庞大。随之,服务间调用越来越错综复杂。
微服务带来的问题
所以,当服务数量越来越庞大时,服务间调用越来越错综复杂。举一个很实际的例子:一个服务从诞生开始,随着业务的不断发展,服务接口数量和调用方都在不断增加,而开发人员又在不断变化。慢慢的,当前服务接口的被使用方有哪些就无人能说清了。
具体说,假设服务A有一个接口a(其实a被3个上游其他的服务调用),但是当前维护a接口的人员可能是新来的,或是时间太过久远而不一定能准确的知道调用a接口的上游服务。这就带来了诸多不确定性风险
本项目能带来什么
为了准确的知道服务及其接口的调用方信息,本项目为此而出现。通过本项目,你可以准确的、实时的获取服务及其接口的被调用(使用)情况,从而帮助你准备的评估迭代需求的工作量,及时统计出更改所影响的上游服务方,拔高说增强了服务的可维护性和稳定性,提高了可用性。
核心目标
本项目意在提供简单快速的使用教程,真正的协助微服务架构模式下的服务维护成本,减少RD们对服务接口调动错乱的记忆,当然也告别前人留下的大坑。
架构图
组件依赖
本项目基于spring cloud openfeign(目前版本:2.1.0.RELEASE)实现
实现原理
本项目分为三部分,一是调用信息收集及发送,二是接收并存储调用信息,三是图表展示微服务调用关系。调用信息的传递(发送和接收)采用的是CQRS模型
调用信息收集及发送
这部分功能由microservice-comb-infrastructure
模块负责。具体原理为调用信息收集采用的是动态代理,通过代理LoadBalancerFeignClient
类,使得有机会在服务调用时收集调用信息;并通过kafka
发送调用信息给接收方。收集的逻辑通过自定义的InvocationHandler:LoadBalancerFeignClientInvocationHandler
完成,发送调用信息通过MessageSender
类完成。接收并存储调用信息
这部分功能由microservice-comb-server
模块负责。使用kafka接收调用信息,随之将信息持久化到硬盘,当前是保存到mysql数据库,后期会采用动态切换,支持mongodb等方式。图表展示微服务调用关系
这部分功能由microservice-comb-admin
模块负责。使用js等图表组件展示服务调用关系图,通过检索服务名/接口名可以知道:A服务的a接口的调用方信息,什么时间调的,一段时间内调用的次数等。但由于本人还不熟悉前端技术,暂时搁置这个模块。
项目结构
module | description |
---|---|
microservice-comb-admin | 调用信息图表展示,核心组件 |
microservice-comb-base | 基础工具包,核心组件 |
microservice-comb-infrastructure | 调用信息收集及发送,超核心组件 |
microservice-comb-server | 接收并保存调用信息,核心组件 |
microservice-comb-example | 使用例子,模拟业务方服务 |
特别说明:1
2
3
4microservice-comb-example包含三个module。
microservice-comb-server-a 模拟业务方服务,此模块引用microservice-comb-server-b-sdk
microservice-comb-server-b 模拟业务方服务,此模块引用microservice-comb-server-b-sdk
microservice-comb-server-b-sdk 模拟业务方服务sdk,此模块引用microservice-comb-infrastructure
使用实例
使用方式
使用很简单
总体分两步:
- 资源准备
- 引用sdk
资源准备
kafka。调用信息发送和接收采用的kafka,所以你需要准备好运行着的
kafka server
,并把kafka
的配置信息换成你的`kafka server信息mysql。你需要有
mysql数据库
。database: server_info
,并建表1
2
3
4
5
6
7
8
9
10
11
12
13CREATE TABLE `server_invocation` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id,自增',
`from_application` varchar(100) NOT NULL DEFAULT '' COMMENT '服务调用方',
`to_application` varchar(100) NOT NULL DEFAULT '' COMMENT '服务被调用方',
`from_path` varchar(100) NOT NULL DEFAULT '' COMMENT '服务调用方接口路径path',
`to_path` varchar(100) NOT NULL DEFAULT '' COMMENT '服务被调用方接口路径path',
`method` varchar(32) NOT NULL DEFAULT '' COMMENT '请求方式: GET POST',
`ctime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`utime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`creator_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '操作人id',
`create_name` varchar(32) NOT NULL DEFAULT '' COMMENT '操作人姓名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务调用信息表';eureka
本项目是基于spring cloud体系,所以eureka是需要的
引用sdk
import源码后,通过mvn将microservice-comb-infrastructure打jar包, 然后引入到应用project中。将原来引用spring-cloud-starter-openfeign
的模块或服务换成引用microservice-comb-infrastructure
,如下1
2
3
4
5<dependency>
<groupId>com.skyler.cobweb</groupId>
<artifactId>microservice-comb-infrastructure</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
模拟实战
为方便使用和接入。microservice-comb-example模块提供了模拟实际公司中的微服务架构模式,microservice-comb-example包含三个子模块,整体关系为1
2
3microservice-comb-server-a依赖microservice-comb-server-b-sdk,因为microservice-comb-server-a需要调用sdk的feign接口
microservice-comb-server-b依赖microservice-comb-server-b-sdk,因为microservice-comb-server-b的controller实现sdk的feign接口
microservice-comb-server-b-sdk依赖microservice-comb-infrastructure
- 运行服务
依次运行如下服务1
2
3microservice-comb-server-a
microservice-comb-server-b
microservice-comb-server
- 访问microservice-comb-server-a接口
skyler@192 ~ curl -X GET 'localhost:9090/combo/getById?id=10'
- 查看数据库数据
如图所示
模拟企业实际应用
microservice-comb-admin的功能负责展示数据库的服务间调用关系图。由于本人没有学习前端技术。效果暂时通过查询sql展示
查看微服务下多个服务间调用关系
- 查询microservice-comb-server-b调用了哪些服务及接口
1
SELECT * FROM server_invocation WHERE from_application = 'microservice-comb-server-b'
结果如下图,
Note: 支持展示精确到调用方法上,附加条件查询就好
- 查询哪些服务及接口调用了microservice-comb-server-b
1
SELECT * FROM server_invocation WHERE to_application = 'microservice-comb-server-b'
结果如下图
项目地址: microservice-comb
微服务架构下服务依赖分析
1 | // 前台business业务服务 |
服务依赖分析的重要性
采用微服务架构的企业中,为了业务模块间解耦,更好的迭代发展业务。整个公司业务分成10个模块服务都是很正常的。而且随着业务的不断发展,服务数量和每个服务中接口的数量都在不断增加,且开发人员又在不断更迭。试想,10模块服务间,比较相互依赖,相互调用,链路如一张网。尤其细化到接口级别,更加繁琐。如果靠开发人员用脑子记,恐怕不现实了。如何准确的知晓一个服务被哪些服务调用,一个服务用调用了哪些服务,对服务的稳定,代码的维护,影响的评估,业务的迭代等等都产生着巨大的帮助。
作为owner或leader,掌握自己的服务及服务上下游的使用情况绝对的必须的。首先,谁依赖我的服务呢,具体到依赖哪些接口。其次,我依赖谁的服务呢,依赖了他们的哪些接口呢。这个做到门清,需求来时,才能
因此,依赖分析是研发过程中非常重要的一环。
java agent原理
示例
创建agent class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public class AgentDemo {
/*
* 该方法在main方法之前运行,与main方法运行在同一个JVM中
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("------ premain方法 有两个入参 ------ agentArgs:" + agentArgs + " inst:" + inst.toString());
// Instrumentation打印加载的类名
for (Class loadedClass : inst.getAllLoadedClasses()) {
System.out.println("loadedClass.getName():" + loadedClass.getName());
}
// ClassFileTransformer打印加载的类名
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("transform method className:" + className);
return new byte[0];
}
});
}
/**
* 如果不存在 {@link AgentDemo#premain(String, Instrumentation)}, 则会执行本方法
*/
public static void premain(String agentArgs) {
System.out.println("------ premain方法,有一个入参 ------ agentArgs:" + agentArgs);
}
}pom中添加配置,用于生成META-INF/manifest.mf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<!-- 值为包含premain方法的类. 启动方式为命令行启动时,javaagent JAR文件清单必须包含 Premain-Class 属性, 代理类必须实现 public static premain()-->
<Premain-Class>com.ke.utopia.combo.agent.javassist.CombAgent</Premain-Class>
<!-- 值为包含agentmain方法的类. 启动方式为JVM启动后启动时 文件清单必须包含 Agent-Class 属性, 代理类必须实现 public static agentmain() -->
<!--<Agent-Class>com.ke.utopia.combo.agent.AgentDemo</Agent-Class>-->
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id><!-- this is used for inheritance merges -->
<phase>package</phase><!-- 指定在打包节点执行jar包合并操作 -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
运行使用,idea或者命令行启动时加入如下指令1
VM options: -javaagent:/Users/yaoliang/skyler/project/lianjia/microservice-comb/microservice-comb-javaagent/target/microservice-comb-javaagent-1.0.0-SNAPSHOT.jar=hello
java agent原理
JVMTI
1 | Java平台调试器架构(Java Platform Debugger Architecture,JPDA)是一组用于调试Java代码的API(摘自维基百科): |
Instrumentation原理
获取Instrumentation接口实例的方法
- JVM在指定代理的方式下启动,此时Instrumentation实例会传递到代理类的premain方法。
- JVM提供一种在启动之后的某个时刻启动代理的机制,此时Instrumentation实例会传递到代理类代码的agentmain方法。
简单来说就是premain
方法和agentmain
方法1
premain对应的就是VM启动时的Instrument Agent加载,即agent on load,agentmain对应的是VM运行时的Instrument Agent加载,即agent on attach。两种加载形式所加载的Instrument Agent都关注同一个JVMTI事件 – ClassFileLoadHook事件,这个事件是在读取字节码文件之后回调时用,也就是说premain和agentmain方式的回调时机都是类文件字节码读取之后(或者说是类加载之后),之后对字节码进行重定义或重转换,不过修改的字节码也需要满足一些要求,在最后的局限性有说明
premain
1 | 1.编写premain函数,包含下面两个方法的其中之一: |
agentmain
1 | 1.编写agentmain函数,包含下面两个方法的其中之一: |
Java Instrumentation结构
Instrumentation是Java提供的一个来自JVM的接口,该接口提供了一系列查看和操作Java类定义的方法,例如修改类的字节码、向classLoader的classpath下加入jar文件等。使得开发者可以通过Java语言来操作和监控JVM内部的一些状态,进而实现Java程序的监控分析,甚至实现一些特殊功能(如AOP、热部署)。
Instrumentation的一些主要方法如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public interface Instrumentation {
/**
* 注册一个Transformer,从此之后的类加载都会被Transformer拦截。
* Transformer可以直接对类的字节码byte[]进行修改
*/
void addTransformer(ClassFileTransformer transformer);
/**
* 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
* retransformation可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
*/
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
/**
* 获取一个对象的大小
*/
long getObjectSize(Object objectToSize);
/**
* 将一个jar加入到bootstrap classloader的 classpath里
*/
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
/**
* 获取当前被JVM加载的所有类对象
*/
Class[] getAllLoadedClasses();
}
其中最常用的方法就是addTransformer(ClassFileTransformer transformer)了,这个方法可以在类加载时做拦截,对输入的类的字节码进行修改,其参数是一个ClassFileTransformer接口,定义如下:1
2
3
4
5public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException;
JVM加载一个class,回调一次transform方法
javaagent开源组件选型
解析 Java 字节码的工具,最常用的包括 Javassit,ASM,CGLib。ASM 是一个轻量级的类库,性能较好,但需要直接操作 JVM 指令。CGLib 是对 ASM 的封装,提供了更高级的接口。相比而言,Javassist 要简单的多,它基于 Java 的 API ,无需操作 JVM 指令,但其性能要差一些(因为 Javassit 增加了一层抽象)。在工程原型阶段,为了快速验证结果,我们优先选择了Javassist。arthas使用了javassist;Byte Buddy操作的是字节码,觉得有些复杂。所以最后使用Javassist实现
javassist学习
示例
1 | public class AccountTest { |
javassist原理
1 | Javassist(Java Programming Assistant)使Java字节码操作变得简单。它是一个用于在Java中编辑字节码的类库;它使Java程序能够在运行时定义新类,并在JVM加载时修改类文件。 |
javaagent 开源应用组件
1 | https://github.com/alibaba/arthas |
参考
https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html
https://paper.seebug.org/1099/#instrumentation_1
https://blog.csdn.net/wangzhongshun/article/details/100287986
https://ivanyu.me/blog/2017/11/04/java-agents-javassist-and-byte-buddy/
https://yq.aliyun.com/articles/135955
https://juejin.im/post/5da2fd6a6fb9a04e23576dd4
https://github.com/jboss-javassist/javassist/wiki/Tutorial-1
https://github.com/legend91325/Getting-Started-with-Javassist/blob/master/GetStartWithJavassist.md
https://zh.twgreatdaily.com/HxGYYWwBUcHTFCnf2NSG.html
https://baiqiantao.github.io/Java/aop/UVFzmi/#MethodCall
https://www.jianshu.com/p/b9b3ff0e1bf8