Measuring Performance
本指南涵盖:
-
如何测量内存使用情况
-
如何测量启动时间
-
Quarkus 默认情况下将哪些其他标记应用于 native-image
-
在工具中协调遗漏问题
我们的所有测试针对给定批次在同一硬件上运行。这不用说,但说得更好。
How do we measure memory usage
在测量 Quarkus 应用程序占用空间时,我们测量 Resident Set Size (RSS),而不是 JVM 堆大小,后者只是整个问题的一小部分。JVM 不仅为堆分配本机内存(-Xms
、-Xmx
),还为 jvm 运行应用程序所需结构分配内存。根据 JVM 实现情况,为应用程序分配的总内存将包括但不限于:
-
Heap space
-
Class metadata
-
Thread stacks
-
Compiled code
-
Garbage collection
Native Memory Tracking
要查看 JVM 使用的本机内存,可以在热点中启用 Native Memory Tracking (NMT) 功能;
在命令行中启用 NMT;
-XX:NativeMemoryTracking=[off | summary | detail] 1
1 | [注意]======此功能将增加 5-10% 的性能开销====== |
然后可以使用 jcmd 导出一个正在运行你的应用程序的 Hotspot JVM 的本机内存使用情况报告;
jcmd <pid> VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB]
Cloud Native Memory Limits
为了查看云原生应用程序的影响,测量整个内存非常重要。对于容器环境而言尤其如此,它将根据其全部 RSS 内存使用情况杀死一个进程。
同样,不要陷入仅测量私有内存的陷阱,这是该进程使用但与其他进程不可共享的东西。虽然私有内存可能在部署许多不同应用程序(从而大量共享内存)的环境中很有用,但在 Kubernetes/OpenShift 等环境中会产生极大的误导。
Measuring Memory Correctly on Docker
要正确地测量内存 DO NOT use docker stat or anything derived from it (例如 ctop)。此方法仅测量正在使用驻留页面的一部分,而 Linux 内核、cgroup 和云编排提供程序将在其记账中利用完整的驻留集(确定一个进程是否已超出限制并且应该被杀死)。
要准确测量,应执行测量 Linux 上 RSS 的类似步骤集。docker top
命令允许在容器主机上针对容器实例中的进程运行一个 ps
命令。通过将此与格式化输出参数结合使用,可以返回 rss 值:
docker top <CONTAINER ID> -o pid,rss,args
例如:
$ docker top $(docker ps -q --filter ancestor=quarkus/myapp) -o pid,rss,args
PID RSS COMMAND
2531 27m ./application -Dquarkus.http.host=0.0.0.0
或者,可以直接跳转到一个特权 shell(在主机上为 root),并直接执行一个 ps
命令:
$ docker run -it --rm --privileged --pid=host justincormack/nsenter1 /bin/ps -e -o pid,rss,args | grep application
2531 27m ./application -Dquarkus.http.host=0.0.0.0
如果你碰巧在 Linux 上运行,你可以直接执行 ps
命令,因为你的 shell 与容器主机相同:
ps -e -o pid,rss,args | grep application
Platform Specific Memory Reporting
为了不产生运行启用 NVM 的性能开销,我们使用特定于每个平台的工具测量一个 JVM 应用程序的总 RSS。
$ ps -o pid,rss,command -p <pid>
PID RSS COMMAND
11229 12628 ./target/getting-started-1.0.0-SNAPSHOT-runner
$ pmap -x <pid>
13150: /data/quarkus-application -Xmx100m -Xmn70m
Address Kbytes RSS Dirty Mode Mapping
0000000000400000 55652 30592 0 r-x-- quarkus-application
0000000003c58000 4 4 4 r-x-- quarkus-application
0000000003c59000 5192 4628 748 rwx-- quarkus-application
00000000054c0000 912 156 156 rwx-- [ anon ]
...
00007fcd13400000 1024 1024 1024 rwx-- [ anon ]
...
00007fcd13952000 8 4 0 r-x-- libfreebl3.so
...
---------------- ------- ------- -------
total kB 9726508 256092 220900
列出了分配给该进程的每个内存区域;
-
地址:虚拟地址空间的起始地址
-
千字节:为该区域保留的虚拟地址空间的大小(千字节)
-
RSS:驻留集大小(千字节)。这是实际使用的内存空间的测量值
-
Dirty:脏页(共享和私有)以千字节为单位
-
模式:内存区域的访问模式
-
映射:包括应用程序区域和进程的共享对象(.so)映射
总 RSS(千字节)行报告了该进程正在使用的总本机内存。
- macOS
-
On macOS, you can use
ps x -o pid,rss,command -p <PID>
which list the RSS for a given process in KB (1024 bytes).
$ ps x -o pid,rss,command -p 57160
PID RSS COMMAND
57160 288548 /Applications/IntelliJ IDEA CE.app/Contents/jdk/Contents/Home/jre/bin/java
这意味着 IntelliJ IDEA 消耗了 281,8 MB 的驻留内存。
How do we measure startup time
一些框架使用激进的延迟初始化技术。衡量从启动时间到第一个请求非常重要,这样才能最准确地反映框架启动所需的时间。否则,你会错过框架“actually”初始化所需的时间。
以下是我们如何在测试中衡量启动时间。
我们创建一个示例应用程序,记录应用程序生命周期中某些点的 timestamp。
@Path("/")
public class GreetingEndpoint {
private static final String template = "Hello, %s!";
@GET
@Path("/greeting")
@Produces(MediaType.APPLICATION_JSON)
public Greeting greeting(@QueryParam("name") String name) {
System.out.println(new SimpleDateFormat("HH:mm:ss.SSS").format(new java.util.Date(System.currentTimeMillis())));
String suffix = name != null ? name : "World";
return new Greeting(String.format(template, suffix));
}
void onStart(@Observes StartupEvent startup) {
System.out.println(new SimpleDateFormat("HH:mm:ss.SSS").format(new Date()));
}
}
我们开始在 shell 中循环,向我们正在测试的示例应用程序的 rest 端点发送请求。
$ while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:8080/api/greeting)" != "200" ]]; do sleep .00001; done
在单独的终端中,我们启动正在测试的定时应用程序,打印该应用程序启动的时间
$ date +"%T.%3N" && ./target/quarkus-timing-runner
10:57:32.508
10:57:32.512
2019-04-05 10:57:32,512 INFO [io.quarkus] (main) Quarkus 0.11.0 started in 0.002s. Listening on: http://127.0.0.1:8080
2019-04-05 10:57:32,512 INFO [io.quarkus] (main) Installed features: [cdi, rest, rest-jackson]
10:57:32.537
最后 timestamp 和第一个 timestamp 之间的差值即为应用程序提供第一个请求的总启动时间。
Additional flags applied by Quarkus
当 Quarkus 调用 GraalVM `native-image`时,它默认会应用一些额外的标记。
如果你要比较其他构建的性能属性,你可能想要了解以下这些:
Disable fallback images
回退本地镜像是 GraalVM 的一项功能,可“回退”到在普通 JVM 中运行你的应用程序,原因在于编译成本地代码可能会失败。
Quarkus 通过设置“-H:FallbackThreshold=0
”禁用此功能:这样可以确保出现编译故障,而不是冒有无法在本地模式下真正运行应用程序的风险。
如果你只想在 Java 模式下运行,这完全有可能:只需跳过原生镜像构建,并将其作为 jar 运行即可。
Coordinated Omission Problem in Tools
在衡量像 Quarkus 这样的框架的性能时,用户体验的延迟尤其有趣,为此也有许多不同的工具。不幸的是,许多工具无法正确衡量延迟,反而会失败并造成坐标遗漏问题。这意味着在系统负载下提交新请求时,这些工具无法适应延迟,并汇总这些数字,从而使延迟和吞吐量数字非常具有误导性。
这个问题有一个很好的演练,见 this video 中 wrk2 作者 Gil Tene 对这个问题的解释,以及 Quarkus Insights #22 中 Quarkus 性能团队的 John O’Hara 展示了它如何出现。
虽然该视频和相关论文和文章都可以追溯到 2015 年,但时至今日,您仍会发现一些工具不足以解决协调的 oission 问题。
在撰写本文时,已知会产生这个问题且不应被用于测量延迟/吞吐量的工具:
-
JMeter
-
wrk
已知不受影响的工具有:
请注意,这些工具不会优于您对它们所测量内容的理解,因此,即使使用 wrk2
或 hyperfoil
也要确保您了解这些数字的含义。