编辑此页面

测量性能

本指南涵盖

  • 我们如何测量内存使用情况

  • 我们如何测量启动时间

  • Quarkus 默认会应用哪些额外的 native-image 标志

  • 工具中的协调遗漏问题

对于给定的批次,我们的所有测试都在相同的硬件上运行。不说也知道,但说了更好。

我们如何测量内存使用情况

在测量 Quarkus 应用程序的占用空间时,我们测量的是常驻内存集合大小 (RSS),而不是 JVM 堆大小,后者只是总体问题的一小部分。JVM 不仅为堆(-Xms-Xmx)分配本机内存,还为 JVM 运行您的应用程序所需的结构分配本机内存。根据 JVM 实现,为应用程序分配的总内存将包括但不限于

  • 堆空间

  • 类元数据

  • 线程栈

  • 已编译代码

  • 垃圾回收

本机内存跟踪

为了查看 JVM 使用的本机内存,您可以启用 hotspot 中的本机内存跟踪 (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]

云原生内存限制

测量整个内存对于了解云原生应用程序的影响非常重要。对于基于其完整 RSS 内存使用情况终止进程的容器环境尤其如此。

同样,不要陷入仅测量私有内存的陷阱,私有内存是进程使用的,不能与其他进程共享的内存。虽然私有内存可能在部署许多不同应用程序的环境中很有用(因此可以大量共享内存),但在 Kubernetes/OpenShift 等环境中,它非常具有误导性。

在 Docker 上正确测量内存

为了正确测量内存,请勿使用 docker stat 或任何由此派生的内容(例如 ctop)。这种方法仅测量正在使用的常驻页面的一个子集,而 Linux 内核、cgroups 和云编排提供程序将在其核算中使用完整的常驻内存集合(确定进程是否超过限制并应被终止)。

为了准确测量,应执行一组类似于在 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

平台特定内存报告

为了不产生启用 NVM 运行时的性能开销,我们使用特定于每个平台的工具来测量 JVM 应用程序的总 RSS。

Linux

linux pmapps 工具提供进程的本机内存映射报告

 $ 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

列出了已为进程分配的每个内存区域;

  • 地址:虚拟地址空间的起始地址

  • Kbytes:为区域保留的虚拟地址空间大小(千字节)

  • RSS:常驻内存集合大小(千字节)。这是实际使用的内存空间度量

  • Dirty:千字节中的脏页(共享和私有)

  • Mode:内存区域的访问模式

  • Mapping:包括应用程序区域和进程的共享对象 (.so) 映射

Total RSS (kB) 行报告进程正在使用的总本机内存。

macOS

在 macOS 上,您可以使用 ps x -o pid,rss,command -p <PID>,它列出了给定进程的 RSS,单位为 KB(1024 字节)。

$ 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 的常驻内存。

我们如何测量启动时间

某些框架使用激进的延迟初始化技术。测量到第一个请求的启动时间非常重要,以便最准确地反映框架启动所需的时间。否则,您将错过框架实际初始化所需的时间。

以下是我们在测试中测量启动时间的方式。

我们创建一个示例应用程序,该应用程序记录应用程序生命周期中某些时间点的时钟。

@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

最终时间戳和第一个时间戳之间的差异是应用程序服务第一个请求的总启动时间。

Quarkus 应用的其他标志

当 Quarkus 调用 GraalVM native-image 时,默认情况下会应用一些额外的标志。

如果您要将性能属性与其他构建进行比较,您可能想了解以下标志。

禁用回退镜像

回退本机镜像是 GraalVM 的一项功能,如果由于某种原因编译为本机代码失败,则“回退”以在普通 JVM 中运行您的应用程序。

Quarkus 通过设置 -H:FallbackThreshold=0 来禁用此功能:这将确保您获得编译失败,而不是冒着未注意到应用程序无法真正在本机模式下运行的风险。

如果您想在 Java 模式下运行,这是完全可能的:只需跳过 native-image 构建并将其作为 jar 运行。

禁用隔离

隔离是 GraalVM 的一项巧妙功能,但 Quarkus 在此阶段未使用它们。

通过 -H:-SpawnIsolates 禁用。

禁用所有 Service Loader 实现的自动注册

Quarkus 扩展可以自动选择它们需要的正确服务,而 GraalVM 的 native-image 默认包括它能够在类路径上找到的所有服务。

我们更喜欢显式列出服务,因为它会生成更好的优化二进制文件。通过设置 -H:-UseServiceLoaderFeature 禁用它。

其他 ...​

本节提供高级指导,但不能假设是全面的,因为某些标志由扩展、您构建的平台、配置详细信息、您的代码以及这些标志的组合动态控制。

一般来说,此处列出的标志最有可能影响性能指标,但在适当的情况下,人们也可以观察到其他标志的不可忽略的影响。

如果您要详细调查某些差异,请确保检查 Quarkus 正在准确调用什么:当构建插件生成本机映像时,会记录完整的命令行。

工具中的协调遗漏问题

在测量像 Quarkus 这样的框架的性能时,用户体验的延迟尤其令人感兴趣,为此有许多不同的工具。不幸的是,许多工具未能正确测量延迟,而是失败并产生协调遗漏问题。这意味着工具无法适应系统负载下提交新请求的延迟,并将这些数字汇总,从而使延迟和吞吐量数字非常具有误导性。

此视频很好地介绍了这个问题,其中 wrk2 的作者 Gil Tene 解释了这个问题,Quarkus Insights #22 中 Quarkus 性能团队的 John O'Hara 展示了它是如何出现的。

虽然该视频以及相关的论文和文章都可以追溯到 2015 年,但即使在今天,您也会发现工具未能解决协调遗漏问题

在撰写本文时,已知存在该问题的工具,不应将其用于测量延迟/吞吐量(它可能用于其他用途)

  • JMeter

  • wrk

已知不受影响的工具是

请注意,这些工具并不比您自己对它们所测量内容的理解更好,因此即使在使用 wrk2hyperfoil 时,也要确保您理解这些数字的含义。

相关内容