当前位置: 首页 > 工具软件 > greys-anatomy > 使用案例 >

Greys主要实现原理

邬弘化
2023-12-01

转载自:https://www.iflym.com/index.php/code/201801170001.html

greys是一个使用java management tool进程注入javaagent实现在线系统的诊断一个工具。原github为(https://github.com/oldmanpushcart/greys-anatomy),其主要的功能在于系统不停机的情况下。可以查看系统中的线程信息,cpu使用情况,jmx信息,以及某个方法在运行时的调用栈,调用参数等。

一个典型的场景就是线上某个功能出bug,但是系统中并没有记录参数信息,这时候即可通过这个功能注入agent,临时地打印出这个调用方法的参数,以方便定位相应的问题。如无此工具,则只能改代码,然后重新上线。在这种情况下,可能出错的场景就不能再复现。(当然也有其它工具(如log monitor)作到在线系统参数记录开关的目的,这里不作描述)

本文主要描述greys是如何工作的,包括如何注入到在线系统,然后脚本client与注入后server端的交互,以及如何实现一个简单的参数拦截记录功能。从整个实现机制层面描述其工作原理。

 

1. 将agent注入到在线系统中

自java 6之后,jvm提供了一系列的tools工具,用于与jvm进行交互。其中在attach中,即可以通过相应的api拿到本地上正在运行的jvm实例,进而获取一系列的数据信息。一个简单的操作如下所示:

01

02

03

04

05

06

07

08

09

10

11

12

public static VirtualMachine findByPid(String pid) throws Exception {

    List<VirtualMachineDescriptor> virtualMachineList = VirtualMachine.list();

    for(VirtualMachineDescriptor descriptor : virtualMachineList) {

        //这里的id即与jps中的pid相同,即进程id

        String id = descriptor.id();

        if(Objects.equals(id, pid)) {

            return VirtualMachine.attach(descriptor);

        }

    }

 

    throw new RuntimeException("不能找到虚拟机:" + pid);

}

如上的代码,即可通过pid找到一个目标虚拟机,然后再通过 VirtualMachine中的相关方法可以拿到相关的信息,但更主要的即是可以通过此类注入一个新的agent到目标jvm中,然后此agent即可以通过instrument对象进行一系列的操作了。相应的代码如下参考所示:

1

2

3

4

5

6

7

8

9

public static void injectAgent(VirtualMachine virtualMachine, String agentPath, String otherConfig) throws Exception {

    //agent path即实际要注入的agent jar的实际地址,如/data/x/agent.jar

    //otherConfig在相应的agent main中,jvm会将此参数传递此目标方法参数中

    virtualMachine.loadAgent(agentPath, otherConfig);

}

 

public static void premain(String agentArgs, Instrumentation inst) {

     

}

在上面的代码中,通过将agent.jar注入到目标系统,并且通过otherConfig参数进一步提供其它参数,这样可以进一步的进行一系列操作。
在greys中,实际注入过程如下。

  1. greys整体分为2个jar, 1个为greys-agent.jar,另一个为greys-core.jar,认为greys-agent.jar是注入引导类,core为主要逻辑实现。(后续简称agent.jar, core.jar)
  2. 通过loadAgent将agent.jar注入在目标系统,并且传递了core.jar的目标地址,以及其它参数
  3. 传递给agent.jar的参数分为2部分,一部分即为core.jar路径信息,另一部分即为后续agent server启动时的配置参数
  4. agent解析参数,拿到core.jar的地址信息,并由此构建一个全新的classLoader,通过classLoader可以拿到此jar中的所有类信息
  5. 由classLoader构建出全局单例对象 gaServer,并将instrument对象绑定其中,后续的类扫描,重定义等均通过instrument来引导处理
  6. gaServer开始解析由agent.jar中传递过来的配置信息,并且启动监听server,监听指定端口,准备接收相应的请求

至此注入过程完成。相应的agent.jar和core.jar都已经在线上系统中存在。其中 core.jar由单独classLoader持有,线上系统感知不到.

2. 指令client与agent server的交互

通过注入过程,相当于在线上系统开了一个后门,并且后门中可以看到重要的信息,比如通过mxbean拿到线上系统信息,同时instrument功能可以重定义对象。
整个过程由类GaServer完成,以下详细描述过程:

  1. 通过GaServer#bind 读取配置信息,初始化ServerSocketChannel对象,并绑定ip和端口
  2. 通过一个简单的nio代码(GaServer#activeSelectorDaemon),开启一个新线程,读取客户端请求连接和输入数据
  3. 客户端通过连接此ip和端口(GreysConsole类实现),并发送简单的指令来完成交互过程,一个连接过程称之为一个Session,其中封装相应的流信息以及控制逻辑
  4. 通过定义简单的通信协议,一行为一个指令,输入回车即完成此指令,并等待server返回,然后将此信息打印在控制台
  5. 服务端读取到指令之后(GaServer#doRead),简单解析(DefaultCommandHandler#executeCommand 和 Commands#newCommand), 并构建出command对象
  6. 通过command对象以及相应的action,根据相应的类型执行不同的action动作,并将响应信息以及执行结果(封装为Affect),写回session动作中,并由简单的循环读取响应信息,最终输入回客户端中

上述的动作,4-6是可以多次处理的,即客户端在执行完一个指令之后,可以再输入下一条指令,继续执行其它指令。
同时,如上所述,所有的指令均是通过Command完成,并通过全局扫描自动添加至Commands中,相应的交互实际即在线上系统中执行相应的command指令。

3. agent server在线系统监控

command对象主要的执行过程主要还是通过读取mxBean和类增强两大类来完成。其中前者不需要改变线上系统内部行为,后者需要改变线上行为,并对类的执行过程产生影响。

3.1 JvmCommand指令
此指令非常简单,即打印出线上机器当前的情况,其Action为silentAction,即不会改变线上结果,仅简单返回数据信息。主要信息通过ManagementFactory拿到各个mxBean,从中获取到如类加载信息,gc信息,内存信息,操作系统信息等,然后进行数据组合,最后封装为TTable对象,展现为一个控制台表格形式的数据。直接返回即可。

3.2 TraceCommand指令

此指令用于输出单个方法在调用时,此方法的代码所访问的每一个方法的调用信息,主要包括耗时信息。其输出如下参考所示:

1

2

3

4

5

`---+Tracing for : thread_name="http-nio-8080-exec-9" thread_id=0xa9;is_daemon=true;priority=5;

    `---+[1,1ms]xxx.YController:zzz()

        +---[1,0ms]org.springframework.web.servlet.ModelAndView:<init>(@45)

        +---[1,0ms]org.springframework.web.servlet.ModelAndView:addObject(@47)

        `---[1,0ms]org.springframework.web.servlet.ModelAndView:setViewName(@48)

其实现机制即通过重定义要进行监控的类,然后在执行过程中打印相应的调用过程。整个过程详细如下:

  1. 通过client指令(如 trace abc.* methodA)中解析出类信息,这里的类信息为一个类正则表达式,可以匹配多个
  2. 通过之前保存的instrument对象拿到线上所有已加载对象(instrument#getAllLoadedClasses), 与类匹配信息进行匹配,提出所要增强的类
  3. 同时,通过增强类列表,再通过方法正则匹配表达式,找到需要监控的方法列表,并同形成 Map<Class<?>, Matcher<AsmMethod>> 对象,即这些类的这些方法需要处理
  4. 通过这些数据以及增强类command所提供action,组合形成一个类增强器Enhancer(使用asm进行代码增强)。 此增强器实现了ClassFileTransformer接口,即可通过instrument#addTransformer添加到线上系统中
  5. 调用instrument#retransformClasses 重新转换这些指定的类,通过之前的匹配,可以避免增强不必要的类,这样对线上系统的影响最小
  6. 在相应的enhance实现中,通过反向勾子,最终利用asm,在整个方法指令集中,通过监控 执行方法前 调用三方方法前, 调用三方方法调用后, 调用三方方法异常后, 执行方法后 这些相应的点,并反向调用相应command提供的监听器。 这里为TraceCommand提供的AdviceListener对象
  7. 在监听器中,即记录起相应的三方方法信息,尽可能启动更多详细的信息,并通过一个简单的树状信息组件 TTree 将这些信息收集在一起(具体可参考相应代码实现)
  8. 最终方法执行后,将此执行信息写回session,即完成一次trace过程

TraceCommand指令主要使用了instrument中的几个关键方法,如getAllLoadedClasses返回所有已加载类,监控的类即从这些类中查找。 retransformClasses重转换类。这些方法都是由jvm自身提供,不需要通过classLoader来中间跳转,并且完成不影响jvm原信息逻辑。 

同时,转换类,即类增强使用了asm,通过在字节码层面处理尽可能多的一些信息,增强这些信息,以拿到线上信息。当然也可以使用javassist(据说greys早期即使用javassist)

值得注意的是,因为每一次增强相当于对线上系统类进行了一次处理,为避免可能对后续执行产生影响,在完成一次类增强之后,greys通过removeTransformer将此次转换器移除掉了,这样此次指令的增强将在下一次指令的增强时失效, 这样可以避免多次相同作用增强。避免出现奇怪的问题.

最后

至此,整个greys的实现分析完毕。整个实现使用到了一些关键的技术,列举如下:

  1. jvm tools attach,用于连接线上jvm信息
  2. classLoader,用于隔离类信息
  3. javaagent & instrument, 用于提供类转换过程, 修改加载类字节码
  4. asm & bytecode, 用于字节码处理,以方便对类进行增强及处理

其中classLoader,instrument和字节码在日常的关键开发中,均起到重要的作用。这些技术有助于技术能力的提升。

 类似资料: