Quarkus 运行时性能
这是本系列博客的第一部分,深入探讨 Quarkus 的性能。一个框架的性能涉及许多方面,从启动时间到内存使用、编译时间和运行时性能。
“性能” 的定义是语境相关的,本系列博客旨在研究 Quarkus 在不同场景下的性能。
本文将重点关注使用 Quarkus 构建的应用程序的运行时性能。
简而言之 - 摘要
创建了一个从 PostgreSQL 数据库通过事务检索数据的 REST 应用程序,用于比较 Quarkus 和 Thorntail 的吞吐量和响应延迟。该应用程序在不同程度的负载下运行,以展示 Quarkus 的扩展能力。
与 Thorntail 单个进程相比,Quarkus 在原生模式下运行,支持 40 个并发连接,最大吞吐量提高了高达 38.4%,同时最大响应时间延迟降低了高达 42.7%。
与 Thorntail 单个进程相比,Quarkus 在 JVM 模式下运行,支持 40 个并发连接,最大吞吐量提高了高达 136%,同时最大响应时间延迟降低了高达 66.3%。
与原生模式相比,Quarkus 在 JVM 上运行时提供了更高的吞吐量和更快的响应时间,但使用了高达 277% 的内存 (RSS)。
对于在容器中运行、内存限制为 2048MB 的应用程序,通过运行多个 Quarkus 应用程序的 JVM 模式实例,理论上可以将应用程序吞吐量比 Thorntail 应用程序提高高达177.3%;通过运行多个 Quarkus 应用程序的原生模式实例,可以提高545%。
原生镜像不仅仅用于短期运行的进程。测试运行了长达 3 小时,没有进程重启,原生镜像处理了超过3300 万个请求!
没有一种万能的解决方案!Quarkus 允许您选择在 JVM 模式下进行纵向扩展,如果您需要单个具有更大堆的实例;或者在原生模式下进行横向扩展,如果您需要更多、更轻量级的实例。
关键问题
“优化启动时间和镜像大小固然好,但响应时间仍然很重要”.
让我们先来解决这个关键问题,Quarkus 到目前为止一直专注于启动时间和内存占用。
“那是因为原生性能很糟糕,对吧?错了!”
通过运行一个示例应用程序,该应用程序通过事务性 REST HTTP 请求从 PostgreSQL 数据库检索数据,我将讨论:
- 
原生模式和 JVM 模式下的单进程吞吐量和响应时间,与 Thorntail 对比 
- 
长进程的原生镜像 
应用程序和测试方法的详细信息可以在本文末尾的 测试应用程序 部分找到。
Quarkus 提供什么?
Quarkus 提供 2 种运行模式供您选择。您可以运行为原生二进制文件或在 JVM 上运行字节码。
这意味着您可以选择满足您的应用程序您的需求的运行时。如果原生镜像不能满足您的需求,没问题,选择您喜欢的 JVM。
但不要认为在 JVM 上运行是次等公民,Quarkus 针对在 JVM 上运行以及在原生模式下运行都进行了优化。
为什么与 Thorntail 进行比较?
Thorntail 是一个更传统的云原生栈,其基础来自 WildFly 社区,我们认为与我们知道如何优化的运行时进行比较是公平的。本次性能测试的目的不是进行框架与框架的比较,而是展示 Quarkus 所做的优化不仅仅局限于启动时间和初始内存消耗。Thorntail 是一个出色的运行时,但与其他传统的云原生栈一样,在独立部署时并不关注的运行时动态行为,在现代部署场景中却成为显著开销的原因。
吞吐量 (请求/秒)
最大吞吐量,以每秒请求数 (Req/Sec) 衡量,告诉我们单个进程应用程序每秒可以服务的最大请求数。最大吞吐量越高越好。
将原生 Quarkus 应用程序与运行在 JVM 上的 Thorntail 进行比较,随着并发用户数的增加,最大吞吐量保持一致。
| Quarkus 0.18.0,在原生模式下运行单个实例,具有 40 个并发连接,与运行在 JVM 上的 Thorntail 2.4.0.Final 相比,最大吞吐量提高了 38.4%。. | 
| Quarkus 0.18.0,在 JVM 模式下运行单个实例,具有 40 个并发连接,与 Thorntail 2.4.0.Final 相比,吞吐量提高了 136%。. | 
 
