The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.
このページを編集

パフォーマンスの測定

このガイドでは以下をカバーしています:

  • メモリ使用量の測定方法

  • 起動時間の測定方法

  • 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.

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

プロセスに割り当てられた各メモリ領域が一覧表示されます;

  • 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:

  1. Warmup — 2 minutes of traffic with 100 concurrent connections (configurable)

  2. Cooldown — a 30-second pause

  3. Load test — 30 seconds of measured traffic with 100 concurrent connections

The configuration uses these defaults, which can be overridden with -P flags:

パラメーター デフォルト 説明

CONNECTIONS

100

Number of concurrent connections

THREADS

2

Hyperfoil executor threads

WARMUP_DURATION

2m

Duration of warmup phase

LOAD_DURATION

30s

Duration of measured load test

PATH

/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 として実行することも可能です。

分離の無効化

分離はGraalVMの優れた機能ですが、Quarkusは現段階では使用していません。

-H:-SpawnIsolates で無効化します。

すべてのサービスローダー実装の自動登録を無効にする

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:

したがって、 wrk2hyperfoil を使う場合でも、その数値が何を意味するのかを理解するようにしてください。

関連コンテンツ