购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

第2章
Quarkus简介

在继续理解响应式之前,让我们花一些时间来了解关于Quarkus的更多信息。

Quarkus是Kubernetes原生Java框架,是为Kubernetes、容器 和云平台量身定制的框架,但同时在裸机和虚拟机上也运行良好。Quarkus应用程序比基于传统框架的应用程序需要更少的内存,启动速度更快。Quarkus应用程序还能够被编译成原生可执行文件,这使得其消耗的内存更少,并且可以立即启动。

Quarkus的一个令人兴奋的核心方面是响应式引擎。在容器或虚拟化环境中运行时,响应式引擎对于减少内存和CPU消耗至关重要。该引擎促使所有Quarkus应用程序变得高效,同时还支持创建响应式应用程序和系统。

在本章中,你将看到Quarkus的主要特征,并且学习创建应用程序、将其部署到Kubernetes,以及创建原生构建。在第6章中,我们将详细介绍响应式引擎,并展示如何使用统一的响应式和命令式编程模型在Quarkus上进行开发。

2.1 云平台中的Java

Java现在已经快30岁了!有时候这很难想象。从三层和客户端/服务器架构的时代开始,Java随着多年来多次架构的变化而演进。然而,当一门语言问世近30年时,总会有一些不适合现代发展的部分。

这是什么意思?当Java的最初版本问世时,云平台、容器、微服务、无服务器以及任何其他与当今计算相关的术语都是无法想象的。在三层和客户端/服务器架构的时代,我们不可能期望Java语言能够像今天在容器中所需要的那样运行。

是的,Java多年来取得了很多进步,尤其是在过去几年中,Java以一种新的、更快的发布节奏出现。与此同时,Java为自己没有破坏开发人员和用户的向后兼容性而自豪。这种方法的一个巨大成本是,Java仍然保留了那些在没有容器及其提供的好处和知识的情况下构思出来的片段。

对于许多应用程序来说,Java将在未来的许多年中继续保持良好的工作状态。然而,在过去几年中,随着微服务的爆炸式发展,以及最近向无服务器的演进,Java并不能自然地适应这些部署模型。