| 并发连接数 | Thorntail | Quarkus - 原生 | Quarkus - JVM | 
|---|---|---|---|
| 1 | 3,273 | 3,316 | 5,138 | 
| 5 | 14,092 | 14,998 | 24,417 | 
| 10 | 25,512 | 26,328 | 44,196 | 
| 15 | 31,855 | 33,389 | 59,007 | 
| 20 | 35,006 | 36,515 | 69,146 | 
| 25 | 37,082 | 38,416 | 73,790 | 
| 30 | 33,369 | 38,849 | 76,992 | 
| 35 | 32,974 | 41,691 | 77,118 | 
| 40 | 32,391 | 44,841 | 76,488 | 
响应时间 (毫秒)
我想以“您所知道的关于延迟的一切都是错误的”这句话开始本节。[1]
响应时间衡量应用程序响应请求所需的时间。响应时间越低越好。但平均响应时间并不能全面反映应用程序的响应能力。最大响应时间比平均响应时间更能说明用户体验。
为什么这很重要?最大响应时间告诉我们最坏的情况,并且 26-93% 的页面加载将体验到 99% 的响应时间 [2]。拥有一个极低、极稳定的最大响应延迟会提高应用程序的响应能力。
在高并发用户数下;Quarkus 在 JVM 模式下的平均响应时间为 0.91 毫秒,而 Thorntail 为 1.69 毫秒。在原生模式下运行时,平均响应时间转变为 2.43 毫秒,以换取更低的内存利用率。
如果我们看最大响应时间;Thorntail 花费了 145.3 毫秒来服务至少一个请求,而 Quarkus JVM 为 65.01 毫秒,Quarkus Native 为 83.27 毫秒。
| Quarkus 在原生模式下的最大响应时间非常稳定,比 Thorntail 低高达 42.7%。 | 
| 在 JVM 上运行的更低的平均响应时间延迟是由于 JVM 中可用的 GC 实现优于 GraalVM 当前可用的 GC 实现。Quarkus 目前仍处于 Beta 发布阶段,并计划在原生模式下运行进行改进。 | 
 
 
| 并发连接数 | Thorntail (平均) | Thorntail (最大) | Quarkus - 原生 (平均) | Quarkus - 原生 (最大) | Quarkus - JVM (平均) | Quarkus - JVM (最大) | 
|---|---|---|---|---|---|---|
| 1 | 0.324 | 9.31 | 0.327 | 6.13 | 0.196 | 9.52 | 
| 5 | 0.461 | 13.12 | 0.494 | 9.86 | 0.232 | 13.85 | 
| 10 | 0.53 | 11.3 | 0.698 | 14.24 | 0.278 | 16.08 | 
| 15 | 0.842 | 145.16 | 0.91 | 14.86 | 0.334 | 18.38 | 
| 20 | 1.02 | 134.9 | 1.15 | 16.4 | 0.389 | 23.7 | 
| 25 | 1.2 | 145.3 | 1.3 | 16.86 | 0.472 | 21.25 | 
| 30 | 1.26 | 34.87 | 1.69 | 26.52 | 0.545 | 83.27 | 
| 35 | 1.35 | 30.94 | 1.84 | 65.01 | 0.78 | 32.9 | 
| 40 | 1.69 | 143.49 | 2.43 | 48.37 | 0.91 | 63.71 | 
应用程序启动时间
使用此处描述的方法测量了每个运行时的启动时间和内存使用情况:https://quarkus.net.cn/guides/performance-measure
| 指标 | Thorntail | Quarkus - 原生 | Quarkus - JVM | 
|---|---|---|---|
| 启动时间 | 8764 毫秒 | 18 毫秒 | 1629 毫秒 | 
最大内存使用量
使用 ps 测量了每个应用程序进程的内存。
$ ps -o rss -p <PID>捕获了运行期间的最大内存使用量。
| Thorntail | Quarkus - JVM | Quarkus - 原生 | 
|---|---|---|
| 651 MB | 414 MB | 122 MB | 
| 与 Thorntail 相比,原生模式下的 Quarkus 使用了仅18.7%的内存来处理多 20.9%的请求,而 JVM 模式下的 Quarkus 使用了63.6%的内存来处理多 108.0%的请求。 | 
因此,使用具有 2048MB 内存的机器,运行多个进程(不受 CPU 限制),理论上可以比 Thorntail 提高以下吞吐量:
| 运行时模式 | 内存 (MB) | 每 2048MB 的进程数 | 每个进程的最大吞吐量 (请求/秒) | 总体最大吞吐量 (请求/秒) | 与 Thorntail 相比 | 
|---|---|---|---|---|---|
| Thorntail | 651 | 3 | 37,082 | 111,246 | 100% | 
| Quarkus - JVM | 414 | 4 | 77,118 | 308,472 | 277% | 
| Quarkus - 原生 | 122 | 16 | 44,841 | 717,456 | 645% | 
| 对于在云环境中运行的应用程序,通过运行多个 Quarkus 应用程序的原生模式实例,理论上可以将相同内存下的应用程序吞吐量提高高达545%。 | 
Quarkus 原生 - 长进程
另一个担忧是 Quarkus 在原生模式下运行不适合长进程。
| 在测试期间,Quarkus 在原生模式下运行了超过 3 小时,并处理了超过51,890,000个请求! | 
这些请求导致了数百次 Full GC 循环,但进程在此期间一直保持稳定。
测试应用程序
测试应用程序是一个事务性 REST/JPA 应用程序,它调用 PostgreSQL 数据库。应用程序和数据库都运行在 Docker 容器中。
构建和运行测试应用程序
构建;
Quarkus JVM
 $ cd ./quarkus
 $ build-quarkus-jvm.sh或 Quarkus 原生
 $ cd ./quarkus
 $ build-quarkus-native.sh或 Thorntail
 $ cd ./thorntail
 $ ./build-thorntail.sh运行;
