Spring Native初探

Spring Native初探_第1张图片

近几年“原生”一词一直泛滥在云计算、边缘计算等领域中,而原生宠幸的语言也一直都是Golang,Rust等脱离Sandbox运行的开发语言。Java得益于上世纪流行的一次编译,到处执行的理念,流行至今,但也因为这个原因,导致Java程序脱离不了JVM运行环境,使得不那么受原生程序的青睐。在云原生泛滥的今天,臃肿的JVM使Java应用程序对比其他语言显得无比的庞大,各路大神也想了很多方式让Java变的更“原生”。最近Spring推出了Spring Native概念,并参考了其他大牛的文章后,今天我们就一探如何让用Spring Boot编写原生应用。

Spring Native借助GraalVM native-image编译器来编译Spring应用,所以我们需要先来了解一下GraalVM。大部分脚本语言或者有动态特效的语言都需要一个语言虚拟机运行,比如CPython,Lua,Erlang,Java,Ruby,R,JS,PHP,Perl,APL等等,但是这些语言的虚拟机水平参差不齐,例如JVM的HotSpotVM、JS的V8都是“艺术”级别的,但CPython的VM就不忍直视。那能不能用一个“艺术”级别的虚拟机跑所有的语言呢?GraalVM就是这么一个高性能的救世主,它使用运行在JVM上的Truffle语言框架,将AST节点编译为机器代码,使用户只需要实现具体语言AST解释器,就能实现性能足够好的虚拟机,而实现这个编译器也是一个Java写的即时编译器Graal,GraalVM也因此得名。

也许有同学会问了怎么用Java语言编译Java代码呢,而且还是这么高性能?这我们就要说说JEP 243的JVMCI。众所周知,HotSpot JVM内置了两个C++写的即时编译器(JIT)C1和C2,一般频繁的代码先用C1编译,如果热点继续,那么会使用C2编译。JVMCI相当于把本该交给C2编译的代码交给高级JIT:Graal编译,说到底就是将一段byte[]在运行时换成另一段byte[]。

那像Go和C/C++这类语言是否也能运行在JVM上呢?答案是肯定的。解决方案是将C/C++这些语言用一些工具(如Clang)转换为LLVM IR,然后使用基于Truffle的AST解释LLVM IR即可。(但,我们为啥要这么做??)

Spring Native初探_第2张图片

到目前为止,几乎所有的语言都能在以JVM为基础,以Graal即时编译器为核心的虚拟机上运行起来了,但大家已经一定疑惑了,程序运行需要依赖JVM,而JVM必须提前安装JDK环境,而且自身启动慢,内存负载高,就不能把程序直接打包成平台相关可执行文件吗?答案是SubstrateVM,它借助Graal编译器,可以将Java程序AOT编译为可执行程序。所以万能的Graal编译器不仅能JIT,还能AOT。

Spring Native初探_第3张图片

好了,我们这些“CRUD仔”们了解这些基础魔法就足够了,至于SVM如何解决反射、GC等问题的高级魔法还是交给大牛们吧。现在进入我们的正题:用Spring Boot来编写一个原生应用。

制作过程

1517896e7b6c4128876bf2f76ef397ef.png

Step 1:安装GraalVM和依赖工具

因为大家都比较熟悉JDK安装过程,所以本过程带过了一些细节,不做重点讲解。首先我们需要安装GraalVM,笔者以自己的macOS系统为例,其他系统请参考官方安装文档。比较遗憾的是,GraalVM并没有提供针对M1优化的AArch64平台的包,我们只能使用AMD64平台,下载地址点击这里[1],我们使用Java 17版本的darwin压缩包,解压至:

/Library/Java/JavaVirtualMachines/

并且设置JAVA_HOME:

export GRAALVM17_HOME=$(/usr/libexec/java_home -v 17)
export JAVA_HOME=$GRAALVM17_HOME

为了使用方便也可以设置Alias:

alias java17g='export JAVA_HOME=$GRAALVM17_HOME;java -version'

由于macOS的安全限制,需要删除quarantine:

$ sudo xattr -r -d com.apple.quarantine $GRAALVM17_HOME

我们依然需要Maven作为本次探索的打包工具,请大家自行安装Maven,这里不再赘述。一切安装完成,我们可以运行java -version和mvn -v来验证一下安装是否成功。

$ java -version
openjdk version "17.0.1" 2021-10-19
OpenJDK Runtime Environment GraalVM CE 21.3.0 (build 17.0.1+12-jvmci-21.3-b05)
OpenJDK 64-Bit Server VM GraalVM CE 21.3.0 (build 17.0.1+12-jvmci-21.3-b05, mixed mode, sharing)

最后,我们需要安装native-image作为原生代码编译工具:

$ cd $GRAALVM_HOME/bin
$ ./gu install native-image

当然,Xcode工具包因为包含GCC等工具,也必须安装,如已经安装可跳过。

$ sudo xcode-select --install

Step 2:建立Spring Boot应用

按着官方的向导建立一个基于Spring Boot 2.6.2版本,Java版本使用1.8的Web应用。注意一定要使用最新的2.6.2+版本,否则不支持AOT功能。并且,Java版本也只支持1.8。目录如下:

.
├── HELP.md
├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── ajk
│   │   │           └── testspringnative
│   │   │               └── TestSpringNativeApplication.java
│   │   └── resources
│   │       ├── application.yml
│   │       ├── static
│   │       └── templates
│   └── test
│       └── java
│           └── com
│               └── ajk
│                   └── testspringnative
│                       └── TestSpringNativeApplicationTests.java