就在几年前,当我们发现Java忽略了cgroups( https : //oreil.ly/Mbux3 )时,Java在容器中的不适用性变得显而易见。对于Docker来说,这造成了一个巨大的问题:Java无法看到分配给容器的内存量,只能看到整个物理机器的内存。

什么是cgroups

术语cgroups是控制组(control groups)的缩写,这是一个内核特性,允许将特定数量的资源(如CPU时间、内存和带宽)分配给进程。在我们的情况下,cgroups用于限制容器可以从整个系统中所获得的资源量。

在每个容器都需要在有限的内存量内工作的环境中,Java并没有发挥得很好。Java很贪婪。此外,Java将根据CPU核心数量创建应用程序线程。这导致在一个有内存和CPU约束的容器中分配了更多的线程。

这有什么大不了的吗?如果将Java应用程序部署到Kubernetes节点,而其他容器保持在它们的内存限制内,那么在容器中部署Java应用程序可能会很幸运。然后有一天出现了负载高峰,Java虚拟机消耗的内存也出现了高峰,之后出现了暴涨!Kubernetes因为容器使用了太多内存而杀死了该容器。

Java和cgroups这个特定问题从Java 10开始就解决了,而且Java开发工具包(JDK)8u131选项也可以用于启用相同的行为。可查看Rafael Benevides在Red Hat开发者网站上的“Java Inside Docker”文章( https://oreil.ly/L5Yh7 ),了解所有细节。

你可能认为Java现在在容器或云平台中会表现良好,对吗?虽然上述的修复程序可以与适当的JDK版本一起使用,但许多企业都在使用JDK 8或更早版本,而且很可能没有使用带有可用标志的JDK 8。Java在云端的问题不仅仅是cgroups。

容器不会因为获取比预期更多的内存而被杀死,这很好。然而,容器中的Java引起了人们对应用程序开始接收请求的速度以及应用程序在运行时消耗内存量的担忧。与容器中运行的其他语言相比,Java应用程序并不符合启动速度和内存消耗的要求。也许对于今天运行的许多应用程序来说,启动速度不是一个问题,但启动速度可能会影响需要快速扩展以应对大流量峰值的微服务,或者影响无服务器应用程序的冷启动时间。

开始接收请求是什么意思?虽然用于构建应用程序的框架通常会记录其启动时间,但开始接收请求指的是框架启动所需的时间。该时间并不代表应用程序在开始接收请求之前所花费的时间。这个时间是容器和云平台的关键指标!

开始接收请求的时间也可以称为第一次请求的时间。如果在应用程序接收和处理任何流量之前还需要2s~3s,则框架能否在0.5s内启动就没有多大意义了。在这样的示例中,一个新应用程序实例可能需要2.5s~3.5s才能开始接收用户请求。

诚然,对于只有几百甚至几千个内部用户的单体应用程序来说,开始接收请求的时间和内存消耗可能并不重要。可以使用Quarkus开发单体应用程序,尽管我们谈论的Quarkus的好处在开发单体应用程序时不会那么突出。然而,对于微服务,尤其是无服务器服务,这两个因素都会影响运行服务的成本和用户的可用性。

框架的启动时间通常较低,因为它会将工作延迟直到收到第一个请求。任何启动任务的其余部分都是在处理第一个请求之前执行的。惰性初始化(lazy initialization)是这种行为的另一个名称,在应用程序真正准备就绪时提供错误指示。开始接收请求的时间是应用程序启动时间的最佳度量。在无服务器工作负载以及任何使用“零扩展”(scale-to-zero)方法(只有在需要时才启动服务)的机制中,具有较短的首次请求时间至关重要。在更常见的架构中,这样快的启动时间缩短了崩溃后的恢复时间。

我们如何度量启动时间?许多方法都可以实现,包括修改端点以便在访问这些端点时输出时间戳。为了简化起见,我们将使用一个由John O'Hara在Red Hat开发的Node.js脚本( https://github.com/cescoffier/reactive-systems-in-java )。该脚本使用应用程序启动命令和访问该命令的URL以在另一个进程中启动应用程序。在计算第一次请求的时间之前,脚本会等待URL直至返回 200 ,这意味着成功。

为了便于使用,我们在 chapter-2/startup-measurement 目录中包含了GitHub存储库( https://github.com/cescoffier/reactive-systems-in-java )的内容和代码。确保你已经安装Node.js,并运行 npm install request 来保证脚本所需要的依赖项。

现在你可能会认为关于启动速度和内存消耗的讨论是一个非常主观的话题。我们完全同意,这就是为什么我们现在要使用传统的Java EE框架[如本例中是Thorntail( https://thorntail.io )]在实践中说明这些概念。我们选择Thorntail作为来自Red Hat的第一个微服务框架进行比较,而Quarkus是最新的。尽管Thorntail项目已不再被维护,这有些不幸,但好消息是Quarkus整合了Thorntail的许多想法。

在编写和运行应用程序之前,还有最后一件事。内存可能是一个模糊的术语,因为有许多类型的内存。当我们谈论内存时,通常指的是常驻集大小(RSS; https://oreil.ly/o4TzC ),而不是JVM堆大小,因为JVM堆只是Java应用程序消耗的总内存的一部分。在JVM上运行应用程序时,分配的总内存可以包括以下内容:

· 类元数据

· 线程栈

· 编译代码

· 垃圾收集

RSS表示进程在主存(RAM)占用的内存量。RSS包含JVM运行应用程序所需的所有内存,为实际占用的内存量提供了更准确的值。当我们在单个JVM进程中运行单个应用程序时,可以很容易地确保没有测量其他非应用程序进程的内存消耗。

所有性能数据都来自我们的MacBook计算机。因此,你在本章中看到的结果可能会略有不同,具体取决于你的特定硬件配置。如果你碰巧有一个Apple M1,就可以看到更好的结果!

好了,是时候运行一些代码看看启动速度和内存消耗了。

2.1.1 Thorntail示例

我们首先使用Thorntail创建一个传统的应用程序,以提供内存或RSS以及首次请求时间指标的比较。对于任何不熟悉Thorntail的人来说,该项目的重点是可定制的WildFly服务器( https://wildfly.org )。Thorntail只获取特定应用程序所需的内存,删除其他所有内存。

Thorntail应用程序需要一个用于RESTful Web Services(JAX-RS; https://oreil.ly/eYjXF )的Java API,以及一个简单的资源端点,以便我们可以对其进行请求。Thorntail示例需要一个JAX-RS应用程序和一个JAX-RS资源,该资源带有一个返回HTTP GET请求问候语的方法。Thorntail示例的所有源代码都可以在 /chapter-2/Thorntail-hello 目录中找到。

这些类没有什么特别之处。它们只是提供一个带有JAX-RS的HTTP端点来进行请求的最低要求。让我们构建一个Thorntail应用程序,然后启动它,如示例2-1所示。

示例2-1:构建并运行Thorntail Hello World应用程序

应用程序启动后,使用 curl 或在浏览器上点击 http://localhost:8080/hello 端点。在你提出了一些请求之后,或者如果你愿意,可以提出更多请求,现在是时候看看流程使用的RSS内存了。在测量RSS之前访问端点很重要,因为应用程序在启动期间可能没有加载所有类,这意味着我们可能会看到一个误导性的数字。

为了能够找出所使用的内存,我们需要Thorntail应用程序正在运行的进程ID。在基于Linux的系统上,包括Mac在内,我们可以使用 ps - e | grep thorntail 列出所有活动进程,并将结果限制为名称中包含 thorntail 的进程。有了进程ID,就可以知道该进程使用了多少RSS(如示例2-2所示)。

示例2-2:测量Thorntail应用程序的RSS使用情况

ps 检索RSS和命令, awk 将RSS值转换为兆字节。

你将看到与前面的终端输出类似的内容,显示进程ID、转换为兆字节( M )的RSS和命令。有关如何查找流程RSS的完整详细信息,请访问Quarkus网站( https://oreil.ly/eYjXF )。

我们可以看到,一个“Hello World”风格的应用程序使用了441MB的内存,其中一个端点返回一个字符串。对于一个仅返回固定字符串的单个JAX-RS端点来说,这占用了大量内存!

我们应该注意,在OpenJDK 11上运行这些测试时,没有对JVM捕获的内存量进行任何配置,也没有对JVM进行任何其他调整。我们可以限制JVM能够获取的内容,并查看其对整个RSS的影响(见示例2-3)。

示例2-3:启动Thorntail应用程序来配置堆大小

现在我们得到示例2-4中的输出。

示例2-4:测量RSS的使用情况

这将内存使用量降至265MB!通过将JVM占用的堆数量限制在48MB,我们节省了近200MB的RSS。就吞吐量而言,48MB可能不是最佳选择,但这需要通过你自己的应用程序进行验证,以便在减少内存消耗和提高吞吐量之间找到平衡。

我们已经展示了RSS的使用情况,现在需要计算首次请求时间。继续之前,请确保Thorntail应用程序所有以前的实例都已停止。让我们查看首次请求时间,如示例2-5中所示。

示例2-5:测量Thorntail应用程序的首次请求时间

我们看到控制台中有大量的 404 消息飞过,直到应用程序返回 200 响应,然后我们看到所花费的时间。在我们的例子中,是6810ms!在微服务和函数的世界里,这并不是很快。你可以运行几次,看看时间变化是否很大。由于启动时间为7s,扩展微服务无法足够快地满足流量峰值,因此会导致用户延迟,甚至可能出现错误。从无服务器的角度来看,我们的情况甚至更糟,因为我们希望在7s之前启动、运行和停止无服务器函数。

time.js 捕捉到的首次请求时间可能会比实际长一点,因为在子进程生成时,在JVM启动之前,只需要很短的时间。我们不太担心这么小的数量,因为这个影响适用于我们以相同方式测试的每个运行时。

所以,我们已经了解了传统应用程序的RSS消耗,以及到达首次请求所需的时间。现在是时候看看Quarkus的情况了。

2.1.2 Quarkus示例

我们将创建一个相同的Hello World端点,尽管这个程序不会说“Hello from Thorntail!”使用Quarkus,我们不需要JAX-RS应用程序类,除了该消息 外,只需要与Thorntail版本内容相同的JAX-RS资源。Quarkus示例的源代码可以在 /chapter-2/Quarkus-hello 目录中找到。

在2.3节中,我们将介绍如何创建Quarkus应用程序。现在构建并启动Quarkus Hello World应用程序,如示例2-6所示。

示例2-6:构建并启动Quarkus Hello World应用程序

与Thorntail一样,我们并没有优化JVM来查看原始RSS的使用情况。单击几次 http://localhost:8080/hello ,如同我们刚才用Thorntail的时候。希望你看到的信息是“Hello from Quarkus!”否则,你仍在运行Thorntail应用程序。

找到Quarkus应用程序的进程ID并查看RSS,见示例2-7。

示例2-7:测量Quarkus Hello World应用程序的RSS使用情况

这里我们看到Quarkus使用133MB的RSS,比使用Thorntail的441MB少308MB!对于本质上相同的应用程序来说,这是一个惊人的改进。

如果我们将最大堆内存限制为48MB,就像对Thorntail所做的那样,能得到多大的改进?重新运行示例2-8。应用程序启动后,不要忘记使用端点。

示例2-8:限制堆内存使用并测量RSS使用情况

这使Quarkus降到了114MB,但让我们看看能把Quarkus配置到多小的堆内存!参考示例2-9。同样,不要忘记在启动后使用端点。

示例2-9:进一步限制Quarkus应用程序的堆内存使用,并测量RSS使用情况

最大堆为24MB,我们的RSS容量降到了98MB!更重要的是,该应用程序仍然有效!在Quarkus无法启动之前,查看你可以将最大堆内存降低到多小。虽然对于常规应用程序,甚至是微服务,你不会将最大堆内存设置得这么小,但能够将其设置得这么小对于无服务器环境至关重要。

将堆内存大小设置为非常小的值可能会影响应用程序的性能,尤其是当应用程序执行大量分配时。不要追求尽可能小的值,而应根据预期的性能和部署密度增益来验证该值。请注意,Quarkus架构试图避免受到这样的惩罚。但是,我们强烈建议你查看你的应用程序。

启动速度的时间参见示例2-10。

示例2-10:测量首次请求Quarkus应用程序的时间

如果你的硬件与我们的类似,则应该会看到大约1001ms的首次请求时间!这比传统应用程序快近7倍!

这一切都很棒,但有什么意义呢?回顾我们之前关于容器中Java的讨论,让我们看看在容器中运行时的影响。假设我们有一个可用的且内存为2GB的节点,每个应用程序可以容纳多少个容器?看看图2-1。

图2-1:容器中的Java内存——Quarkus允许增加部署密度

容器密度是Kubernetes云部署的一个关键特征。给定一个特定的节点大小,比如2GB的RAM,在单个节点中运行的容器越多,可以提供的容器密度就越大。提高容器密度可以更好地利用可用资源。从图2-1中的示例来看,4个实例或14个实例能否实现更高的吞吐量?如果这14个容器中的每一个都比传统容器支持更少的吞吐量或每秒请求数,那么这并不重要。一个Docker吞吐量的小幅下降可以通过支持14个Docker而不是4个Docker来抵消。

容器密度是确定所需实例数的一个重要指标。开发人员需要确定他们的预期或期望吞吐量。也许更少的容器和更大的内存要求可以满足今天的需求,但请记住,情况会发生变化,你可能很容易需要4个以上的容器来满足你的用户!

现在,你了解了JVM上传统应用程序的RSS内存量和首次请求时间,以及Quarkus如何显著减少应用程序的这两方面。Quarkus希望用一种新方法解决改进容器中Java的挑战。这种方法提高了Java在容器中的启动速度和内存消耗。

下一节将详细解释Quarkus如何实现这一点,更重要的是,这种方法与传统框架方法之间的区别。

2.2 Quarkus方式

我们确信你想知道Quarkus如何比传统框架启动更快、消耗更少内存的所有复杂细节,对吗?我们先解释传统框架,再讲述Quarkus方式。

图2-2描述了传统框架的一些众所周知的特征:

· 代码中的注解定义了预期的行为,多年来我们使用过很多这样的例子。典型的例子包括 @Entity @Autowired @Inject 等。

· 各种配置文件。从定义类应该如何连接到一起,到配置持久数据源,以及这两者之间的一切,这些配置文件都可以完成。

· 仅在启动期间用于创建运行时元数据的类,以及用于应用程序运行的类。

· 利用反射确定要调用的方法,将值设置到对象中,并仅按名称动态加载类。

图2-2:Quarkus方式

我们当然不是说Quarkus没有注解、配置文件或传统框架的任何其他功能,而是说Quarkus以一种非常不同的方式来处理这些功能。

为什么传统框架的这些特征会被认为是“不好的”?这是一个很好的问题,回答这个问题需要对这些框架如何处理前面的特征有所了解。当需要解析任何类型的配置或发现任何注解时,都需要用框架类来执行这项工作。根据过程的复杂程度,执行任务可能需要几十到几百个类。此外,每个类通常都会在自己内部保持状态(表示启动时的中间状态),或处理完一切后的最终理想状态。

这没什么特别的。框架已经以这种方式工作了多年,甚至几十年。然而,你可能没有意识到的是,即使JVM进程运行了6个月而没有重启,用于执行这些启动任务的所有类仍然存在!虽然这些类占用的任何内存最终都应该被垃圾收集,但前提是这些类在工作完成时正确释放了对内存的控制,启动类的类元数据仍然存在于JVM中,即使是在最新的Java版本上。这些启动类可能看起来不太多,但几百个不再需要的类可能会影响JVM所需的内存量。

这个问题影响到今天所有的JVM,没有框架进行特殊处理。只有当类的所有对象都可以被垃圾收集,对该类的所有引用都被删除,而且最重要的是,同一个类加载器中的所有其他类也不再被引用时,JVM才能对启动期间使用的所有类进行垃圾收集,而且永远不会再次使用。为了方便启动类的垃圾收集,框架需要为启动类使用一个类加载器,为运行时类使用另一个类加载器。在使用线程池(尤其是 ForkJoinPool )时,以及在启动期间设置线程局部变量时,很难为启动类启用垃圾收集。

如果类对象永远不再被使用,我们为什么要在JVM内存中保留它们?理想情况下,我们不应该这样做,因为这是浪费。这就是Quarkus闪耀的地方。Quarkus扩展的设计和构建旨在将传统框架启动处理的各个部分分离为更小的工作块。这样做使得Maven或Gradle的构建过程能够利用这些较小的工作块,并在构建过程中执行这些工作块,而不是等到运行时开始。在构建期间利用启动类意味着这些类不需要在运行时包含在JVM中!这为我们节省了内存和启动时间。

Quarkus在构建时如何帮助实现这一点,以及运行时需要的输出在哪里?Quarkus扩展使用字节码记录器(bytecode recorder)来完成一切工作,从为运行时的类设置静态值,到创建新的类来保存运行时所需的元数据。这是什么意思?前面我们讨论了框架在启动期间做了大量工作,Quarkus能够在构建期间创建该工作的输出,并编写字节码,与传统框架在启动时的结果相同。在运行时,JVM没有执行启动工作,而是将Quarkus扩展编写的类加载到内存中,就好像所有启动工作都刚刚发生,而不需要内存和类来完成这些工作。

查看传统框架在启动时执行的一些步骤,我们可以在图2-3中看到Quarkus如何以不同的方式处理它们。

图2-3:传统框架与Quarkus中的框架启动阶段

虽然Quarkus在构建时读取配置,但某些属性(如位置和认证凭据)仍在运行时配置和读取。然而,在构建时可以决定的一切以应用程序为中心的事情都是在构建过程中处理的。到目前为止,我们一直在使用构建时来描述Quarkus何时完成这些通常与启动相关的任务,但还有另一个术语:提前(AOT)编译。你已经看到,Quarkus与传统框架的不同之处在于其优化应用程序代码和依赖项的方式。是的,这种方法减少了通常在运行时处理的可变性。

然而,部署在云平台或容器中的现代工作负载不需要这种可变性,因为几乎所有东西都是在构建时就已知的。我们希望你现在能更清楚地了解Quarkus通过这种创新方法提供了什么,以及为什么Quarkus再次为云中的Java开发带来了兴奋。

2.3 创建第一个Quarkus应用程序

有很多方法可以创建Quarkus应用程序:

· 手动创建项目 pom.xml build.gradle 文件,添加Quarkus依赖项,设置和配置插件,以及定义源文件夹。

· 使用Maven和Quarkus插件构建项目框架。

· 访问 https://code.quarkus.io 并选择所需的依赖项。这是最简单、最快捷的入门方式,也是我们将使用的方法。

是时候开始创建一个项目了!前往 https://code.quarkus.io ,你将看到图2-4所示的页面。我们圈出了一些关键部分来详细解释这些内容的含义。

图2-4:Quarkus项目选择

页面顶部是生成项目的Quarkus版本。就在它下方的左,你可以自定义项目的组名和工件名。如果需要,也可以在以后进行更改,不必拘泥于使用 org.acme

在右边,用户可以决定是否要将初学者代码添加到项目中。默认值是Yes,因此如果选择任何带有CODE标记的扩展,例如RESTEasy JAX-RS,那么将为该项目生成该扩展的启动代码。页面顶部下方是所有可用Quarkus扩展的列表。有很多扩展可用,图2-4只显示了一部分。使用每个复选框选择要包含在项目中的特定扩展。

最后,如果你不想滚动浏览所有扩展名,请在所有扩展名上方的搜索框中输入词条。输入时,下面的扩展名列表将进行筛选,只显示与搜索条件匹配的扩展名。选择扩展时,这些被选中的扩展会出现在“Generate your application”旁边的“Selected Extensions”区域下。

图2-5显示了我们即将生成应用程序时的屏幕外观。

图2-5:Quarkus项目生成

你可以看到,我们选择了不生成任何启动程序代码,并且选择了RESTEasy JAX-RS扩展。我们目前仍坚持使用普通的JAXR,第8章将探讨一种响应式更强的JAX-RS。

当我们悬停在“Generate your application”上时,可以决定将该项目作为ZIP文件下载或发布到GitHub存储库。现在,我们将以ZIP文件的形式下载。该文件将自动下载,其名称与工件名称匹配。下载完成后,将ZIP文件解压缩到一个目录中。

完成后,我们打开一个终端窗口,并切换到提取生成项目的目录。让我们开始使用实时重新加载(见示例2-11),体验真正的开发者乐趣!

示例2-11:在开发模式下运行应用程序

前面的命令在实时重新加载中启动Quarkus,使我们能够快速迭代代码并立即看到结果。成功启动将产生终端输出,如示例2-12所示。

示例2-12:输出Quarkus应用程序

在浏览器中打开 http://localhost:8080 ,你将看到 Resource not found

事实上,没出什么问题。目光锐利的读者可能已经注意到,启动日志只将 cdi 列为已安装的功能。那RESTEasy呢?我们在创建项目时为其选择了扩展。看看 pom.xml 内部,你将看到依赖项(见示例2-13)。

示例2-13:生成项目的Quarkus扩展依赖项( chapter-2/code-with-quarkus/pom.xml

RESTEasy绝对是一个依赖项,那么到底发生了什么?在构建过程中,Quarkus发现实际上没有任何代码在使用RESTEasy,因此卸载了该功能并将其删除,以节省内存。现在我们来解决这个问题。

Quarkus仍在运行时,在 /src/main/java 中创建 org.acme 包。现在在包和示例2-14的上下文中创建一个名为 MyResource 的类。

示例2-14:JAX-RS MyResource chapter-2/code-with-quarkus/src/main/java/org/acme/MyResource.Java

你可能想知道为什么在前面的代码片段中没有任何Quarkus特定的 import 行。Quarkus利用不断增长的50多个同类最佳库,提供了一个内聚的全栈框架。在前面的示例中,我们使用JAX-RS(这是一种简单但高效且灵活的方法)来构建HTTP和REST API。

刷新 http://localhost:8080 ,我们现在在浏览器中看到 Hi 。发生了什么事?查看终端窗口,见示例2-15。

示例2-15:代码更改后自动重启应用程序

我们可以看到Quarkus注意到了对 MyResource.Java 的修改,停下,然后重新启动。看看已安装的功能,我们看到应用程序现在已包括 resteasy 。更妙的是,服务器在300多毫秒内就停止并重新启动了。

我们为什么不多探索一下实时加载呢,更好地感受真正的开发者乐趣吧! mvn quarkus:dev 仍在运行时,在浏览器中打开 http://localhost:8080/welcome ,如图2-6所示。

图2-6:找不到资源

不要太害怕,我们确实期待它,因为我们还没有任何东西来响应 /welcome 端点。然而,Quarkus为我们提供了一些内部链接,帮助我们根据对应用程序的了解来诊断问题。我们将看到有效端点的列表。在本例中,REST资源只有一个HTTP,叫GET/。

在“Additional endpoints”下有一些端点可以在开发应用程序时提供帮助。在本例中,我们有与ArC [ ArC是Quarkus基于上下文和依赖项注入(CDI)的Bean容器]相关的端点,以及到开发者控制台的链接。单击开发者控制台链接将进入其主页,如图2-7所示。

图2-7:Quarkus开发者控制台

现在没有太多,但需要记住,我们添加的唯一扩展是RESTEasy。随着我们用更多扩展来增强应用程序,开发者控制台将提供更多选项和功能。我们偏离了正轨,所以让我们回到解决页面加载失败的问题上来!当浏览器中的 /welcome 页面打开时,返回源代码,创建一个名为 WelcomeResource 的新类(见示例2-16)。

示例2-16:JAX-RS WelcomeResource chapter-2/code-with-quarkus/src/main/java/org/acme/WelcomeResource.java

编写完类后,返回浏览器并单击“刷新”。

触发HTTP请求会导致Quarkus检查自上次请求以来是否有任何文件被修改,因为我们正在使用实时重新加载运行。Quarkus注意到 WelcomeResource 的存在,对其进行编译,然后重新启动服务器。如果你和我们一样,那么可能没有意识到幕后发生的一切,因为浏览器毫不延迟地给出了预期的响应。

这有关于首次使用 https://code.quarkus.io 网页来创建Quarkus项目,并体验使用Quarkus实时加载带来的轻松开发的大量信息。开发过程还存在缺点,我们将继续探索实时加载可以做的每一件事。

2.4 10分钟讲明白Kubernetes下的Quarkus

上一节在更改代码和查看应用程序动态更新方面,我们获得了很多乐趣。

虽然这对于开发代码来说很好,但我们可以在生产中使用实时编码吗?也许你可以,但我们真的认为你不想这么做!

为了部署到生产环境中,我们希望使用不可变的容器,这需要容器编排,这在大多数情况下意味着Kubernetes。3.2节将详细介绍云原生和Kubernetes原生应用程序。

为什么Quarkus对云平台尤其是Kubernetes来说是伟大的?Quarkus应用程序旨在在容器中高效运行,并具有内置的健康检查和监控功能。Quarkus还提供了良好的用户体验,包括用一个命令部署Kubernetes集群的能力,而无须编写Kubernetes资源描述符。

Kubernetes引入了自己的特定术语,这可能会让人感到困惑。本节介绍其主要概念。

获取上一节中的Quarkus应用程序并将其部署到Kubernetes的步骤是什么?让我们扩展上一节中生成的应用程序,第一件事是将Kubernetes的扩展添加到我们的应用程序中,如示例2-17所示。

示例2-17:Kubernetes扩展依赖项( chapter-2/code-with-quarkus/pom.xml

通过这种新的依赖项,构建可以生成将应用程序部署到Kubernetes所需的资源文件,并使我们能够部署应用程序。让我们看看这是如何工作的!

在了解其工作原理之前,我们需要选择首选的容器化机制。使用Quarkus,我们可以在容器平台Docker、Jib( https://oreil.ly/Ybxcs )和Source-to-Image(S2I)之间进行选择。我们将选择Jib,因为所有依赖项都缓存在与应用程序分离的一个层中,从而使后续容器构建得更快。让我们添加Jib容器依赖项,如示例2-18所示。

示例2-18:Jib容器扩展依赖项( chapter-2/code-with-quarkus/pom.xml

首先,我们需要一个Kubernetes集群!最简单的方法是使用minikube,但也可以使用Docker Desktop或MicroK8s( https://microk8s.io/ )。在本书中,我们将使用minikube,因为这是最简单的解决方案之一。minikube不是一个完整的Kubernetes集群,但为我们提供了足够的功能。

按照minikube文档( https://oreilly/Vn7Jf )中的说明下载并安装minikube。安装minikube后,启动minikube,如示例2-19所示。

示例2-19:启动minikube

除非我们设置了特定的配置选项,否则将使用minikube的默认配置。现在,默认设置还不错,即一台虚拟机有两个CPU和4GB RAM。如果这是minikube的第一次运行,minikube会下载必要的镜像,这会有短暂的延迟。

Quarkus为minikube提供了额外的扩展,专门为minikube定制Kubernetes资源。这种方法的一大优势是不需要Kubernetes入口(Kubernetes Ingress )来访问Kubernetes内部的服务。相反,我们可以通过节点端口( NodePort )服务访问Kubernetes内部的服务。这使我们能够在运行命令 minikube services list 时看到服务的本地主机可访问URL(localhost-accessible URL)。要激活本地主机可访问URL,我们需要另一个依赖项,如示例2-20所示。

示例2-20:minikube扩展依赖项( chapter-2/code-with-quarkus/pom.xml

在部署我们的应用程序之前,先来了解一些Kubernetes使用相关的概念。可以使用 kubectl 命令与Kubernetes集群交互,参见示例2-21。

示例2-21:检索节点

此命令输出Kubernetes管理的节点。你会看到,这里只有一个节点,名为 master 。这就是你的计算机或虚拟机,取决于你的操作系统。

与Docker等其他系统不同,Kubernetes不直接运行容器。相反,Kubernetes将一个或多个容器包装成一个更高层次的结构,称为pod。pod被用作复制单元。如果应用程序收到的请求太多,而单个pod实例无法承载负载,则可以要求Kubernetes实例化新副本。即使在负载不重的情况下,也最好有一个pod的多个副本,以实现负载均衡和容错。你可以使用 kubectl get pods (见示例2-22)获得pod列表。

示例2-22:使用 kubectl 命令列出正在运行的pod

没什么大不了的,我们的集群是空的。

在2.1节中,我们讨论了很多关于减少容器中用Java编写服务内存量的问题。

为了在minikube中确定这一点,我们需要在部署服务之前安装一个附加组件(见示例2-23)。

示例2-23:将metric服务器添加到minikube集群中

要创建pod,我们需要部署。部署有两个主要目的:

· 指明pod中需要运行的容器。

· 指明一次应该运行的pod实例数。

通常,要创建部署,需要以下内容:

· Kubernetes集群可访问的容器镜像。

· 描述部署的YAML(Yet Another Markup Language,描述Kubernetes资源最常用的格式)文档。

Quarkus提供了避免创建镜像和手动编写部署文件的工具,例如前面提到的Kubernetes、minikube和Jib容器扩展。

一切准备就绪,现在是我们构建应用程序并通过minikube将其部署到Kubernetes的时候了!打开一个终端窗口并切换到项目目录。因为我们不想运行自己的Docker守护程序来构建容器,所以可以运行命令 eval $(minikube -p minikube docker-env )将Docker守护程序从minikube公开到本地终端环境。

eval $ minikube -p minikube docker-env )必须在我们用来访问minikube的每个终端窗口中运行,否则,我们执行的任何Docker命令都将使用本地Docker守护进程,这是我们不想要的。

接下来,我们构建并部署容器,如示例2-24所示。

示例2-24:将Quarkus应用程序部署到Kubernetes

❶打包应用程序,创建容器镜像,创建部署描述符文件,并将其部署到集群。

执行 kubectl get pods 命令进行验证,如示例2-25所示。

示例2-25:用 kubectl 列出运行的pod

是的,我们的应用程序正在运行!

Quarkus为我们创建部署,如示例2-26所示。

示例2-26:列出已安装的部署

你可以在 target/kubernetes/minikube.yml 中检查创建的部署,如示例2-27所示。

示例2-27:生成的部署

❶副本的数量。

❷容器镜像名称。

如你所见,部署YAML文件指明副本的数量和pod中运行的一组容器(这里是一个)。

如果仔细查看生成的描述符,你将看到 Service

❶通过随机本地端口号,我们可以访问服务。

该服务是一个通信信道,授权给一组pod(使用标签选择)。在我们的示例中,服务名为 code-with-quarkus 。其他应用程序可以使用此名称来发现我们公开的功能。此服务将端口8080委托给具有匹配标签( app.kubernetes.io/name app.kubernetes.io/version )的pod。好消息是我们的pod配置了这些标签。因此,在端口8080上调用 code-with-quarlcus 将代表我们容器的端口8080。

Quarkus生成多个描述符。 minikube.yml 描述符是为minikube定制的,而 kubernetes.yml 描述符更通用,主要区别在于创建的服务类型。

那么,让我们调用服务吧!我们需要让minikube提供服务URL,如示例2-28所示。

示例2-28:检索服务URL

打开浏览器并使用服务的URL进行访问,如果愿意,也可以使用 curl 。如果部署成功,我们将 Hi 视为根路径上的响应。添加 /welcome 将看到 Welcome to Quarkus !我们在Kubernetes部署了Quarkus服务!

我们验证了Quarkus服务已按预期部署和工作,但内存情况如何?让我们看看示例2-29。

示例2-29:使用 kubectl top 测量资源使用情况

只有80MB!与容器中的传统框架相比,这是一个很大的改进。

你刚刚了解了如何获取Quarkus应用程序以及如何将其部署到Kubernetes,在本例中,部署到minikube。在定义部署所需的Kubernetes资源时肯定存在潜在的缺陷,但这就是为什么我们使用Quarkus的Kubernetes扩展来处理这些问题。我们不希望手动编写YAML或JSON代码,无意中因为缩进而出错,然后看着部署失败!

2.5 谈谈原生程序

原生意味着什么?我们指的是为环境构建原生可执行文件的能力。我们每天在机器上使用的许多应用程序都是原生可执行文件,这意味着应用程序的代码被编译为特定操作系统(在我们的例子中是macOS)的低级指令。

开发Java应用程序离不开JVM。然而,最近GraalVM( https://www.graalvm.org )项目发布的版本使得从Java代码构建原生可执行文件成为可能。在本节中,我们将解释如何利用Quarkus的GraalVM项目为你的Java代码生成原生可执行文件!

在2.2节中,我们讨论了Quarkus如何利用AOT编译在构建时(而不是在应用程序启动时)执行操作。

Quarkus扩展通过将所有工作分为三个独立的阶段来实现这一点。

增强

本阶段构建步骤处理描述符和注解,并通过生成包含任何所需元数据的字节码来扩充应用程序类。这个阶段总是在JVM的构建过程中执行。

静态初始化

本阶段运行任何旨在以字节码捕获其结果输出的步骤。这些步骤的功能受到限制,因为本阶段不应该打开监听端口或启动线程。

运行时初始化

这些步骤在启动期间作为应用程序主方法的一部分运行。在这个阶段中任务应该保持在最低限度,以充分利用AOT。

在JVM上执行时,静态初始化和运行时初始化都发生在启动时。然而,对于原生可执行文件,我们有一个额外的好处。通过将初始化分为两个阶段,我们可以在原生可执行文件构建过程中执行静态初始化。这允许将静态初始化阶段的输出直接序列化到原生可执行文件中,允许在原生可执行文件中删除此阶段中使用的任何类,因为这些类不再需要了。这有助于缩短原生可执行文件的启动时间,降低内存需求。

作为GraalVM原生可执行文件构建过程的一部分,所有执行路径都会被评估。任何被认为不在执行路径上的类、方法或字段都将从生成的原生可执行文件中删除。这就是为什么在没有特殊标志的情况下,反射、动态类加载和JVM使用的其他功能是不允许的,因为其目标是不在原生可执行文件中保留每一段代码。如果我们试图为之前的Thorntail示例构建一个原生可执行文件,则需要设置标志,以允许反射、动态类加载以及其他可能的操作。Thorntail的设计不适合原生可执行构建,而Quarkus从一开始就考虑了代码缩减的目标。

让我们看看实际构建原生可执行文件需要什么 。使用 https://code.quarkus.io 创建项目意味着Maven配置文件已经为我们把原生程序构造所需要的配置信息添加到项目的 pom.xml 中。示例2-30显示了其内容。

示例2-30:原生镜像生成Maven配置文件( chapter-2/code-with-quarkus/pom.xml

虽然我们现在有能力为Quarkus构建原生可执行文件,但如果没有安装GraalVM,就走不了多远!有关安装GraalVM以构建原生可执行文件的所有详细信息,请参阅“Building a Native Executable”指南( https://oreil.ly/HFtWl )。

安装GraalVM后,让我们构建一个原生可执行文件,参见示例2-31。

示例2-31:将Quarkus应用程序编译为原生可执行文件

不幸的是,构建原生可执行文件的时间确实比通常的JVM构建要长。因此,我们建议不要定期构建原生可执行文件,并建议将这些构建作为CI管道的一部分。

随着应用程序中类的数量增加,原生可执行文件的构建需要更长的时间才能完成。这段较长的时间是需要评估的执行路径数量较多造成的。

通过构建原生可执行文件,我们可以使用 /target/code-with-quarkus-1.0.0-SNAPSHOT-runner 运行它。享受原生可执行文件的启动速度,确保我们创建的两个端点仍然有效。

我们现在已经为本地环境构建了一个原生可执行文件,但除非我们使用Linux操作系统,否则原生可执行文件在容器中无法工作!由于原生可执行文件是特定于操作系统的,因此我们需要专门构建一个用于Linux容器的原生可执行文件。

要为容器构建原生可执行文件,我们需要利用Docker( https://docker.com )。安装Docker后,确保Docker已启动。由于当前终端已切换到使用minikube内部的Docker守护程序,因此我们需要打开一个新终端,以便使用本地Docker进行构建。导航到项目的目录并运行示例2-32。

示例2-32:将Quarkus应用程序编译为Linux 64位原生可执行文件

我们所做的是利用本地Docker环境为Linux操作系统构建原生可执行文件。如果我们试图运行原生可执行文件,而本地操作系统不是Linux,则会看到一个错误,如示例2-33所示。

示例2-33:启动未针对主机操作系统编译的应用程序时出现格式错误

我们现在需要回到上一个终端,因为我们想与minikube中的Docker守护进程进行交互。让我们在minikube内部运行Docker构建,如示例2-34所示。

示例2-34:构建一个容器,运行编译为原生可执行文件的Quarkus应用程序

别忘了用本地Docker用户名替换< your_docker_username >。

在minikube中有了一个可用的容器,所以让我们创建应用程序部署,参见示例2-35。

示例2-35:将Quarkus应用程序部署到minikube

我们使用了早期版本中特定于minikube的Kubernetes YAML文件来创建部署。这个版本创建了我们从本地环境访问服务所需的NodePort服务,但也将容器的 imagePullPolicy 修改为 IfNotPresent 而不是 Always 。最后一个更改阻止minikube尝试从Docker Hub检查更新的容器镜像,这很好,因为minikube在那里找不到任何一个容器镜像!

部署完成后,从 minikube service list 中获取URL并再次测试端点。一切都应该很好,我们得到的信息和以前一样。

现在来看有趣的部分!早些时候,我们将指标服务器安装到minikube中以跟踪内存利用率,现在是时候看看原生可执行文件是什么样子了。虽然我们发出了请求,并且部署程序已经存在,但在指标可用之前可能需要几分钟。继续尝试,直到这些指标出现,你应该看到类似于示例2-36的内容。

示例2-36:测量Kubernetes的资源使用情况

太棒了!只使用了7MB内存!

这就是Quarkus和原生可执行文件相结合真正发挥作用的地方。我们还可以检查pod的日志,看看Docker启动的速度有多快,我们预计为10~20ms。

我们试图为Thorntail构建一个原生镜像,以提供一个鲜明的对比。然而,我们在构建可用的原生镜像时遇到了问题,并被一个 UnsatisfiedLinkError https://oreil.ly/hxcqo )所阻止。

2.6 本章小结

Quarkus是Kubernetes原生Java框架,专注于使用AOT最小化内存需求,并在需要原生可执行文件时进一步缩减内存。通过Kubernetes和容器扩展,Quarkus为我们解决了手写YAML部署文件的麻烦!

在本章中,你了解了以下内容:

· 理解容器中Java的问题。

· 了解Quarkus在使用AOT将运行时启动任务转移到构建时方面与传统框架的区别。

· 使用 https://code.quarkus.io 创建Quarkus项目。

· 使用Kubernetes和minikube扩展生成所需的部署配置。

· 使用GraalVM为Quarkus构建原生可执行文件。

· 使用Kubernetes扩展将Quarkus应用程序部署到容器环境。

在接下来的章节中,我们将讲述分布式系统、响应式系统、响应式编程,以及它们之间的关系。 dVCqUi2e1k8J6enADBer2hKpUb0iFkX2p3Rik0d9Sm1FYvRoA5M44iV0Av579zu0

点击中间区域
呼出菜单
上一章
目录
下一章
×