云原生Java框架—Quarkus

一、简介

Quarkus是由Red Hat公司于2018年开始研发的一款面向云原生的Java开发框架,旨在使 Java 成为Kubernetes 和无服务环境中的领先平台,目前最新版本为2.3.0,已生产可用。主要特点是:

  1. 云原生:支持通过GraalVM Native-Image将Java应用打包成本地二进制镜像,减少内存使用、缩短应用启动时间

  2. 低使用成本:与常见的Java标准、框架等协同工作,如:Spring、Hibernate、Netty、RestEasy等,无需学习新的标准和规范

  3. 高开发效率:代码热更新,无需重启即可查看代码改动的结果(dev环境下),支持单元测试、本地debug等

  4. 同时支持命令式和响应式代码

  5. 支持同时运行在GraalVM和HotSpot两种虚拟机上

 

Quarkus运行时内存、启动时间对比

二、背景—Quarkus的初衷

2.1 困境

在JVM上运行企业级Java应用所需的漫长初始启动时间和庞大的内存消耗无法完美适应云原生的需要

  1. Java虚拟机的设计本身是为了满足长时间稳定运行的服务,而K8S、Serverless等场景下需要应用快速灵活的启动、销毁,难以长时间稳定运行,导致JIT编译器在程序启动时占用了比较多的时间但却很难在运行时发挥出威力。例如:AWS的Lambda函数支持的最长运行时间是15min(刚开始时仅支持5min的运行时间)。

     

  2. 从上图可以看出:应用程序在启动过程中,类加载和JIT编译消耗了比较多的时间。
    JIT原理简介:

  3. 容器环境下,每个Java应用程序启动时都需要先启动Docker容器,然后在Docker内启动JVM,最后JVM再加载应用,整个过程耗时较长

  4. 打包的镜像中,JDK动辄上百兆,远大于应用本身的大小

  5. 构建的镜像中存在大量运行时未使用的代码和依赖

  6. 内存占用大,一个空的Spring Web内存占用约为122MB左右,而Go Web应用在10MB左右 GraalVM多语言架构

2.2 契机

GraalVM的出现为Java开发者拥抱云原生打开了一扇窗户 GraalVM是Oracle发布的通用型虚拟机,当然也可以用来运行Java程序,被称为下一代Java虚拟机。2016年6月发布第一个release版本。主要特点是:

  • 高性能:GraalVM 的高性能AOT 编译器支持在构建阶段生成可直接运行的本地代码,由于一系列高级的编译器优化和积极的内联技术,使得生成的本地代码运行速度更快,产生的垃圾和占用的CPU均 更少,可极大降低云和基础设施的成本。同时,由于没有使用JIT运行时优化,程序在启动时即可达到峰值性能,不需要预热时间。

    AOT编译过程

    官方数据:在Renaissance 基准测试套件中,GraalVM 企业版实现了比 OpenJDK 8 高 1.55 倍的几何平均加速,以及与 OpenJDK 11 类似的结果。

  • 多语言:支持多种语言编写的程序运行在GraalVM上并且支持多种语言之间互相调用

    GraalVM多语言架构

    GraalVM支持多语言的原理:将其他语言编译成 .class文件

  • 快速启动、减少内存使用:GraalVM 0.20版本开始出现的一个极小型(相比于HotSpot)的运行时环境Substrate VM:1. 完全脱离了HotSpot虚拟机,拥有独立的运行时,包含异常处理,同步,线程管理,内存管理(垃圾回收)和JNI等组件;2. 在AOT编译时已保存初始化好的堆快照,并支持内存占用对比启动时间对比从程序入口直接开始运行。

    内存占用对比

    启动时间对比

  • 完善的工具:提供了丰富的开发工具,用于Java及其他语言的Debug、监控、Profile及资源消耗优 化

参考资料:InfoQ Quarkus Java框架答疑周志明—云原生时代的Java

三、困难与挑战—封闭的程序空间

