如何为线上正在运行的服务的某个类加条日志?

前言

请您思考这样一个问题:如何为线上服务的某个类加条日志?

您可能说,这还不简单,在代码里加条日志,Git一提交,发布一下不就搞定了!

但是如果这个服务特别重要,你没办法随意重启,你该怎么办呢?

本篇,我们就来聊一聊这个“头疼的问题”。

如何为正在飞驰的汽车换轮子

你有没有遇见过这样的场景,一个接口的逻辑非常之复杂,涉及到大量的接口调用与内部多层次逻辑嵌套处理,好似这样:

用户登录接口流程
突然某一天,产品来找你,说她登录不好用了!你得排查一下吧,看了半天代码,你怀疑是她手机问题(逃),开个玩笑,你怀疑是serviceB的返回值可能不对,但是代码里还没有打日志,线上服务正在运行,现在是流量高峰期,你也没办法重启,产品还一个劲的催你解决,你想这可咋办呀?
我也很绝望呀

别慌,我们来一起想想,有什么办法可以解决这个问题。

要把大象装冰箱,一共分几步

不知你有没有看过赵本山与宋丹丹老师春晚经典的小品,其中经典的台词就是:要把大象装冰箱,一共分几步?

分析这个问题,也是一样的道理,我们来拆解一下问题,我们的核心诉求是希望知道serviceB的返回值是什么,那么最好的方式就是加点日志看看,但是还不能重启服务,那么问题就变成了怎么在不重启服务的前提下,给代码中加点日志呢?
核心问题

OK,我们再来把问题向下分解,Java类运行,都是需要编译为class文件,在JVM虚拟机上去执行的,那么我们希望在Java类中加点日志输出,本质上是需要生成一个新的class文件,来替换掉JVM上正在运行的class文件,这样我们的诉求就可以达成了,那么又带来两个问题,

1、JVM是如何加载class文件的。

2、我们如何替换JVM上正在运行的class文件。

我们一个一个来说。

JVM加载class文件过程

那么JVM是如何去加载class文件的呢?我们来看一下Oracle官方的说法:

The Java Virtual Machine dynamically loads, links and initializes classes and interfaces. 
Loading is the process of finding the binary representation of a class 
or interface type with a particular name 
and creating a class or interface from that binary representation. 
Linking is the process of taking a class or interface 
and combining it into the run-time state of the Java Virtual Machine 
so that it can be executed. 
Initialization of a class or interface consists of 
executing the class or interface initialization method <clinit>

JVM把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是JVM的类加载机制。

类加载时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个部分统称为连接。
类的生命周期

那什么情况下需要开始类加载过程的第一个阶段,加载呢?

Java虚拟机规范中没有进行强制约束,这一点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

  1. 遇到newgetstaticputstatic或者invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK1.7的动态语言支持的时候,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStaticREF_putStaticREF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

对于这5种触发类进行初始化的场景,虚拟机规范使用了一个很强的限定语:“有且只有”,这5种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。

类加载器

上面我们了解了JVM中判断一个class何时应该被加载,那么这个加载的活是谁来干呢?答案是类加载器。

JVM设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到JVM外部来实现,以便让应用程序自己决定如何获取所需的类。实现这个动作的代码模块称为类加载器。

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段,对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

这句话可以表达的更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器的前提下才有意义,否则,即使这两个类来源自同一个class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

如何替换JVM上正在运行的class文件

好了,第一个问题我们解释完毕,那么再来看第二个问题,如何替换JVM上正在运行的class文件?

设计JDK的大师们早就预料到了这个情况,所以早在Java5中,就加入了这个能力,来解决这个问题。

java.lang.instrument.Instrumentation

那么这个包是干嘛用的呢?我们来看一下Oracle官方的解释:

This class provides services needed to instrument Java programming language code. 
Instrumentation is the addition of byte-codes to methods for 
the purpose of gathering data to be utilized by tools. 
Since the changes are purely additive, these tools do not modify application state or behavior. 
Examples of such benign tools include monitoring agents, profilers, coverage analyzers, and event loggers.

简单的说,使用Instrumentation,可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。

有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的AOP实现方式,使得开发者无需对JDK做任何升级和改动,就可以实现某些AOP的功能了。

看到这里,是不是有点小激动呢?通过JDK提供的这个能力,我们就可以在不重启JVM的前提下,动态的修改class的内容了,那么具体该怎么做呢?

翻阅API文档,我们发现这么两个接口:redefineClassesretransformClasses。一个是重新定义class,一个是修改class。这两个大同小异,看redefineClasses的说明:

This method is used to replace the definition of a class 
without reference to the existing class file bytes, 
as one might do when recompiling from source for fix-and-continue debugging. 
Where the existing class file bytes are to be transformed 
(for example in bytecode instrumentation) retransformClasses 
should be used.

都是替换已经存在的class文件,redefineClasses是自己提供字节码文件替换掉已存在的class文件,retransformClasses是在已存在的字节码文件上修改后再替换之。

