今天想和大家聊聊Java中的APM,简单介绍Java中的Instrumentation技术,然后重点分析bistoury的实现原理
即Java探针技术,通过Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在JVM上的程序,甚至能够替换和修改某些类的定义而对业务代码没有侵入,主要场景如APM,常见的框架有:SkyWalking、Pinpoint、Zipkin、CAT,arthas和bistoury其实也算吧。
推荐一篇博客:Instrumentation
从JDK1.5开始支持
Agent逻辑在main方法之后执行,两个关键方法:
// 优先级高
public static void premain(String agentOps, Instrumentation instrumentation);
public static void premain(String agentOps);通常agent的包里面MATE-INF目录下的MANIFEST.MF中会有这样一段声明
Premain-Class: Agent全类名在启动应用的时候,添加Agent参数触,Agent逻辑在main方法之后执行
java -javaagent:agentJar.jar="Hello World" -jar agent-demo.jar从JDK1.6开始支持
Agent逻辑在main方法之后执行,两个关键方法:
// 优先级高
public static void agentmain(String agentArgs, Instrumentation inst);
public static void agentmain(String agentArgs); 可以动态触发,通过VirtualMachine这个类attach到对应的JVM,然后执行VirtualMachine#loadAgent方法
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(agentJar路径);在程序运行的过程中,可以通过 Instrumentation API 动态添加自己实现的 ClassFileTransformer
Instrumentation#addTransformer(ClassFileTransformer)An agent provides an implementation of this interface in order to transform class files. The transformation occurs before the class is defined by the JVM
代理程序(即自己的Agent)提供实现类,用于修改class文件,该操作发生在 JVM 加载 class 之前。它只有一个transform方法,实现该方法可以修改 class字节码,并返回修改后的 class字节码,有两点要注意:
byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException;例如
// 定义一个 ClassFileTransformer
public abstract class Transformer implements ClassFileTransformer {
private static final Logger logger = BistouryLoggger.getLogger();
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
if(className.equals(xxxx)){
通过ASM修改字节码,并返回修改后的字节码
}
return null;
} catch (Throwable e) {
logger.error("", "transform failed", "Classs: {}, ClassLoader: {} transform failed.", className, loader, e);
}
}
}
// 添加一个Agent JDK1.5
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new Transformer());
}
// 触发Agent JDK1.5
java -javaagent:/agent.jar="传递的参数" -jar test.jar
// 添加一个Agent JDK1.6
public static void agentmain (String agentArgs, Instrumentation inst) {
inst.addTransformer(new Transformer());
}
// 触发Agent JDK1.6
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent("agent.jar");有关于ASM的简单介绍,推荐两篇博客:ASM访问者模式、ASM使用
ClassReader#accept方法,传入一个ClassVisitor对象,在ClassReader中遍历树结构的不同节点时会调用不同ClassVisitor对象中的visit方法,从而实现对字节码的修改ClassWriter是ClassVisitor的实现类,它是生成字节码的工具类, 将字节 输出为 byte[],在责任链的末端,调用ClassWriter#visitor 进行修改后的字节码输出工作JMX(Java Management Extensions)是一个为应用程序植入管理功能的框架。JMX是一套标准的代理和服务,实际上,用户可以在任何Java应用程序中使用这些代理和服务实现管理
说的有点抽象,推荐一篇博客 JMX
我自己的理解,JMX分为Server和Client, MBean是它的核心概念
MBean的容器,负责管理所有的MBean,同时我认为它就是一个Agent程序,在Java应用启动的时候自己启动。让我不太明白的是,为什么通过jps命令不能看到这个进程呢?Server建立连接,常见的客户端有:jvisualvm、jconsole、自己小工具MBean做一些事情,动态改改属性值啥的,也就是说,JMX只认识MBean,不认识别的。基于内置的一些MBean,可以获取内存、线程、系统等指标信息。所以如果想做一些监控上的事情,可以基于它内置的MBean 去哪儿网开源的一个对应用透明无侵入的Java应用诊断工具,可以让开发人员无需登录机器或修改系统,就可以从日志、内存、线程、类信息、调试、机器和系统属性等各个方面对应用进行诊断,提升开发人员诊断问题的效率和能力。内部集成了arthas,所以它是arthas的超集。其中两个比较有特色的功能:在线DEBUG、动态监控,就是基于 Instrumentation + ASM 做的。
在开始分析这个框架之前,可以先看看它的整体架构 Bistoury设计文档
即 bistoury-instrument-agent模块,这就是Agent,里面有一个核心类 AgentBootstrap2,该类同时持有 premain 和 agentmain 方法,并在pom.xml文件中配置了 Premain-Class 和 Agent-Class
public static void premain(String args, Instrumentation inst) {
main(args, inst);
}
public static void agentmain(String args, Instrumentation inst) {
main(args, inst);
}
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>qunar.tc.bistoury.instrument.agent.AgentBootstrap2</Premain-Class>
<Agent-Class>qunar.tc.bistoury.instrument.agent.AgentBootstrap2</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>从上可以看出,不管是 premain 和 agentmain 方法,里面都调用了 main 方法,而 main 方法主要负责以下功能
java.arthas.Spy类,将AdviceWeaver中的各个方法引用赋值给Spyqunar.tc.bistoury.instrument.spy.BistourySpys1类,将GlobalDebugContext,SnapshotCapture,AgentMonitor中的各个方法引用赋值给BistourySpys1,这些方法最总通过ASM方式进行调用BistouryBootstrap#bind方法,启动一个telnetServer端,所以我们可以通过telnet向其发送命令和在线DEBUG相关,涉及到两个方法, isHit 和 hasBreakpointSet,这两个方法最终通过字节码的形式进行调用
保存实 属性、静态属性、局部变量、方法调用堆栈 等信息,最后返回将这些信息返回给前端 , 涉及的方法列表如下:
AgentMonitor和动态监控相关,动态监控可以监控方法的调用次数、异常次数和执行时间,同时也保留最近几天的监控数据。而动态监控的实现原理也很简单,就是在方法执行前后记录调用次数和响应时间,而这部分逻辑就是通过ASM动态插入字节码来实现的
上面已经说过,在main方法中会调用BistouryBootstrap2#bind 方法,该方法用于启动一个ShellServer,这里指的是TelnetServer。
这个类参考了ArthasBootstrap, ArthasBootstrap#bind方法中,主要启动了两个ShellServer,即: TelnetServer和HttpServer, 所以我们在使用arthas的时候可以通过web和telnet方式访问。
BistouryBootstrap与ArthasBootstrap有些不同
BistouryBootstrap只创建了TelnetServer, 并没有创建HttpServer;BistouryBootstrap在arthas的基础上实现了一个自己的CommandResolver, 即 QBuiltinCommandPack, 该类负责管理所有的Command,也就是说,从功能上来讲, bistoury是arthas的超集;核心bind方法如下,源码感兴趣的自己看一下
public void bind(Configure configure) throws Throwable {
long start = System.currentTimeMillis();
if (!isBindRef.compareAndSet(false, true)) {
throw new IllegalStateException("already bind");
}
try {
/**
* 涉及到各个 Client 的初始化, 将参数 instrumentation 传到各个 client 中
*
* JarDebugClient
* AppConfigClient
* QDebugClient
* QMonitorClient
* JarInfoClient
*/
InstrumentClientStore.init(instrumentation);
ShellServerOptions options = new ShellServerOptions()
.setInstrumentation(instrumentation)
.setPid(pid)
.setWelcomeMessage(BistouryConstants.BISTOURY_VERSION_LINE_PREFIX + BistouryConstants.CURRENT_VERSION);
shellServer = new ShellServerImpl(options, this);
QBuiltinCommandPack builtinCommands = new QBuiltinCommandPack();
List<CommandResolver> resolvers = new ArrayList<CommandResolver>();
resolvers.add(builtinCommands);
shellServer.registerTermServer(new TelnetTermServer(
configure.getIp(), configure.getTelnetPort(), options.getConnectionTimeout()));
for (CommandResolver resolver : resolvers) {
shellServer.registerCommandResolver(resolver);
}
shellServer.listen(new BindHandler(isBindRef));
} catch (Throwable e) {
if (shellServer != null) {
shellServer.close();
}
InstrumentClientStore.destroy();
isBindRef.compareAndSet(true, false);
throw e;
}
}上面提到的只是前置处理会触发的逻辑,即Java Instrumentation 会触发的逻辑,而Agent模块的main方法,其实在是qunar.tc.bistoury.indpendent.agent.Main中,执行这个方法会触发以下逻辑
bistoury.proxy.host获取Proxy地址Proxy发送一个Http请求,请求地址为proxyIp:9090/proxy/config/ Proxy返回与Agent建立连接的Ip和端口AgentClient#initNettyClient方法与Agent建立TCP连接SPI加载所有的AgentGlobalTaskFactory实现类,然后调用他们的start方法失败重试的定时任务,每分钟执行一次public static void main(String[] args) throws Exception {
log();
AgentClient instance = AgentClient.getInstance();
instance.start();
System.in.read();
}至此,Agent的启动完成。
Proxy的启动逻辑在qunar.tc.bistoury.proxy.container.Bootstrap#main方法中,默认Tomcat端口9090
-Dbistoury.conf=/Workspace/ZTO/forensic/bistoury-proxy/conf NettyServerManagerBean初始化的之前,执行一些初始化操作NettyServerManager#startAgentServer方法启动针对Agent的Server端, 处理来自Agent的请求,默认端口为9880 NettyServerManager#startUiServer方法启动针对UI的Server端, 处理来自UI的Websocket连接,默认端口为9881 UI的启动逻辑在qunar.tc.bistoury.ui.container.Bootstrap#main方法中,默认Tomcat端口9091
-Dbistoury.conf=/Workspace/ZTO/forensic/bistoury-ui/conf UI -> Prosy -> Agent -> Proxy -> UI以在界面点击查看主机信息为例
ConfigController#getProxyWebSocketUrl接口,入参=agentIpProxy agentIp为入参,请求proxyIP:9090/proxy/agent/get,此步骤用于判断agentIp对应的那个Agent是否可用Proxy返回Agent信息UI后后端接口返回前端一个Websocket地址,浏览器和Proxy通过Websocket连接 ws://10.10.134.174:9881/ws UI通过Websocket连接向Proxy发送命令Proxy将命令转发请求到Agent Agent收到命令进行逻辑处理,将结果回给Proxy Proxy将结果返回给UI Proxy接收请求经过 解码 -> 主机有效性校验,最终请求来到UiRequestHandler#channelRead方法,UiRequestHandler构造函数包含4个关键入参
DefaultCommunicateCommandStore, 构造函数会注入所有的UiRequestCommand DefaultUiConnectionStore, 维护Channel和UiConnection之间的关系,UiConnection#write方法返回的ListenableFuture可以添加回调DefaultAgentConnectionStore, 维护agentIp和AgentConnection之间的关系,AgentConnection#write方法返回的ListenableFuture可以添加回调DefaultSessionManager, 维护请求Id和Session的关系,Session中持有RequestData AgentConnection UiConnection 属性,这是实现请求转发的关键有关于Session,下次再重点介绍,它是是实现请求转发的关键
请求流程
code(code可以看作是命令的唯一标识)找到对应的CommunicateCommand, 然后获取CommunicateCommand的CommunicateCommandProcessor属性CommunicateCommandProcessor#preprocessor方法AgentServerInfo找到对应的AgentConnection, 执行sendMessage方法,即执行Session#writeToAgent方法,该方法用于向Agent发送命令UiConnection#write方法,用于向UI返回结果浏览器与Proxy建立Websocket连接的时候,基于Channel创建一个UiConnection,然后基于UiConnection和AgentConnection创建一个DefaultSession,AgentConnection从哪里来?
AgentConnection是 Agent与Proxy维持心跳时创建的,核心类AgentMessageHandler、ProxyHeartbeatProcessor,创建之后缓存到DefaultAgentConnectionStore, key就是agentIp
Proxy建立了Websocket连接,浏览器向Proxy发送一个指令qmonitoradd Proxy与Agent通过Netty建立了TCP连接,Proxy将命令转发给Agent Agent收到消息,解析指令,通过TelnetClient与ShellServer建立telnet连接ShellServer收到指令,找到对应的Command, 这里指QMonitorAddCommand QMonitorAddCommand#process方法,然后执行QMonitorClient#addMonitor方法,最后执行DefaultMonitor#doAddMonitor方法DefaultMonitor#instrument方法,这里面涉及到Java的Instrumentation技术和ASM技术MonitorClassFileTransformer对象,它实现了ClassFileTransformer接口,织入代理逻辑,就是通过这个对象完成的MonitorClassVisitor完成。涉及到的知识点:ClassReader、ClassWriter、ClassVisitor AgentMonitor相关方法的调用,而AgentMonitor的相关方法会将 调用次数、响应时间、异常数 存入Metrics中qunar.tc.bistoury.agent.task.monitor.TaskRunner启动时,调用顺序如下:QMonitorClient#reportMonitor -> QMonitorMetricsReportor#report -> 获取Metric 原理和动态监控一样,也是通过 Instrumentation + ASM 实现
qdebugadd QDebugAddCommand QDebugClient#registerBreakpoint -> DefaultDebugger#doRegisterBreakpoint -> DefaultDebugger#instrument DebuggerClassFileTransformer、DebuggerClassVisitor、DebuggerMethodVisitor 本地局部变量、实例属性、静态变量、方法调用堆栈信息 保存到SnapshotCapture中DebuggerMethodVisitor#processForBreakpoint方法,将所有相关的信息存到DefaultSnapshotStore的缓存中添加断点按钮之后,即发送qdebugadd指令之后,前端会开启一个定时任务,每3s向服务端发送一个qdebugsearch指令,直到服务端返回数据。服务端收到指令,从DefaultSnapshotStore中获取数据返回前端其它功能下次补充