首先启动运行在 Docker 容器中的 PostgreSQL;
docker run -d --rm -p 5432:5432 --network host  \
	-e POSTGRES_DB='rest-crud' \
	-e POSTGRES_USER='restcrud'  \
	-e POSTGRES_PASSWORD='restcrud' \
	docker.io/postgres:10.5然后启动运行在 Docker 容器中的应用程序;
 $ cd ./quarkus
 $ ./run-quarkus-jvm.sh或 Quarkus 原生
 $ run-quarkus-native.sh或 Thorntail
 $ cd ./thorntail
 $ ./run-thorntail.sh运行时验证
在浏览器中导航到 http://{REMOTE_HOST}:8080/
或
$ curl -D - http://{REMOTE_HOST}:8080/fruits
HTTP/1.1 200 OK
Connection: keep-alive
Content-Type: application/json
Content-Length: 75
Date: Mon, 01 Apr 2019 07:57:17 GMT
[{"id":2,"name":"Apple"},{"id":3,"name":"Banana"},{"id":1,"name":"Cherry"}]运行时环境
被测系统
CPU: 32 x Intel® Xeon® CPU E5-2640 v3 @ 2.60GHz
操作系统: Red Hat Enterprise Linux Server release 7.6 (3.10.0-693.25.2.el7.x86_64)
内存: 262GB
以太网: Solarflare Communications SFC9020 10G 以太网控制器