バーチャルスレッドは実アプリでどこまで使えるか

0.9K Views

November 15, 25

スライド概要

JJUG CCC 2025 Fall 登壇資料

profile-image

Java, Spring Boot, React, Vue.js など

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

バーチャルスレッドは 実アプリでどこまで使えるか 〜コンテナ環境での効果検証でわかった注意事項〜 株式会社サンアーチ Takeshi Miyajima 2025.11.15 JJUG CCC 2025 Fall

2.

バーチャルスレッド(仮想スレッド)とは ● Java21で導入された軽量スレッド ● 既存のスレッド(プラットフォームスレッド)と同じAPIで利用可能 ● 多数の仮想スレッドで少数のOSスレッドを共有し、 ブロッキング時に切り替える仕組み プラットフォームスレッド 仮想スレッド OSスレッドを占有 OSスレッドを共有 OSスレッド#1 PT#1 OSスレッド#2 PT#2 Blocking Blocking OSスレッド#1 VT#1 OSスレッド#2 VT#2 VT#3 VT#4 VT#1 VT#2 2

3.

仮想スレッドに対する期待 ● ● プラットフォームスレッドでは不可能な数万単位のスレッドを作成可能 大量の同時アクセスに耐えられるはず 仮想スレッドは高速スレッドではありません。つまりプラットフォーム・ス レッドよりも速くコードが実行されることはありません。速度(低レイテン シ)ではなく、スケール(高スループット)を提供するために存在します。 仮想スレッドを使用する理由 https://docs.oracle.com/javase/jp/21/core/virtual-threads.html 3

4.

仮想スレッドに関する疑問 実際に本番で導入してみたいが・・・ ● 有効化すると本当に効果があるの?どれくらい違うの? ● WebFluxとのパフォーマンス上の違いは? ● 導入する上での注意事項はないの? 導入に向けての疑問点を、 実環境に近い状況で検証してきました! 4

5.

自己紹介 Takeshi Miyajima 宮島 健 株式会社サンアーチ 業務システム開発 / 運用ツール開発 Java / Spring Boot / React / Vue / etc 5

6.

注意事項 ● あくまで個人でできる範囲での検証結果です ● アプリの特性や状況により、 全く違った結果が出る可能性があります ● 本番に投入する際はしっかり検証をお願いします 6

7.

省略表記 ● PT:プラットフォームスレッド(従来のスレッド) ● VT:仮想スレッド(バーチャルスレッド) 7

8.

検証方法 8

9.

検証条件設定のポイント ● スリープではなくAPIアクセスにすることで、 実際に遭遇しうる注意点や傾向を探る ● 処理方式の違いによる差はどの程度あるのかを探る ○ PTでもスレッド上限を上げさえすれば、同じパフォーマンスが出るのか? ○ VTとWebFluxはどちらが効率的か? 9

10.

検証条件 PT/VT/WebFlux を切り替え サンプル アプリ 同時アクセス数を 段階的に2,000まで増加 リソース制限 CPU=4.0, MEM=2GB Nginx 200msスリープ 往復250ms程度 期待スループット 2000/0.25 = 秒間8,000リクエスト にどこまで近づける? 10

11.

その他検証条件 ● Dockerイメージ:Spring Bootの「build-image」で作成 ● Dockerコンテナ:CPU4コア、メモリ2GB制限 ● PT:同時アクセス2,000件向けに上限を調整 デフォルトの200だと 比較にならないため ○ server.tomcat.threads.max=2000 ○ BPL_JVM_THREAD_COUNT=2050 (Tomcatスレッド+予備50) ○ -Xss256k (スレッドあたりのメモリ量を1MBから変更) ● その他はデフォルトのまま、GCチューニングもなし 11

12.

検証結果 12

13.

検証結果 1 WebFlux 8,000 6,000 4,000 2,000 500 1,000 1,500 同時アクセス数 2,000 3 PT レイテンシー(p95) 高いほど良い レスポンス(ms) リクエスト数/秒 スループット(平均) VT 2 低いほど良い 800 600 400 200 500 1,000 1,500 2,000 同時アクセス数 13

14.

結果分析:WebFlux ↔ PT・VT WebFlux WebFluxは省リソースでGCが少ない VT PT GC時間(平均) 低いほど良い 1,250 1,000 750 500 250 低いほど良い 300 ms / 秒 MB ヒープ使用量(平均) 500 1,000 同時アクセス数 1,500 2,000 200 100 500 1,000 1,500 2,000 同時アクセス数 14

15.

結果分析:PT ↔ VT・WebFlux VT・WebFluxはコンテキストスイッチが少ない WebFlux 回数 / REQ コンテキストスイッチ 複数のプロセス・スレッドが1つのCPUを 共有できるように、実行中の作業を中断 し、別の作業に切り替えること。 コンテキストスイッチが増えると処理効 率が低下する。 VT コンテキストスイッチ数 (1リクエストあたり) PT 80 60 40 20 500 1,000 1,500 2,000 同時アクセス数 低いほど良い 15

