1.4 Quarkus介绍
在使用Java平台开发微服务时,可以选择的框架有很多种。Spring和Spring Boot是目前主流的Java应用开发框架,同样可以用在微服务开发中。Spring 5.3中增加了对GraalVM的支持,弥补了Spring框架的一个很大的短板,使得Spring框架更加适用于开发云原生应用。
不过Spring也存在一些局限性。作为一个通用的Java应用开发框架,在使用Spring开发微服务时的一个突出问题是它过于复杂。这主要是因为Spring框架有很长的历史,所包含的内容非常多。这些限制使得Spring开发云原生应用的性能不是很理想。除了Spring之外,社区中出现了一些新的云原生开发框架,包括本书要介绍的Quarkus、Helidon和Micronaut等。这些新框架的特点是专门针对云原生而设计,可以解决Spring的问题。
在介绍具体的开发框架之前,首先介绍Eclipse基金会提出的MicroProfile规范。
1 MicroProfile规范介绍
作为Eclipse基金会下的孵化项目,MicroProfile规范的目标是优化企业Java应用开发来适应微服务架构。微服务架构有一些常见的问题需要解决。MicroProfile的目标是提供一套通用的API和功能集来满足微服务开发的需求。从作用上来说,MicroProfile与Spring Cloud存在一定的相似性,不过MicroProfile只关注规范和开放API,并不关心具体的实现。MicroProfile也会用到Jakarta EE相关的规范,侧重于企业级Java应用的开发。
MicroProfile规范由一系列子规范组成。MicroProfile规范有自己的版本号,其中的子规范有各自的版本号。表1-3列出了MicroProfile 4.0中包含的子规范及其版本号。
表1-3 MicroProfile规范的组成
由于MicroProfile只提供了规范和API接口,相关的具体实现由社区来提供。对于同一个规范,可能有不同的实现可供选择。SmallRye项目提供了很多MicroProfile规范的实现。
2 GraalVM介绍及其意义
介绍Quarkus时就必须要提到GraalVM,这是Quarkus能够提升性能的基础。GraalVM是Oracle开发的高性能的多语言运行时。除了Java和其他JVM语言(如Kotlin、Groovy和Scala等)之外,GraalVM还提供了对JavaScript、Python、Ruby、R、C/C++和Rust等的支持。如果应用在实现中使用了多种编程语言,在运行环境上仅需要安装GraalVM即可,而不再需要同时安装多个语言的运行时。这可以简化应用的部署和运维。GraalVM还支持应用在不同语言之间的互操作。比如,一个NodeJS开发的API可以调用Java标准库中的方法,反之亦然。
GraalVM的另外一个重要功能是创建原生可执行文件。Java平台的一个重要卖点是编写一次,到处运行。Java源代码被编译成平台无关的字节代码,再由虚拟机在不同的平台上运行。虚拟机屏蔽了底层平台的差异性。对于一个打包好的JAR文件,不管是在Windows、Linux还是macOS上运行,所产生的效果是相同的。Java的这个特点简化了Java应用的部署和运维,但是也不可避免地影响了Java应用的性能。随着虚拟机的不断优化,这种对性能的影响在不断减少,但是性能的损失是客观存在的。
在运行Java应用时需要Java虚拟机的支持。即便是最简单的输出“Hello World”的Java应用也需要一个完整的Java虚拟机才能运行。在部署Java应用时,必须首先确保目标环境提供了兼容的Java虚拟机。当以容器镜像来部署时,虚拟机本身会占据镜像的大部分空间。应用自身的JAR文件可能只有几十MB,而虚拟机自身的文件在解压之后有数百MB。这就意味着大量的存储空间被消耗在应用无关的地方。Java 9中引入了模块系统,对JDK也进行了模块化。应用可以使用jlink创建自定义JDK,仅包含应用所需的JDK模块。通过这种方式可以减少JDK的尺寸,但是仍然无法从根本上解决这个问题。
GraalVM提供的原生镜像(Native Image)功能可以把Java代码预先编译(Ahead-Of-Time,AOT)成独立的可执行文件。该可执行文件包括了应用本身的代码、所依赖的第三方库和JDK本身。该执行文件并不运行在Java虚拟机之上,而是名为Substrate的虚拟机。与运行在传统的Java虚拟机上相比,原生可执行文件在运行时的启动速度更快,所耗费的内存资源更少。
GraalVM生成的原生可执行文件与底层平台相关,不能在当前平台之外的其他平台上运行。对云原生应用来说,这并不是一个问题。云原生应用的设计目标是在容器中运行,所运行的底层平台是固定的。
3 Quarkus项目介绍
Quarkus是Kubernetes原生的Java应用开发框架,并对OpenJDK HotSpot虚拟机和GraalVM进行了优化。Quarkus的开发由Red Hat提供支持。
Quarkus的设计哲学是容器优先,对在容器中运行进行了针对性的优化。在容器中运行时,Quarkus应用的启动速度非常快,运行时所消耗的资源也更少。
Quarkus同时支持传统的命令式和反应式非阻塞两种编程模式。对开发人员来说,命令式编程更加熟悉,容易理解和掌握;反应式编程虽然较难理解,但是可以提供更好的性能。
Quarkus框架的内核很小,大部分工作都由不同的扩展来完成。应用可以根据需要来选择要启用的扩展。这就保证了Quarkus应用所包含的第三方依赖尽量最少。
Quarkus框架构建在已有的规范和流行框架的基础上,降低了学习的难度。Quarkus中使用的第三方框架包括Eclipse Vert.x、RESTEasy、JPA、Hibernate、MicroProfile规范及其实现库和SmallRye Messaging等。
4 Quarkus提升Java应用性能
Quarkus使用多种方式来提升Java微服务的性能。Quarkus在构建时会进行尽可能多的处理,提升应用的启动速度。构建之后的应用中只包含运行时真正需要的类,减少了应用的尺寸。Quarkus也尽可能地减少反射的使用。在创建原生可执行文件时,Quarkus会在构建阶段进行预启动,并把部分代码的运行结果直接记录在可执行文件中。当应用运行时,框架的启动代码已经被执行过了,可以进一步提升启动速度。
一般的应用在启动时需要进行很多初始化的工作,包括读取和解析配置文件,扫描CLASSPATH中找到的类上的注解,生成额外的类的字节代码,创建动态代理等。这些操作都会耗费一定的时间,越复杂的应用所需要的时间越长。只有完成这些初始化工作之后,应用才能执行自身的业务逻辑。当运行在Kubernetes上时,这意味着需要等待较长的时间才能让容器进入可用的状态。这对于故障恢复和水平扩展都很不利。
在目前的应用开发框架中,一个流行的趋势是优化开发人员的开发体验,而把大部分的工作交由框架来完成。开发人员通常只需要使用框架提供的注解就可以进行开发。比如在进行数据库访问时,只需要定义某个实体类对应的仓库接口即可,具体实现由框架在运行时提供。在简化开发的同时,也增加了应用启动时的工作量,使得启动速度更慢。
在应用运行时,有些代码只会在启动时运行一次,仅作为应用初始化的辅助。典型的例子是XML类型的配置文件的解析。解析XML文件需要相应的第三方库的支持,而解析操作只在启动时执行一次。在完成解析之后,XML相关的库就不再需要,但是会一直存在于运行的应用中。这会造成不必要的内存浪费。
由于启动时需要执行初始化的工作,在整个应用的运行过程中,对于资源的消耗是不平均的。对于一些相对简单的应用来说,在刚启动的时间段之内,应用会需要较多的CPU和内存资源来进行初始化。等初始化完成之后,应用所消耗的资源会降低很多。尤其是微服务的应用,每个微服务在没有收到请求时,所占用的资源非常少。
这种资源的消耗模式会对Kubernetes上的部署带来一些挑战。当运行在Kubernetes上时,推荐的实践是为Pod中的容器指定请求的资源和资源的上限。一个应用在正常运行时可能仅需要128MB的内存,但是在启动时需要的内存可能是256MB。为了让应用可以正常启动,内存的上限必须设置为256MB,而不是实际的128MB。这实际上造成了资源的浪费。
这个问题也可能会影响Kubernetes的自动水平扩展。如果自动水平扩展使用CPU的利用率作为触发条件,在容器启动时,由于CPU的利用率会很高,超过自动水平扩展设置的阈值,导致Kubernetes自动创建新的Pod。在前一个容器完成启动之后,CPU的利用率会直线下降。由自动水平扩展所创建的Pod又会被销毁。
使用传统框架的Java应用分成构建和运行两个阶段。构建阶段的目标是把Java源代码转换成可执行的字节代码。构建阶段会执行的动作包括编译源代码,调用注解处理器和生成字节代码等。运行阶段则是以Java虚拟机来运行应用,在完成框架的初始化之后,执行应用的代码。
Quarkus在构建时添加了一个额外的阶段,称之为增强阶段,用来进行附加的处理。Quarkus应用的构建和启动一共分成三个阶段,分别是增强、静态初始化和运行时初始化。
1)在增强阶段,Quarkus的扩展会加载并扫描应用的字节代码和配置。这些扩展会根据收集到应用的元数据来进行预先的处理。处理的结果会直接记录在字节代码中,作为应用的一部分。比如,Hibernate会对添加了注解@Entity的实体类进行处理来改写其字节代码。Quarkus的Hibernate扩展在增强阶段就完成了对实体类的处理,在运行时可以直接使用。
2)静态初始化阶段负责执行一些额外的动作。这个阶段主要是针对原生可执行程序的构建。在构建原生可执行程序时,静态初始化的代码会在一个正常的JVM中运行,所产生的对象会被直接序列化到可执行文件中。如果不创建原生可执行文件,静态初始化阶段和运行时初始化阶段并没有区别,只是首先运行而已。
3)运行时初始化阶段的代码在应用的主方法中运行。
增强阶段所需的依赖并不需要出现在应用运行时。以前面提到的读取XML配置文件为例,当Quarkus扩展读取配置文件并解析之后,所生成的字节代码被记录在应用中。XML解析相关的库不会出现在应用中。
实际上,Quarkus的扩展由两个模块组成,分别是运行时模块和部署模块。运行时模块提供了Quarkus应用在运行时使用的API,作为应用的依赖;部署模块在Quarkus应用构建时进行处理,并不是应用的依赖,而是由Quarkus在构建时自动添加。这两种模块的划分,可以更好地区分构建和运行时所需的依赖。
通过一个简单的数字对比可以看出Quarkus对应用的性能提升。使用Quarkus和GraalVM的简单REST应用的启动时间仅为16ms,占用内存仅12MB。如果使用传统的基于Java虚拟机实现,应用的启动时间需要4.3s,占用内存为136MB。
[1] CNCF的全称是Cloud Native Computing Foundation,负责维护很多云原生相关的项目。
[2] 更多内容参见https: //kubernetes.io/zh/docs/concepts/extend-kubernetes/operator/。
[3] Kubernetes可以使用任何符合Container Runtime Interface规范的容器运行时。