/common-hotswap

提供几种游戏服务器中 Java 热更方案

Primary LanguageJava

Java 代码热部署总结

本工程提供的方案

核心代码:common-hotswap-core

有以下两种方案:

  • 动态加载ClassJar

    • 基于Instrumentation的函数体级别更热

      此更新方案只允许更新方法体内的代码。比如一个类某个方法有bug,在方法体内修改完之后可以热更,但是如果新的类中添加了新的字段,新的方法或者父类发生变化了,是不行的。不过大部我们的bug都是方法内的逻辑bug,使用此方案还是可以的。

    • 基于InstrumentationJar动态加载

      此更新方案允许新增Class。比如某个方法需要增加一个排序比较器Comparator,或者做代理转发(把原方法的调用转发到新的Class方法调用上),等等。。

    agent包:

    源码:ariescat-hotswap-agent

    Instrumentation实例的获取需要从agent代理程序上获取,上面的agent包同时支持premain方式和agentmain方式。

    premain方式启动:需要加上启动参数-javaagent:libs\\hotswap-agent-1.1.jar

    agentmain方式启动:采用pid绑定进行attach获取VirtualMachine,由VirtualMachineloadAgent

    核心实现:com.ariescat.hotswap.instrument.ClassesHotLoadWatch

    测试代码:com.ariescat.hotswap.example.instrument.TestClassesHotLoadWatch

  • 动态编译脚本,可随意更改类结构进行热更改

    采用JDK动态编译,并整合进Spring架构,用户无需关注具体实现方式

    用法:

    把自己的热点代码写进一个单独的非source folders的文件夹(本工程是script),然后用mavenmaven-resourczes-plugin把该文件夹当成resources编译进运行目录。

    测试代码:com.ariescat.hotswap.example.javacode.TestSpringInject

    注意事项

    经测试发现如果在脚本里开启循环线程,热更的话之前的线程会得不到释放,可能会导致内存溢出。因此尽量不要在脚本里开启循环线程:

    public class Person implements IHello {
        public Person() {
    //        TODO 如果在脚本里开启循环线程,热更的话会得不到释放,可能会导致内存溢出。
    //        new Thread(()->{
    //            while (true) {
    //                System.err.println("Person xxx1");
    //            }
    //        }).start();
        }
    }
  • 以上两种方案应该可以满足大部分生产环境上的bug修复。

本人探索到的一些热更方式

  • JDK提供

    • 通过 premainagentmain(推荐后者)获取到 Instrumentation 这个类的实例, 调用retransformClass/redefineClass进行函数体级别的字节码更新

      基于Attach机制实现的热更新,更新类需要与原来的类在包名,类名,修饰符上完全一致,否则在classRedefine过程中会产生classname don't match 的异常。

      例如显示这样的报错:redefineClasses exception class redefinition failed: attempted to delete a method.

      具体来说,JVM热更新局限总结:

      1. 函数参数格式不能修改,只能修改函数内部的逻辑
      2. 不能增加类的函数或变量
      3. 函数必须能够退出,如果有函数在死循环中,无法执行更新类(笔者实验发现,死循环跳出之后,再执行类的时候,才会是更新类)
    • 通过Instrumentation#appendToSystemClassLoaderSearch来增加一个classpath,可以动态加载Jar包。

      只要能动态加载Jar包,就能做很多事情了

      See:AgentAddAnonymousInnerClass

    • 定义不同的classloader

      该方式必须要使用新的ClassLoader实例来创建类的对象,运行新对象的方法。

      Tomcat的动态部署就是监听war变化,然后调用StandardContext.reload(),用新的WebContextClassLoader实例来加载war,然后初始化servlet来实现。类似的实现还有OSGi等。

    • JavaCompiler 动态编译( JDK 1.6 开始引入 )

  • 脚本

    • java结合groovy,把热点代码写进脚本里
  • 第三方

    • Github HotswapAgent

      该工程基于dcevm,需要给jvm打上补丁(也就是要修改原生的jvm),该做法存在风险(自己团队没有在生产环境上跑过这种补丁,是否会存在未知风险?),同时没有对最新的JDK进行支持(目前最新补丁支持到 Java 8u181 build 2)。

    • 阿里arthas

      使用Arthas三个命令就可以搞定热更新 :

      jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java
      
      mc /tmp/UserController.java -d /tmp
      
      redefine /tmp/com/example/demo/arthas/user/UserController.class
      • 这个工具还可以协助完成下面这些事情(转自官网):
        1. 这个类是从哪个jar包加载而来的?
        2. 为什么会报各种类相关的Exception
        3. 线上遇到问题无法debug好蛋疼,难道只能反复通过增加System.out或通过加日志再重新发布吗?
        4. 线上的代码为什么没有执行到这里?是由于代码没有commit?还是搞错了分支?
        5. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现。
        6. 是否有一个全局视角来查看系统的运行状况?
        7. 有什么办法可以监控到JVM的实时运行状态?

参考