16.

リソース効率比較 CPU使用率・メモリ使用量もWebFluxが高効率 WebFlux CPU% / REQ CPU使用率 (1リクエストあたり) 低いほど良い 0.20% 0.10% 0.06% 0.04% 500 1,000 1,500 同時アクセス数 2,000 メモリ(MB) / REQ VT コンテナメモリ使用量 (1リクエストあたり) PT 低いほど良い 2.00 0.80 0.40 0.20 500 1,000 1,500 2,000 同時アクセス数 16

17.

補足:CPUバウンドの場合 素数計算 約200ms サンプル アプリ WebFlux VT PT 往復250ms程度 CPUが4コア(同時に4件まで) → どれも秒間20アクセスが限界 リクエスト数/秒 スループット(平均) 20 15 10 5 20 40 60 80 100 同時アクセス数 17

18.

補足:CPUバウンドでのVT・WebFluxの注意点 CPUがフル回転してりると、VT・WebFluxは監視用アクセスに応答しづらくなる → ブロッキング時にスレッドを切り替える仕組みであることが原因 → micrometer-registry-prometheusでメトリクス収集する場合は要注意 CPU使用率(VT) メトリック収集できていない (新規アクセスを全く受け付けない) CPU使用率(PT) PTはブロッキング関係なく OSがスレッドをスイッチするため、 安定して収集可能 18

19.

検証結果まとめ 19

20.

IOバウンド ● パフォーマンス:WebFlux > VT > PT ● 効率性:WebFlux > VT > PT ● WebFlux ↔ VT・PTの差:GCプレッシャー (チューニング余地あり) ● WebFlux・VT ↔ PTの差:コンテキストスイッチ ○ スレッド数が増えるほど差が広がる ○ PTのスレッド上限を上げても不利になっていく 20

21.

CPUバウンド ● パフォーマンス:差異なし ○ VTはCPUバウンドが苦手というわけではない ● 安定性:PT > WebFlux = VT ○ VT・WebFluxでは限界時のアクセス停止に注意 21

22.
[beta]
じゃあWebFluxでいいのでは?
→ WebFluxはリアクティブプログラミング必須
学習コストが高く、複雑な業務ロジックは辛い・・・
WebFluxは
学習コストが高い

// WebFluxの例
return repo.findById(id)
.switchIfEmpty(Mono.error(new NotFound()))
.flatMap(u -> web.get().uri("/p/{id}", id)
.retrieve()
.bodyToMono(Profile.class)
.map(p -> new Detail(u, p)))
.onErrorResume(e ->
Mono.error(new ApiError(e)));

VTはこれまで通りの
同期スタイルでOK

// 仮想スレッドの例
try {
var u = repo.findById(id)
.orElseThrow(NotFound::new);
var p = client.get().uri("/p/{id}", id)
.retrieve()
.body(Profile.class);
return new Detail(u, p);
} catch (Exception e) {
throw new ApiError(e);
}

22

23.

検証結果まとめ ● 仮想スレッドは、IOバウンド処理が主体のアプリに優位性あり ○ PTよりも高スループット、WebFluxより書きやすい ● 業務ロジックの少ないアプリ(※) → WebFluxがおすすめ ○ リアクティブプログラミングのデメリット少 ○ より高パフォーマンス・高効率 ※Spring Cloud Gatewayのようなゲートウェイサービスなど 23

24.

仮想スレッド使用時の注意点 24

25.

Java24以降を使うべし Java24でsynchronizedブロックでのスレッド固定化の問題が解消し、 固定化によるパフォーマンス低下リスクが激減した。 JEP 491: Synchronize Virtual Threads without Pinning 仮想スレッドがキャリアに固定されている場合、ブロッキング操作中にマウント解除する ことはできません。仮想スレッドが固定されるのは次の状況です: ● 仮想スレッドが、synchronizedブロックまたはメソッド内でコードを実行します ● 仮想スレッドが、nativeメソッドまたは外部関数実行します 仮想スレッドのスケジュールおよび固定された仮想スレッド https://docs.oracle.com/javase/jp/21/core/virtual-threads.html Java24で削除 25

26.
[beta]
micrometer-java21の追加を忘れずに
Micrometerで仮想スレッドのメトリクスを計測するには、
「io.micrometer:micrometer-java21」を追加する必要あり。
JVM Metrics :: Micrometer
jvm_threads_virtual_live_threads{application="sample-web",scheduling_status="mounted"} 0.0
jvm_threads_virtual_live_threads{application="sample-web",scheduling_status="queued"} 0.0
jvm_threads_virtual_parallelism{application="sample-web"} 10.0
jvm_threads_virtual_pinned_seconds_count{application="sample-web"} 0
jvm_threads_virtual_pinned_seconds_sum{application="sample-web"} 0.0
スレッドの固定化
jvm_threads_virtual_pinned_seconds_max{application="sample-web"} 0.0
関連
jvm_threads_virtual_pool_size_threads{application="sample-web"} 0.0
jvm_threads_virtual_submit_failed_total{application="sample-web"} 0.0

