パフォーマンスの測定
このガイドでは以下をカバーしています:
-
メモリ使用量の測定方法
-
起動時間の測定方法
-
how we measure throughput
-
Quarkusがデフォルトでネイティブイメージに適用する追加フラグ
-
ツールにおける協調的省略(Coordinated omission)問題
すべてのテストは、バッチに対して同じハードウェアで実行しています。言うまでもないことですが、言うべきことです。
| The spring-quarkus-perf-comparison repository contains the runnable scripts used for our reference benchmarks, used to produce most of the performance figures on this site. The sections below describe the methodology; the repository has the exact, up-to-date commands. |
メモリ使用量の測定方法
Quarkusアプリケーションのフットプリントを測定する際には、 Resident Set Size(RSS) を測定します。JVMのヒープサイズではありません。JVMヒープサイズは全体的な問題のほんの一部です。JVMは、ヒープ用のネイティブメモリー( -Xms , -Xmx )を割り当てるだけでなく、アプリケーションを実行するためにjvmが必要とする構造体も割り当てます。JVMの実装に応じて、アプリケーションに割り当てられる総メモリーは、以下のものを含みますが、これに限りません。
-
ヒープ空間
-
クラスメタデータ
-
スレッドスタック
-
コンパイルされたコード
-
ガベージコレクション
ネイティブメモリのトラッキング
JVMで使用されているネイティブメモリを表示するには、hotspotでNMT( Native Memory Tracking )機能を有効にします;
コマンドラインで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アプリケーションの影響を見るためには、メモリー全体を測定することが重要です。特に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
また、特権シェル(ホスト上の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 コマンドを直接実行することができます:
ps -e -o pid,rss,args | grep application
プラットフォーム固有のメモリレポート
In order to not incur the performance overhead of running with NMT enabled, we measure the total RSS of a JVM application using tools specific to each platform.
The performance lab captures RSS after the first successful request using pmap:
pmap -x $PID | grep total | awk '{print $4}'
This returns the total RSS in kilobytes, which is then converted to MiB.
$ 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
プロセスに割り当てられた各メモリ領域が一覧表示されます;
-
Address: 仮想アドレス空間の開始アドレス
-
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,8MBのRSSを消費していることになります。
起動時間の測定方法
Some frameworks use aggressive lazy initialization techniques. It is important to measure the time to first request (TTFR) to most accurately reflect how long a framework needs to start. Otherwise, you will miss the time the framework actually takes to initialize.
ここでは、テストにおける起動時間の測定方法を説明します。
Approach
We record a timestamp immediately before launching the application, start polling the application’s HTTP endpoint, launch, and then continue polling until it returns a 200 response. The difference is the Time to First Request (TTFR).
The time-to-1st-request.sh script in the benchmark repository implements this precisely. It records the start time using nanosecond granularity:
$ date +%s%N
On macOS, the built-in date does not support %N. Install GNU coreutils and use gdate +%s%N instead.
|
A background polling loop attempts TCP connections and sends HTTP GET requests until a 200 response is received:
function _date() {
current=$(date +%s%N)
if [ $? -ne 0 ]; then
current=$(gdate +%s%N)
fi
echo "$current"
}
ts=$(_date)
while [[ $(curl -s -o /dev/null -w ''%{http_code}'' ${TARGET_URL}) != 200 ]]
do
# Spin here and do nothing rather waiting some arbitrary unlucky timing
:
done
TTFR=$((($(_date) - ts)/1000000))
echo "${TTFR}"
The TTFR is then computed as the difference between the response timestamp and the start timestamp, in nanoseconds.
Why not just use the framework’s reported startup time?
A framework-reported startup time (e.g. "started in 0.002s") only measures internal initialization. It will not include time spent loading shared libraries, JVM bootstrap, and there may also be a further gap between the claimed ready-time and the time when the application can actually serve a request. Measuring TTFR end-to-end captures the full cost.
How do we measure throughput
Throughput is measured using Hyperfoil, a tool designed to avoid the coordinated omission problem. We run Hyperfoil via JBang, which handles downloading and launching it automatically.
The performance lab invokes Hyperfoil as follows:
$ jbang \
-Dio.hyperfoil.rootdir=${HYPERFOIL_LOGS_DIR} \
-Dio.hyperfoil.cpu.watchdog.idle.threshold=0.0 \
run@hyperfoil \
-o ${HYPERFOIL_LOGS_DIR} \
load-test-fixed-threads-read-all.hf.yml
The Hyperfoil configuration defines three phases:
-
Warmup — 2 minutes of traffic with 100 concurrent connections (configurable)
-
Cooldown — a 30-second pause
-
Load test — 30 seconds of measured traffic with 100 concurrent connections
The configuration uses these defaults, which can be overridden with -P flags:
| パラメーター | デフォルト | 説明 |
|---|---|---|
|
100 |
Number of concurrent connections |
|
2 |
Hyperfoil executor threads |
|
2m |
Duration of warmup phase |
|
30s |
Duration of measured load test |
|
/fruits |
Endpoint under test |
Throughput is calculated from the Hyperfoil results as requests completed divided by elapsed time.
JVM flags for benchmarking
The benchmark scripts apply these JVM flags when running the application under test:
-XX:ActiveProcessorCount=4 -Xms512m -Xmx512m
-XX:ActiveProcessorCount limits how many processors the JVM believes are available, ensuring consistent thread pool sizing across environments.
-Xms and -Xmx are set to the same value to eliminate heap resizing during the measurement.
In the performance lab, CPU affinity is further controlled using taskset to pin processes to specific cores, preventing interference between the application, database, load generator, and monitoring.
Quarkusで適用される追加フラグ
QuarkusがGraalVM native-image を起動すると、 デフォルトでいくつかの追加フラグが適用されます。
他のビルドと性能特性を比較する場合には、以下のようなものを把握しておくとよいです。
フォールバックイメージの無効化
フォールバックネイティブイメージは、何らかの理由でネイティブコードへのコンパイルに失敗した場合に、アプリケーションを通常のJVMで実行するために「フォールバック」するためのGraalVMの機能です。
Quarkusは、 -H:FallbackThreshold=0 を設定することでこの機能を無効にします。これにより、アプリケーションが本当にネイティブモードで実行できないことに気づかないリスクの代わりに、コンパイルに失敗するように出来ます。
その代わりに Java モードで実行したい場合は、ネイティブイメージのビルドをスキップして jar として実行することも可能です。
すべてのサービスローダー実装の自動登録を無効にする
Quarkusのエクステンションでは、必要なサービスを自動的に選択することができますが、GraalVMのネイティブイメージのデフォルトでは、クラスパス上で見つけることができるすべてのサービスが含まれています。
より最適化されたバイナリを生成するために、明示的にサービスをリストアップすることをお勧めします。 -H:-UseServiceLoaderFeature により、これを無効にすることも可能です。
その他…
このセクションは、高レベルのガイダンスとして提供されています。フラグの中には、エクステンションや構築しているプラットフォーム、設定の詳細、あなたのコード、そして場合によってはこれらの組み合わせによって動的に制御されものがあるため、このセクションは包括的なものではありません。
一般的に言えば、ここに挙げたものはパフォーマンス・メトリクスに影響を与える可能性が高いものですが、適切な状況下では、他のフラグによる影響も無視できないものになるでしょう。
いくつかの差異を詳細に調査する場合は、Quarkusが呼び出しているものを正確にチェックしてください。ビルドプラグインがネイティブイメージを生成しているときには、コマンドライン全体がログに記録されます。
ツールにおける協調的省略(Coordinated omission)問題
When measuring performance of a framework like Quarkus the latency experience by users are especially interesting and for that there are many different tools. Unfortunately, many fail to measure the latency correctly and instead fall short and create the Coordinate Omission problem. Meaning tools fails to accommodate for delays to submit new requests when system is under load and aggregate these numbers making the latency and throughput numbers very misleading.
このビデオ で、wrk2 の作者である Gil Tene がこの問題について説明しています。 Quarkus Insights #22 では、Quarkus パフォーマンス チームの John O’Hara が、それがどのように現れるか説明しています。
Although that video and related papers and articles date all back to 2015 then even today you will find tools that fall short with the coordinated omission problem.
Tools that at current time of writing are known to exhibit the problem and should NOT be used for measuring latency/throughput (they may be used for other things):
-
JMeter
-
wrk
Tools that are known to not be affected:
-
Hyperfoil — this is what the Quarkus benchmark suite uses
したがって、 wrk2 や hyperfoil を使う場合でも、その数値が何を意味するのかを理解するようにしてください。