present_to_all

Spring Native

shenlan
shenlan 7月13日

Spring Native

其实我主要使用的编程语言是 Java 来着

背景

springboot 2.5.2
spring native 0.10.1

前两天在 spring 上生成项目的时候,忽然发现有个叫 Spring Native 的依赖,听名字就觉得不一般,赶紧玩一玩。

简要介绍

spring native 初步看下来是为了减少打包体积,减少内存使用量,加速容器启动和启动服务速度。

先看数据

stats

CONTAINER IDNAMECPU %MEM USAGE / LIMIT MEM %
05cbc775970acranky_moser0.03%31.82MiB / 7.506GiB

log

2021-07-07 08:04:11.145  INFO 1 --- [           main] o.s.nativex.NativeListener               : This application is bootstrapped with code generated with Spring AOT

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.2)

2021-07-07 08:04:11.146  INFO 1 --- [           main] c.e.demo.SpringNativeAppApplication      : Starting SpringNativeAppApplication using Java 11.0.11 on 05cbc775970a with PID 1 (/workspace/com.example.demo.SpringNativeAppApplication started by cnb in /workspace)
2021-07-07 08:04:11.146  INFO 1 --- [           main] c.e.demo.SpringNativeAppApplication      : No active profile set, falling back to default profiles: default
2021-07-07 08:04:11.174  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
Jul 07, 2021 8:04:11 AM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-nio-8080"]
Jul 07, 2021 8:04:11 AM org.apache.catalina.core.StandardService startInternal
INFO: Starting service [Tomcat]
Jul 07, 2021 8:04:11 AM org.apache.catalina.core.StandardEngine startInternal
INFO: Starting Servlet engine: [Apache Tomcat/9.0.48]
Jul 07, 2021 8:04:11 AM org.apache.catalina.core.ApplicationContext log
INFO: Initializing Spring embedded WebApplicationContext
2021-07-07 08:04:11.175  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 28 ms
Jul 07, 2021 8:04:11 AM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["http-nio-8080"]
2021-07-07 08:04:11.193  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-07-07 08:04:11.194  INFO 1 --- [           main] c.e.demo.SpringNativeAppApplication      : Started SpringNativeAppApplication in 0.053 seconds (JVM running for 0.055)

分析

我有两个问题:

  1. 为什么存在?
  2. 为什么基于 docker?

1. Run anywhere VS Fast

我们先不考虑第二问题。先考虑传统部署方式。

部署流程:

server

  1. 创建操作系统实例
  2. 安装 JVM 环境(通常直接使用 JDK 了 也就是通过 sdkman 部署)
  3. 下载 JAR
  4. 运行项目 提供服务(java -jar spring-app.jar)

developer

  1. git pull
  2. mvn package
  3. 上传 JAR

在这个过程中 最慢的两个步骤是

  • 上传 / 下载 JAR(在一个项目代码量不算太多的情况下 100M+ 很常见)
  • 构建 / 运行项目

这两个问题的主要原因其实就是曾经 Java 的优势: “一次构建,到处运行”。

Java 这么设计的目的在 10 年前可谓是大放光彩,因为谁都不知道 将来的风向在哪里,如果 某个大头突然没了,那么对于项目来说 是直接死掉的。

例如 X86 架构 切换到 ARM 架构,虽然过程还需要需要适配,但是 项目中的大部分代码是不需要重写的。

但是这个特性会造成一些问题,现实的情况是 用户非常依赖系统环境,我项目的代码不需要到处运行。例如:Android OS 的应用 apk。

这个额外的特性会带来额外的开销,并且很大,所以 Android 在 4.4 版本 就将 Dalvik(DVM) 替换到了 ART。apk 就是 apk 它是针对于 Android OS 来进行编程的,不需要在 Ubuntu 里运行。4.4 版本之后 Android 应用启动速度明显快了很多。但是这仍然比不上原生应用。

Android 的 ART 和今天要讲的 Spring native 原理类似。

javac 编译一个 java 代码之后 会生成一个 class 文件,这个文件不是操作系统可执行的二进制文件,而是 编程语言 和 JVM 的一个中间二进制文件,这个二进制文件可以屏蔽操作系统差异,而操作系统之间的差异 由 JVM 来解决。

再回到刚才的 例子 Android OS,Android 没有那么那么多的操作系统之间的差异 所以仅需要编译 对应平台版本(Android 版本碎片化,x86 架构基本死绝)的可执行二进制文件就可以了。

编译成平台可执行的二进制文件的目的 就是为了 快,为了效率。和操作系统调用可以更好交互。

2. AOT(Ahead of compilation)

既然和操作系统绑定,那 ART 的功能 岂不是每个平台都要做一遍?

非常知名的 Git 曾经就遇到这样的问题,Git 本来是在 Linux 下的一个工具,但是扛不住 windows 用户基数大,如果不做 windows 版本,那么整个市场将会被 SVN 占有。

如果要重新基于 windows 写一个 Git 又太麻烦,那为何不模拟出 linux 环境来运行 Git 呢?其实大概就是这么解决的,这里不做展开介绍了。

Docker 正是解决这个问题的钥匙,Docker 容器 可以屏蔽系统之间(部分)的差异,让不同的操作系统下 运行的容器始终都是一致的。

并且 Docker 本身市场占有率就非常高,并且 还有各种大厂和技术做背书 Google(K8s),PaaS,SaaS 等。

所以 Docker 是非常标准化的一个组件,系统环境解决了 那么 就差 ART 功能了,也就是 Spring Native。我知道有人可能了解过 jpackage,但 jpackage 仍然不够完善(JDK16)。而 Spring Native 虽然完成度不高,但是沉淀的时间明显没有 jpackage 久。

jpackage 目前为止仅能在本平台下打包出本平台下能用的包(虽然我一次都没有成功过),无法交叉打包。

但 Spring Native 就没有这个问题,因为 这个功能做的非常之“极限”,Docker 是一个仅在 Linux 环境下的一个工具(不严谨 但你可以 get 到我想表达什么意思),那么也就是说 目标环境运行已经确定了,就可以直接针对这个环境来做了,包括优化。

这也就是为什么 Spring Native 打出的包,执行起来非常快,并且体积非常非常小。

这可能是是我能在 Java 平台做出最小的应用了。

应用即黑盒

(这里我们先抛开不支持某些特性)

但是这些都不是十全十美的,因为这些优化工作,你不知道镜像的每一层的内容,你无法连接到容器内部,你无法观察应用运行状态。

但好处也相当明显,一个真正服务用户的应用, 用 Docker/K8s 类似的工具才是未来 并且 未来已来。

Spring Native 借助这些工具来打造效率更高的应用,这才是下一代应用该有的样子。

结语

这是我目前对 Spring Native 的一些看法,因为中间断断续续在写,所以有些地方可能没有讲的很全 例如 GraalVM 和 OpenJDK's VM, 并且 这个项目仍然在孵化中,它完全不适合部署在生产环境中。但是我们总是能看到大佬做出一些颠覆行业的东西,例如 Apple Silicon。

作为一个开发者 非常高兴能够推动这些优秀的东西。

本文链接:https://blog.inmind.ltd/index.php/archives/88/
This blog is under a CC BY-NC-SA 4.0 Unported License