当然,运行时直接替换类很不安全。比如新的class文件引用了一个不存在的类,或者把某个类的一个field给删除了等等,这些情况都会引发异常。所以如文档中所言,instrument存在诸多的限制:

The redefinition may change method bodies, the constant pool 
and attributes. The redefinition must not add, 
remove or rename fields or methods, change the signatures of methods, or change inheritance. 
These restrictions maybe be lifted in future versions. 
The class file bytes are not checked, verified and installed 
until after the transformations have been applied, 
if the resultant bytes are in error this method 
will throw an exception.

什么意思呢?重定义可能会更改方法体、常量池和属性。重定义不得添加、移除、重命名字段或方法;不得更改方法签名、继承关系。在以后的版本中,可能会取消这些限制。在应用转换之前,类文件字节不会被检查、验证和安装。如果结果字节错误,此方法将抛出异常。

可以理解成Instrumentation提供的能力,类比你租来的房子,只允许在客厅里放盆花,加个凳子之类的,但是你想砸承重墙,两居改三居,那是万万不要想滴!

如何替换服务器上正在运行的class文件

好了,最开始的两个问题,我们现在都已经有了答案,通过JDK提供的Instrumentation,我们就可以达成我们的诉求,现在我们理论方针是有了,那么最关键的一步,我们如何去替换服务器上正在运行的class文件呢?

直接操作字节码修改class

通过JDK提供的Instrumentation,我们可以通过一些手段直接修改class文件,在类中加一段打印日志的代码,然后调用retransformClasses就可以了。

比较有名的操作字节码的框架有cglib、ASM,我们知道Spring就是通过cglib来直接操作字节码,生成代理对象的。

但是这里又有一个问题:我们并非先知,不可能知道未来有没有可能遇到这种问题。我们也不可能在每个工程中都开发一段专门做这些修改字节码、重新加载字节码的代码。

同时ASM、cglib的使用并不友好,编写代码较为复杂,让我们直接去开发这种代码,难度显然是有点颇高,那么有没有一个简单一些,对开发人员较为友好的工具,也可以达成一样的效果呢?

幸运的是,答案是肯定的。

BTrace

BTrace是一个开源项目,源码托管于GitHub,在GitHub上也有很高的热度,目前的Star数是3.5K+,地址https://github.com/btraceio/btrace

那么BTrace是什么?

A safe, dynamic tracing tool for the Java platform.

BTrace can be used to dynamically trace a running Java program
 (similar to DTrace for OpenSolaris applications and OS). 
 BTrace dynamically instruments the classes of the target 
 application to inject tracing code ("bytecode tracing").

BTrace是基于Java语言的一个安全的、可提供动态追踪服务的工具。BTrace基于ASM、Java Attach Api、Instruments开发,为用户提供了很多注解。依靠这些注解,我们可以编写BTrace脚本(简单的Java代码)达到我们想要的效果。

BTrace真像它说的这么简洁高效么?我们来看一个官方的示例(https://github.com/btraceio/btrace/blob/master/samples/ArgArray.java):

package com.sun.btrace.samples;

import com.sun.btrace.annotations.*;
import com.sun.btrace.AnyType;
import static com.sun.btrace.BTraceUtils.*;

/**
 * This sample demonstrates regular expression
 * probe matching and getting input arguments
 * as an array - so that any overload variant
 * can be traced in "one place". This example
 * traces any "readXX" method on any class in
 * java.io package. Probed class, method and arg
 * array is printed in the action.
 */
@BTrace public class ArgArray {
    @OnMethod(
        clazz="/java\\.io\\..*/",
        method="/read.*/"
    )
    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args) {
        println(pcn);
        println(pmn);
        printArray(args);
    }
}

这个示例演示了去拦截所有java.io包中所有类中以read开头的方法,并打印类名、方法名和参数名。

似乎没有骗我们,真的非常简洁易用!

BTrace的功能非常强大,可以支持通过进程PID,来动态的执行操作,比如这样:

btrace java进程PID ./scripts/你实现的Instrumentation去替换class的操作.java

关于BTrace的功能,这里我仅做一个抛砖引玉,具体的能力,您可以查找相关资料,进行深入了解。

总结

好了,到现在,我们在回头看本文开头的问题,如何在不重启线上服务的前提下,在Java类中加行日志输出呢?

要把大象装冰箱,一共分几步?一样,分三步:

1、把冰箱门打开(编写Instruments的实现,替换掉目标class文件)

2、把大象装进去(在服务器执行BTrace命令,执行替换操作)

3、把冰箱门关上(观察日志,验证class替换是否生效)

关于本文中提到的技术实现,为您推荐几篇博文:

IBM社区的《Instrumentation 新功能》:

https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html

JDK8官方文档:https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html

BTrace github:https://github.com/btraceio/btrace

本篇的内容就到这里,感谢您的阅读。

本文参考:

Java动态追踪技术探究

BTrace

Instrumentation

Instrumentation 新功能

深入理解JVM虚拟机:(五)虚拟机类加载机制(上)

深入理解JVM虚拟机:(六)虚拟机类加载机制(下)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值