传统的Java程序空间是开放的,即完全可以在运行时动态的加载配置、类等资源并进行初始化。但GraalVM进行Native-Image打包时,需要在构建过程中将程序运行时用到的所有类、资源等初始化好并以堆内存快照的形式保存起来,即Native-Image要求程序的运行空间必须是封闭的。实际上,SubstrateVM在构建Native-Image的阶段将探索程序的整个编译空间,并通过静态分析的方法推算出所有调用的 目标方法,最终将所有推算出的方法都纳入编译范围,这将导致动态加载其他类库、反射、动态代理和CGlib代理等功能无法正常使用。例如:传统的程序支持运行时,根据用于传入的类全限定名和方法来动态加载类并反射调用目标方法,这种功能如何在AOT编译阶段实现?

3.1 如何解决反射的使用

  • 自动检测:静态分析器主动拦截诸如: Class.forName(String) , Class.forName(String, ClassLoader) , Class.getDeclaredField(String) , Class.getField(String) , Class.getDeclaredMethod(String, Class[]) , Class.getMethod(String, Class[]) , Class.getDeclaredConstructor(Class[]) , and Class.getConstructor(Class[]) )等方 法,如果字符串可以被替换成常量,则直接在构建阶段将其替换,如果检测出错误,则直接将对应的代码替换成错误。

  • 手动配置:在配置文件中配置运行时需要被反射访问到的类,示例:

    [
        {
            "name" : "java.lang.Class",
            "allDeclaredConstructors" : true,
            "allPublicConstructors" : true,
            "allDeclaredMethods" : true,
            "allPublicMethods" : true,
            "allDeclaredClasses" : true,
            "allPublicClasses" : true
        },
        {
            "name" : "java.lang.String",
            "fields" : [
                { "name" : "value" },
                { "name" : "hash" }
            ],
            "methods" : [
                { "name" : "<init>", "parameterTypes" : [] },
                { "name" : "<init>", "parameterTypes" : ["char[]"] },
                { "name" : "charAt" },
                { "name" : "format", "parameterTypes" : ["java.lang.String", "java.lang.Object[]"] }
            ]
        },
        {
            "name" : "java.lang.String$CaseInsensitiveComparator",
            "methods" : [
                { "name" : "compare" }
            ]
        }
    ]
  • native-image-agent收集:在项目常规运行期间,用agent与JVM交互,拦截所有查找类、方法、字段、资源或者请求代理访问的调用。然后agent将在指定的目录下生成以下文件:jniconfig.json,reflect-config.json,proxy-config.json以及resource-config.json等。

3.2 如何使用动态代理(不支持使用CGLib代理)

  • 自动检测:静态分析器主动拦截诸如: java.lang.reflect.Proxy.newProxyInstance(ClassLoader, Class<?>[],InvocationHandler)and java.lang.reflect.Proxy.getProxyClass(ClassLoader,Class<?>[])等方法的调用,获取代理接口数组的值。

  • 手动配置:在配置文件中配置运行时动态代理需要用到的类

    [
        ["java.lang.AutoCloseable", "java.util.Comparator"],
        ["java.util.Comparator"],
        ["java.util.List"]
    ]

    3.3 其他问题

    • AOT编译需要更长的时间(几十分钟),需要消耗更多的内存(几十GB)

    • 提前初始化并不总是安全的

    class A {
        static B b = new B();
    }
    class B {
        static {
            C.dosomething();
        }
    }
    class C {
        static long currentTime;
        static {
            currentTime=System.currentTimeMillis();
        }
        static void dosomething(){…}
    }

    显示指定需要在运行时初始化的类。 参考资料:Static Compilation of Java Applications at Alibaba at Scale阿里巴巴的GraalVM Nativeimage

    四、简单示例

    4.1 QuickStart

    • Quarkus + Native、Quarkus + JVM、Traditional SpringCloud 启动时间、消耗内存的比较

    • 使用 native-image-agent 自动生成Reflect、DynamicProxy等文件

    4.2 Dubbo支持GraalVM打包成Native Image

    4.3 Quarkus+GraalVM应用上线

  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值