VirtualThreadsとは
JVM内で生成される軽量なスレッド。
そもそも、JavaはスレッドをOSで生成する。OSで生成されたスレッド(以下:OSスレッド)はJVMで生成されるスレッドより重い。だが、その分色んな処理を実行できる。
例えば、IO処理。(DBアクセス、ファイルアクセス、外部APIコール等)
WebアプリにIO処理はつきもの。
IO処理の厄介なところはIO待ちが発生すること。
OSスレッドはIO待ちが発生するとそれを待ち続けてしまう。
待ちが増えるということは、スループットが落ちるということ。
(スループット=単位時間あたりに実行された処理の数)
これをなんとかするために導入されたのが、Virtual Threads.(仮想スレッド)
Virtual Threadsを理解する上で知っておくべき登場人物は↓の3つ
- Platform, Carrier, Virtual
- Platform Thread
- OSスレッドを薄くラップしたスレッド
- ≒OSスレッドだと思って良さそう?
- Carrier Thread
- Virtualからマウント/アンマウントされるスレッド
- 実体はPlatform Threadらしい
- Virtual Thread
- JVM内で生成されるスレッド
- JVM内で大量に生成して、Carrierにマウント
- Platform Thread
クライアントからリクエストが来たら、まずJVMでVTを生成する。
VTはCTにマウントされ、IO処理を始める。
IO待ちが発生したら、VTはCTからアンマウントされ別の処理を始める。
IO待ちが解消したらVTに知らせて(Interrupt?)、CTに再びマウントして中断していた処理を再開する。
結果、IO待ちの無駄な時間に別の処理を実行できるため、スループットは向上する。
仮想スレッドは高速なスレッドではありません。プラットフォーム スレッドよりも高速にコードを実行することはありません。これらは、速度 (待ち時間の短縮) ではなく、スケール (スループットの向上) を提供するために存在します。
SpringBootで活用するには
下記をapplication.propertiesに設定するだけ。簡単。
spring.threads.virtual.enabled=true
【検証】 platform vs virtual
パフォーマンスを検証してみる。
検証に使ったソースコードはこちら。
スレッド数の違いを検証
以下のサンプルコードを使って、PlatformとVirtualの違いを確認する。
package io.github.tttol.virtualthreadexample.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.github.tttol.virtualthreadexample.service.VirtualThreadsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping
public class VirtualThreadsController {
private final VirtualThreadsService virtualThreadsService;
@GetMapping("/platform/{count}")
public String doPlatform(@PathVariable int count) {
log.info("start platform. count={}", count);
virtualThreadsService.execPlatformThread(count);
return "platform";
}
@GetMapping("/virtual/{count}")
public String doVirtual(@PathVariable int count) {
log.info("start virtual. count={}", count);
virtualThreadsService.execVirtualThread(count);
return "virtual";
}
}
package io.github.tttol.virtualthreadexample.service;
import java.util.stream.IntStream;
import org.springframework.stereotype.Service;
@Service
public class VirtualThreadsService {
public void sleep() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("Interrupted!");//適当
}
}
public void execPlatformThread(int count) {
IntStream.range(0, count)
.forEach(e -> Thread.ofPlatform().start(() -> sleep()));
}
public void execVirtualThread(int count) {
IntStream.range(0, count)
.forEach(e -> Thread.ofVirtual().start(() -> sleep()));
}
}
スレッド数の推移はjconsole
を使ってモニタリングする。
まずはPlatform。
curlした瞬間に200スレッド一気にどばっと増えている↓
curl http://localhost:8080/platform/200
次にVirtual。
10スレッドほど増えただけ↓
curl http://localhost:8080/virtual/200
JMeterで負荷を加えたときの検証
JMeterで1000リクエストほどの負荷をかけてみて、スループットとレイテンシを計測してみる。
JMeterの使い方に関しては以下記事が非常にわかりやすかった。
Platform/Virtualの切り替えはspring.threads.virtual.enabled
のtrue/falseで切り替えて比較する。
Controllerに以下のようなメソッドを作って、これをリクエストすることで計測する。
@GetMapping("sleep")
public String sleep() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
log.error("Interrupted!", e);
}
return "sleep";
}
スループット[/sec] | 最小レイテンシ[sec] | 最大レイテンシ[sec] | 平均レイテンシ[sec] | |
---|---|---|---|---|
Platform Thread | 14.6 | 5.001 | 47.520 | 26.216 |
Virtual Thread | 124.8 | 5.000 | 5.133 | 5.006 |
スループットが約10倍、レイテンシもVirtualのほうはほぼ5秒で完結している。
Platformのほうは遅延がひどい。
次に、sleep時間をもう少し長く10秒にしてみる。
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
スループット[/sec] | 最小レイテンシ[sec] | 最大レイテンシ[sec] | 平均レイテンシ[sec] | |
---|---|---|---|---|
Platform Thread | 10.0 | 10.001 | 97.543 | 53.737 |
Virtual Thread | 76.9 | 10.000 | 10.059 | 10.003 |
Virtualのほうは10秒台で完結。Platformは平均53秒近くかかっていしまっている。
ただし、5秒のときも10秒のときも、Virtual Threadの処理自体が高速になっているわけではない。
スループットが向上するだけである点に注意。
参考