26

27.

Java Buildpackではスレッド数を調整すべし 環境変数「BPL_JVM_THREAD_COUNT」で調整 Memory Calculator デフォルト(MB) 変更後(MB) 備考 Total Container Memory 2048 2048 Heap 1411 1611 Non-Heap 637 437 Direct Memory 10 10 JVM Default Metaspace 137 137 自動計算 Reserved Code Cache 240 240 JVM Default Thread Stack 250 50 Thread Stack Size Thread Count Headroom 1 250 0 WebFluxは自動調整 仮想スレッドは手動調整 1 JVM Default 50 プラットフォームスレッド数の見積もり 0 https://paketo.io/docs/reference/java-reference/#memory-calculator 27

28.

隠れプラットフォームスレッド生成に注意 見えないところでプラットフォームスレッドが作られるパターンあり →スレッドスタック不足の要因にも // NGパターン return RestClient.builder() .requestFactory(new JdkClientHttpRequestFactory(HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .build())) .build(); // OKパターン return RestClient.builder() .requestFactory(new JdkClientHttpRequestFactory(HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .executor(Executors.newVirtualThreadPerTaskExecutor()) .build())) .build(); 28

29.

接続プールの上限に注意 接続プール上限がボトルネックになるため、調整必須 リクエスト数/秒 スループット(平均) VT PT 500 400 300 200 100 500 1,000 同時アクセス数 DB接続プール枯渇状況 接続プール数 WebFlux DB接続プール 上限100 同時アクセス 200 100 500 余ってる 足りない 1,500 2,000 時間 29

30.

限界付近でのOutOfMemoryErrorに注意 ヒープに余裕がないと、限界付近でOutOfMemoryErrorが頻発する →捌ききれないリクエストでもスレッドを起動してしまい、 仮想スレッドがヒープを食いつぶしてしまうのが原因と推測 対策 ● 最大同時接続数を調整 ● ヒープは多めに確保 Spring Boot & Tomcatの場合は、 server.tomcat.max-connectionsで 調整できそう(未検証) 30

31.

おわりに 31

32.

おわりに IOバウンド中心のアプリに仮想スレッドは有効 監視も含めしっかり検証して導入を 32

33.

ご清聴ありがとうございました アンケートへのご協力お願いします 全体アンケート セッションアンケート 33

34.

APPENDIX 34

35.

IOバウンド (APIアクセス) CPU=4.0, MEM=2GB 35

36.

IOバウンド (APIアクセス) CPU=4.0, MEM=2GB 36

37.

IOバウンド (APIアクセス) CPU=4.0, MEM=2GB 37

38.

IOバウンド (DBアクセス) CPU=4.0, MEM=2GB, DB接続プール調整後

39.

IOバウンド (DBアクセス) CPU=4.0, MEM=2GB, DB接続プール調整後

40.

IOバウンド (DBアクセス) CPU=4.0, MEM=2GB, DB接続プール調整後

41.

CPUバウンド CPU=4.0, MEM=2GB 41

42.

CPUバウンド CPU=4.0, MEM=2GB 42

43.

CPUバウンド CPU=4.0, MEM=2GB 43

44.

Spring Bootでの仮想スレッド有効化方法 プロパティファイルで有効化だけ →大半のスレッドが仮想スレッドになる ● ● 組み込みWebサーバー(Tomcat, Jetty)、RabbitMQ、Kafka、 @Async、@Scheduled、etc… // application.properties spring.threads.virtual.enabled=true この指定のON/OFFで PTとVTを変更 44

45.

WebClientの接続プール WebFluxで使用するWebClientは、内部に接続プールを持つ →調整しないと、高負荷時にエラーが頻発 待機キュー 1,000まで WebClient 同時接続 500まで リクエスト数/秒 スループット(平均) “Pending acquire queue has reached its maximum size of 1000” 同時アクセス数 45

46.

参考サイト ● ● ● ● ● ● ● Virtual Threads (Java SE 25 / Core Libraries) JEP 491: Synchronize Virtual Threads without Pinning SpringApplication :: Spring Boot - リファレンス (仮想スレッド) REST クライアント :: Spring Framework - リファレンス HTTP Client :: Reactor Netty Reference Guide (Connection Pool) Java Buildpack Reference (Memory Calculator) JVM Metrics :: Micrometer (Java 21 Metrics) 46

47.

EOF 47