社内ミートアップでLTの時間をもらったので、ここに書いた内容を3分で喋ろうとしたんだけど、どうしても3分をちょっと超えてしまうので無理を言って4分もらって喋ってきた(運営のみなさんありがとうございます)。ぐったり。明日から普通のエンジニアに戻ります。
実感を大切にしてみた
こういうことに気をつけてLTしてみた:
- ブログの記事に書いてるコードを見せるんじゃなくて、実際のIDE内のソースコードを見せること
- そのときにIDEAのプレゼンテーションモードで見せること
- 実際にアプリを動かした状態でcurlで叩いて動いてるのを見せること
- Gatlingを実際にgradleで実行すること
- そこでBlockingAppのスレッドが増えるところをVisualVMで実際に見せること
その方が聞いてて「へー。本当に動くんだ」とか「さわってみようかな」とか思ってもらえるかなーと思って。ってことで以下は内容の日本語訳。全部喋ると全然時間が足りないので、実際はこの中の要点だけをpick upして喋った。
バズワードをポッケに
はいどーもしーばです。マイクロサービスとかブロックチェーンとか聞いたことある?そういう流行りみたいなのが色々出てくるけど鵜呑みにせずに自分の目で確かめて必要なときに取り出せるように引き出しに入れておくのが大切だと思うの。てことでリアクティブプログラミングについて触ってみた話。
— Mitsuyuki Shiiba (@bufferings) 2018年3月24日
リアクティブなウェブアプリケーションで嬉しいことのひとつは、たくさんのリクエストを少ないスレッドで効率良くさばくことができるってこと。なので、それを実際に自分の目で確認してみようと思う。
SpringBoot2 Reactive Web
SpringBoot2が今月頭についにリリースされた。その中の大きな新機能がReactive Web対応(Spring5が対応して、それをSpringBoot2でもサポートしたという感じかな)。これまでの、サーブレットAPIベースでブロッキング処理のServlet Stack(下図の右半分)はもちろんそのまま使えるんだけど、それとは別に、リアクティブストリームベースのReactive Stack(図の左半分)というものが追加された。
ので、従来のSpring MVCを使ったアプリと、新しいWebFluxを使ったアプリのパフォーマンスを比べてみる。
(Image from https://spring.io/ )
デモアプリの構成
SpringBoot1とSpringBoot2の比較をしためっちゃ良い記事があったので、これをほぼそのまま参考にしてSpring MVCとSpring WebFluxの比較をすることにした。
こんな感じの構成:
3つのアプリがある:
- delay-service
- blocking-app
- reactive-app
delay-serviceは外部APIみたいな役割をしてて、300ms待ってからレスポンスを返す。それをblocking-appとreactive-appがそれぞれ呼び出してて、blocking-appはブロッキングなやり方で、reactive-appはリアクティブなやり方で処理してそれぞれレスポンスを返す。このblocking-appとreactive-appのパフォーマンスを比較する。
ソースコードはここに置いといた:
delay-service
さっきは「delay-serviceは300ms待ってからレスポンスを返す」って書いたけど、実際はパスパラメータで指定した時間だけ待ってからレスポンスを返すようにしてる:
@GetMapping("/{delayMillis}") public Mono<String> get(@PathVariable int delayMillis) { return Mono.just("OK") .delayElement(Duration.ofMillis(delayMillis)); }
(最初はブロッキングなやり方で書いてたんだけど、そうするとこいつがパフォーマンスのボトルネックになってしまったので、リアクティブなやり方に変えた)
blocking-app
blocking-appは普通のSpring MVCアプリ。delay-serviceをRestTemplateで呼び出して文字列を返してるだけ:
private static final String DELAY_SERVICE_URL = "http://localhost:8080"; private final RestTemplate client; public BlockingApp(RestTemplateBuilder builder) { client = builder.rootUri(DELAY_SERVICE_URL).build(); } @GetMapping("/{delayMillis}") public String get(@PathVariable String delayMillis) { String result = client.getForObject("/" + delayMillis, String.class); return "Blocking:" + result; }
reactive-app
reactive-appはリアクティブになってるんだけど、ほんとにこれまでのアプリを書くのと同じような感じで書ける。delay-serviceをWebClientを使って呼び出して Mono
でラッピングした文字列を返すだけ:
private static final String DELAY_SERVICE_URL = "http://localhost:8080"; private final WebClient client = WebClient.create(DELAY_SERVICE_URL); @GetMapping("/{delayMillis}") public Mono<String> get(@PathVariable String delayMillis) { return client.get() .uri("/" + delayMillis) .retrieve() .bodyToMono(String.class) .map(s -> "Reactive:" + s); }
てことで、パフォーマンスをチェックしよう
delay-serviceを起動:
./gradlew -p apps/delay-service clean bootRun curl -w "\n%{time_total}s\n" localhost:8080/1000 # returns "OK" after 1000ms curl -w "\n%{time_total}s\n" localhost:8080/2000 # returns "OK" after 2000ms
blocking-appを起動:
./gradlew -p apps/blocking-app clean bootRun curl -w "\n%{time_total}s\n" localhost:8081/2000 # returns "Blocking:OK" after 2000ms
reactive-appを起動:
./gradlew -p apps/reactive-app clean bootRun curl -w "\n%{time_total}s\n" localhost:8082/2000 # returns "Reactive:OK" after 2000ms
これで3つとも起動したぞー。
負荷テストのシナリオ
Gatling(https://gatling.io/)を使う。シナリオは「1,2秒の間隔をあけてAPIを30回呼び出すのを1000人のユーザーが実施する」という感じ。初Scala:
val myScenario = scenario("Webflux Demo").exec( repeat(30) { exec( http("request_1").get(targetUrl) ).pause(1 second, 2 seconds) } ) setUp(myScenario.inject(rampUsers(simUsers).over(30 seconds)))
blocking-appに対してGatlingを実行
./gradlew -p apps/load-test -DTARGET_URL=http://localhost:8081/300 \ -DSIM_USERS=1000 gatlingRun
VisualVMで見てみると、スレッド数が200まで増えてることが分かる。この200はTomcatのmaxThreadのデフォルト値。
reactive-appに対してGatlingを実行
./gradlew -p apps/load-test -DTARGET_URL=http://localhost:8082/300 \ -DSIM_USERS=1000 gatlingRun
こっちはリクエストをさばくのに4スレッドしか使ってないからずっと一定:
負荷テストの結果
下に結果を貼った。1000ユーザーのときはどちらのアプリも想定通り300msでレスポンスを返してる。けど、blocking-appのパフォーマンスは3000ユーザー、6000ユーザーに対してだんだん悪くなってる。
一方で、reactive-appはずっとグリーンのまま。最終的には6000ユーザーに対して95パーセンタイルが427msで、2000rpsくらい処理してる。(Core i7-7500U 2.7GHz/16GB RAMで確認)
へー。実際に書いて動かしてみると面白いね。
blocking-app
with 1000 users:
with 3000 users:
with 6000 users:
reactive-app
with 1000 users:
with 3000 users:
with 6000 users:
やり残し
TomcatのmaxThreadsのチューニング
今回はデフォルト設定のままで比較してみたんだけど、TomcatのmaxThreadsをチューニングしたら良いパフォーマンスが出そうではある。600rpsという数字も300msかかる処理を200スレッドで処理してるんだったらそうだろうという感じよね。
環境の分離
自分のノートPCで全部動かして確認したから、アプリとdelay-serviceとGatlingがそれぞれ作用しあってたよなーって思う。特にCPU。なので、環境をキレイにわけて別のマシンで動かしてるやつでパフォーマンスをちゃんと見たらまたちょっと違うのかなーと。
まとめ
面白かった。マイクロサービスアーキテクチャみたいにいっぱいAPIを呼び出すような場合は、リアクティブなやり方だと待ち時間が有効に使えるから良さそう。ただ、リアクティブプログラミングは、これまでサーブレットでブロッキングな処理を書いてきたエンジニアにとっては頭の切り替えが必要だろうから(僕もそう)、そういう人はちょくちょく触って遊んでおくといいんじゃないかなー。
ということでWebFluxをポッケに入れといた。よー。今回はMonoだけを使ったけど、Fluxでごにょごにょしてみるのも面白そうだなー。