其中TestSpringNativeApplication代码如下:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class TestSpringNativeApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestSpringNativeApplication.class, args);
    }

    @GetMapping("/hello")
    public String hello(@RequestParam(value = "name", defaultValue = "World") String name) {
        return String.format("Hello %s!", name);
    }

}

配置文件application.yml代码如下:

server:
  port: 9000
  shutdown: graceful

spring:
  profiles:
    active: default

logging:
  level:
    root: info

Step 3:配置Maven

为了方便演示,我们使用了最简单的代码和配置,重点是Maven的配置,以至于我需要用整个Step来说明。

由于使用了官方向导生成的项目,所以基础pom.xml文件如下:


    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.6.2
         
    
    com.ajk
    test-spring-native
    0.0.1-SNAPSHOT
    test-spring-native
    test-spring-native
    
        1.8
    
    
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        
    
    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

接下来我们开始配置Spring Boot Native,官方有两种方式实现编译原生应用:

  • 用Spring Boot Buildpacks生成包含原生应用的OCI容器。

  • 用GraalVM native image Maven plugin生成原生应用。

由于篇幅关系,这里只介绍第二种方式,即编译为原生应用。

首先增加包和插件依赖库:


    
    
        spring-release
        Spring release
        https://repo.spring.io/release
    


    
    
        spring-release
        Spring release
        https://repo.spring.io/release
    

再次确认我们的Spring Boot版本为2.6.2(因为Spring Native 0.11.1版本支持此版本),并添加如下依赖:


    
    
        org.springframework.experimental
        spring-native
        0.11.1
    

添加Spring AOT部署插件:


    
        
        
            org.springframework.experimental
            spring-aot-maven-plugin
            0.11.1
            
                
                    generate
                    
                        generate
                    
                
                
                    test-generate
                    
                        test-generate
                    
                
            
        
    

再添加原生编译插件,这里使用一个profile来更好的管理:


    
        native
        
            
                
                    org.graalvm.buildtools
                    native-maven-plugin
                    0.9.9
                    true
                    
                        
                            build-native
                            
                                build
                            
                            package
                        
                    
                    
                        
                    
                
                
                
                    org.springframework.boot
                    spring-boot-maven-plugin
                    
                        exec
                    
                
            
        
    

一切妥当!开始编译吧!

$ mvn clean -Pnative -DskipTests package

官方推荐编译的机器不能少于8核8G内存,否则编译工具会报错。在我的M1的机器上,编译大概需要10分钟左右,编译时CPU峰值使用率大概在50%,内存占用6.9GB。

简单评测

2b022febf5c82b6f81473ff92acb9654.png

首先看一下编译文件大小对比:

  • fatjar包文件为17.8M(不包含JRE),原生可执行文件为68.2M。

  • 使用spring-boot-maven-plugin生成包含JRE运行环境的容器镜像大小为270M,而使用Tiny Core Linux+原生应用的形式,镜像大小可以控制在100M以内,为96M。压缩比达到35%之多。

再来看看启动速度对比:

  • fatjar启动时间为8.2s

  • 原生文件启动时间为5.6s

程序使用CPU和内存对比:

  • fatjar空载CPU 0.5%,内存使用528M

  • 原生应用空载CPU 0.3%,内存使用85M

如下表格:


FatJar包 Native包
应用大小 17.8M 68.2M
容器镜像大小 270M 96M
启动速度 8.2s 5.6s
空载CPU 0.5% 0.3%
空载内存 528M 85M

总体来讲,原生应用从产物大小,启动速度,运行负载来讲都优与Jar包应用,这还是在没有针对arm的指令集做优化的基础上的,但对比官方宣传的内存使用20M内存占用还有一定差距。

总结

606700112818ed550529aa0b7d2d2157.png

经过几天折腾,GraalVM的性能即使不编译为原生应用也优于HotSpot VM,在编译为原生应用后,性能也有一定的提升。但目前Spring Native还不够成熟,笔者想用undertow代替Tomcat Web容器而编译后的原生应用,始终无法运行。相信后面版本应该会修复一些问题。

本文总结了一种编译原生的方式,另一种生成原生镜像的方式大家可以自行研究(注意,编译成原生镜像需要阅读大量文章)。另外,由于时间有限,在两者的压测过程中,原生应用GC回收内存速度快于jar包应用,大家也可以深入研究原生内存回收方式。

所有代码可在GitHub[2]上参考。

相关链接:

  1. https://github.com/graalvm/graalvm-ce-builds/releases

  2. https://github.com/huang-kai/test-spring-native

参考资料:

  1. https://www.graalvm.org/docs/introduction/

  2. https://docs.spring.io/spring-native/docs/current/reference/htmlsingle/

  3. https://openjdk.java.net/jeps/243

  4. http://trufflesuite.com/truffle/

  5. https://github.com/graalvm/labs-openjdk-17

作者:黄凯,58安居客新房技术部负责人。

Kubernetes线下实战与CKA培训

8e299c9c131d25a8771deee3087f289e.png

本次培训在深圳开班,基于最新考纲,理论结合实战,通过线下授课、考题解读、模拟演练等方式,帮助学员快速掌握Kubernetes的理论知识和专业技能,并针对考试做特别强化训练,让学员能从容面对CKA认证考试,使学员既能掌握Kubernetes相关知识,又能通过CKA认证考试,理论、实践、考证一网打尽,学员可多次参加培训,直到通过认证。点击下方图片或者阅读原文链接查看详情。

Spring Native初探_第4张图片

你可能感兴趣的:(编译器,java,maven,spring,boot,分布式)