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%。.

Throughput as a function of concurrent users
图 1. 最大吞吐量 (请求/秒) 作为并发用户的函数

表 1. 最大吞吐量 (请求/秒)
并发连接数 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 发布阶段,并计划在原生模式下运行进行改进。

Mean Response Time as a function of concurrent users
图 2. 平均响应时间 (毫秒) 作为并发用户的函数

Mean Response Time as a function of concurrent users
图 3. 最大响应时间 (毫秒) 作为并发用户的函数

表 2. 响应时间 (毫秒)
并发连接数 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 容器中。

构建和运行测试应用程序

先决条件

  • Docker (最小 v1.13.1)

  • Maven (最小 3v.5.4)

构建;

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"}]

运行时性能指标

吞吐量和响应时间使用 wrk 命令行工具测量。https://github.com/wg/wrk

提供了一个用于运行 wrk 的 shell 脚本;

$ ./runWrk.sh

运行时环境

被测系统

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 以太网控制器

客户端系统

CPU: 24 x Intel® Xeon® CPU E5-2640 @ 2.80GHz

操作系统: Red Hat Enterprise Linux Server release 7.6 (3.10.0-229.el7.x86_64)

内存: 64GB

以太网: Solarflare Communications SFC9020 [Solarstorm]

JVM

Java HotSpot™ 64 位服务器 VM (build 25.191-b12, 混合模式)