springboot-webfluxのバックプレッシャーを体験してたらいい感じだった

springboot-webfluxのバックプレッシャーを体験してたらいい感じだった
目次

2018/3にリリースされた springboot2 から spring5 がバンドルされるようになりました。 リリースの中でも注目機能と言われている webflux 、とりわけ webflux が内包しているリアクティブプログラミングライブラリである Reactor はspringユーザであれば気になるはずです。今回はバックプレッシャーがいい感じだったので、それをまとめてみました。

今回作成したリポジトリ

今回作成したリポジトリは こちら です。 全てローカル環境で動かせるように docker-compose でコンポーネント化してあるものの、 ローカルマシンのリソースを食い合うため、負荷試験をするときはLinuxサーバ上に展開することをオススメします。

RouterFunctionを登録する

以下のような RouterFunction を作成し、 @Bean で登録しておきます。 RouterFunctionのレスポンスを返す部分はもう少しいい実装がありそうですが、一旦こうしました。

  • RouterFunction
 1@Component
 2public class HelloWebClientHandler {
 3
 4    @Value("${app.backend.uri}")
 5    private String baseUri;
 6
 7    private static final String PATH = "/test";
 8
 9    public RouterFunction<ServerResponse> routes() {
10        return RouterFunctions.route(
11                RequestPredicates.GET("/hello")
12                        .and(RequestPredicates.accept(MediaType.APPLICATION_JSON))
13                , this::webclient);
14    }
15
16    private Mono<ServerResponse> webclient(ServerRequest req) {
17        return WebClient.builder()
18                .baseUrl(baseUri)
19                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON.toString())
20                .build()
21                .get()
22                .uri(uriBuilder -> {
23                    uriBuilder.path(PATH);
24                    if (req.queryParam("time").isPresent()) {
25                        uriBuilder.queryParam("time", req.queryParam("time").get());
26                    }
27                    return uriBuilder.build();
28                })
29                .accept(MediaType.APPLICATION_JSON)
30                .exchange()
31                .flatMap(response ->
32                    ServerResponse.ok()
33                            .contentType(MediaType.APPLICATION_JSON)
34                            .body(response.bodyToMono(User.class), User.class)
35                            .switchIfEmpty(ServerResponse.notFound().build())
36                );
37    }
38}
  • RouterFunctionを登録する側

作成した HelloWebClientHandler を登録します。

 1@Configuration
 2@EnableWebFlux
 3public class WebConfig extends DelegatingWebFluxConfiguration {
 4    // ~中略~
 5
 6    @Bean
 7    RouterFunction<ServerResponse> route7(HelloWebClientHandler webClientHandler) {
 8        return webClientHandler.routes();
 9    }
10}

あとは main メソッドを持ったクラスを作ってあげればspringbootアプリケーションは作成完了です。

パフォーマンスを測定してみた

springbootのjarファイルをEC2上に置いて実際にバックプレッシャーの効果を見てみましょう。

環境情報

私のローカルマシン上からgatlingを実行し、EC2上のspringbootアプリケーションに負荷がけをします。 springbootアプリケーションは、バックエンドのmockサーバ(OpenRestyを使用)に対して WebClient を使ってAsyncなHTTP通信を行います。

architechture

なお、EC2インスタンスは t2.small を使用し、JVMへの割当メモリは 最大256M に設定しています。 また、バックプレッシャーを観測したいので、mockサーバではsleep処理を入れています。

バックプレッシャーを体験する

springboot-webfluxは普通に生きている

バックプレッシャーの効果を見てみましょう。 gatlingのリクエスト量と、mockサーバ側のsleep時間は以下です。

gatlingのリクエストmockのsleep時間
150req/s1s

webflux-sleep-150

普通に全て200レスポンスが返却されていますね。すごい。

次にsleep時間を 5s にして見てみます。 対照実験的な意味で 150req/s がよかったのですが、今回は私のマシンのパワー不足により 130 までしか出ませんでした。

gatlingのリクエストmockのsleep時間
130req/s5s

webflux-sleep-130-5s

バックエンドサーバが5秒も応答待ちでも普通に200応答できていますね。

springboot-webmvcはやっぱり死んだ

比較として、従来の springboot-webmvc ではどうでしょう。 サーブレットコンテナはデフォルトの embed-tomcat として、application.yaml の設定もデフォルトとします。 また、mockへの通信を行う HttpClient はConnectionPoolingから取得するように実装した上で以下の条件でリクエストを流してみました。

gatlingのリクエストmockのsleep時間
100req/s1s

mvc-sleep-100

うむ。やはりだめでしたか。

一応、同条件にて、HttpClientのPool数も増やしたりして調整しましたが、エラーレスポンス件数が0にはなりませんでした。

mvc-sleep-100-tuned

スレッド増加の傾向を見てみる

負荷試験中のスレッドの増加傾向も見てみましょう。この観点は単純に netty4 vs tomcat に依存する部分が大きいのですが、見てみましょう。

webflux(Netty4)の場合は起動時からスレッド数が一定ですね。

webflux-thread

tomcatはやはりリクエストをさばくためにスレッドが必要になってしまうため、増加傾向にあります。

tomcat-thread

まとめ

今回は springboot-webfluxspringboo-webmvc を比較して、バックプレッシャーがどんな感じかを確認しました。 梱包されている netty4 が持つnon-blockingな仕組みのおかげで、バックエンドサーバの遅延に引きずられることなくレスポンスを返却できていることがわかります。

しかしながら、もちろん銀の弾丸ではなくて、実装する上でのデメリットや考慮ポイントが他のサイトを見ると情報が色々出てきます。 例えば、自身が書こうとしている処理がblockingな処理なのか、non-blockingな処理なのかを実装する側が気をつけないといけない、という点があります。 そのためには、ライブラリがどのように動いているかをきちんと把握しないといけないでしょう。 加えて、スレッドを共有する形でアプリケーションが動作するので、 ThreadLocal をむやみに使わない方が良い気もしています。

ただ、 tomcatでサポートしているServlet 3.1の非同期IOよりは良さそうなので、用法を見定めた上で使っていきたいですね。

参考にさせていただいたサイト