昔から注目していたProject Loom がJava 19にPreview版としてようやく入ってきました。 そこで、Virtual Threadの効果を試してみました。
今回試したのは、以下の環境でデータベースアクセスして結果を返すRest APIを作成し、大量同時アクセスしてみる、ということを行いました。
これは私が本業で使っている環境に近い構成です。今後の採用指針として調査する目的も兼ねてます。
効果があるなら、少ないメモリ、スレッド数でたくさんの処理をさばいてくれるはずです。
Spring WebMVCでVirtual Threadを有効にする
今回、Spring WebMVCを使っています。サーバはデフォルトのTomcatで、Tomcatがスレッドを生成する処理を置き換える形で、Virtual Thread が生成されるようにします。
処理の参考は、こちらを参考にしています。
/** * Virtual Threadを生成する設定。 * @see https://spring.io/blog/2022/10/11/embracing-virtual-threads */ @Configuration class ExecutorConfig { @Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME) fun asyncTaskExecutor(): AsyncTaskExecutor? { return TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor()) } @Bean fun protocolHandlerVirtualThreadExecutorCustomizer(): TomcatProtocolHandlerCustomizer<*>? { return TomcatProtocolHandlerCustomizer { protocolHandler: ProtocolHandler -> protocolHandler.executor = Executors.newVirtualThreadPerTaskExecutor() } } }
Virtual Threadはまだpreview機能なので、実行するときにJavaのオプションに、--enable-previewを指定して実行しないと、機能が有効になりません。
サーバ本体は、以下のような感じで起動してます。
java --enable-preview -jar build/libs/virtualthread-0.0.1-SNAPSHOT.jar
データベースアクセス
データベースはMySQL 8です。使用したORMはJPA(Hybernate)となります。今回、usersテーブル、articlesテーブル、commentsテーブルという3つのテーブルを用意し、articlsに100件、それぞれコメントデータを1件づつ関連付けてデータを入れました。
ベンチマーク
早速ベンチマークしてみましょう。実行環境は私が使っているデスクトップマシンで、以下のようなスペックです。
条件
wrkというベンチマークツールを使い、5スレッド生成、同時 500 アクセスで30秒間実行しています。 処理する機能は、100件あるarticlesデータをすべてJson形式で取得する処理となっています。 また、サーバは最大500スレッドまで生成可能にしました。
Virtual Threadを使わない
まずはVirtual Threadを使わない、今と同じ1接続、1Threadを生成して処理するパターンです。
1回目
yaku@ubuntu:~$ wrk -t5 -c500 -d30s http://localhost:8080/api/articles Running 30s test @ http://localhost:8080/api/articles 5 threads and 500 connections Thread Stats Avg Stdev Max +/- Stdev Latency 311.84ms 207.85ms 1.84s 72.09% Req/Sec 335.71 119.71 770.00 66.34% 49339 requests in 30.08s, 3.54GB read Socket errors: connect 0, read 0, write 0, timeout 1 Requests/sec: 1640.14 Transfer/sec: 120.48MB
2回目
yaku@ubuntu:~$ wrk -t5 -c500 -d30s http://localhost:8080/api/articles Running 30s test @ http://localhost:8080/api/articles 5 threads and 500 connections Thread Stats Avg Stdev Max +/- Stdev Latency 244.59ms 140.92ms 1.28s 72.79% Req/Sec 417.50 125.41 1.13k 73.34% 61028 requests in 30.08s, 4.37GB read Requests/sec: 2028.76 Transfer/sec: 148.72MB
きちんと1接続、1Thread生成しているようですね。メモリもかなり食います。
Virtual Threadを使う
次に、Virtual Threadを使用したパターンで実施しました。
1回目
yaku@ubuntu:~$ wrk -t5 -c500 -d30s http://localhost:8080/api/articles Running 30s test @ http://localhost:8080/api/articles 5 threads and 500 connections Thread Stats Avg Stdev Max +/- Stdev Latency 305.18ms 203.35ms 1.99s 79.87% Req/Sec 344.68 144.35 808.00 68.14% 50687 requests in 30.07s, 3.63GB read Socket errors: connect 0, read 0, write 0, timeout 79 Requests/sec: 1685.55 Transfer/sec: 123.55MB
2回目
yaku@ubuntu:~$ wrk -t5 -c500 -d30s http://localhost:8080/api/articles Running 30s test @ http://localhost:8080/api/articles 5 threads and 500 connections Thread Stats Avg Stdev Max +/- Stdev Latency 261.18ms 79.57ms 890.32ms 91.12% Req/Sec 378.99 116.71 828.00 69.27% 56642 requests in 30.05s, 4.05GB read Requests/sec: 1885.12 Transfer/sec: 138.18MB
すばらしい。Virtual Threadを使わない場合と比べて、メモリ使用量が2分の1以下、最大スレッド数も14分の1と、消費するマシンリソースはかなり減ってます。
1秒あたりのリクエスト数は、約8%ほどVirtual Threadのほうが少ないようです。
また、最初の方はタイムアウト(=2秒)も結構発生していました。
まとめ
Virtual Threadを使用すると、同じリクエスト数の処理に対して、明らかに必要とするマシンリソースは少なくなるようです。 ただ、1リクエストの処理は若干遅く、起動直後は処理が追いつかなくなることもあるようでした。 この処理の遅延がどこにあるのかまだ分かりませんが、正式リリースされる頃には解消されることを期待したいです。
現状、mysqlのJDBCドライバはPostgreSQLのJDBCドライバと同様に、 synchronized ブロックが入った処理があるようです。この処理がVirtual Threadに親和性のあるロック処理に変わらないと、性能が発揮できないのかなぁと思ったりしてます。
今回使用したソースはこちら github.com