Devopsトレンドレポート
DevOps領域で今年見落とすべきではない技術は何か調べよう。チームのイノベータになりKubernetesやサービスメッシュ、カオスエンジニアリングについて学ぼう。このレポートを読もう。
Spring Framework 5は2つのwebスタックであるサーブレットとリアクティブから選ぶ、もしくは一緒に使うという選択肢をくれる。これはアプリケーションにおける非同期でノンブロッキングな並行性への大きな、全体的な移行を反映している。この記事の目的は少し背景に触れ、取り得る選択肢の範囲を説明し、これらのスタックから選択する際のガイダンスを提供することである。
以下この記事では"サーブレットスタック"と"リアクティブスタック"をSpring Framework 5がサポートする2つのwebスタックの略語として使う。アプリケーションはSpring MVC (spring-webmvc
モジュール)とSpring WebFlux (spring-webflux
モジュール) webフレームワークを通じて使えるものである。
変更へのモチベーション
今まで"リアクティブ"という言葉を異なるところでたくさん聞いていて、すっかりバズワードになってしまった。とは言えその背後には実際の動向がある。非同期性の高まりというストーリであり、それはアプリケーションが相対的に単純で自己完結していたサーブレットコンテナの黎明期から始まり、ほとんどすべての場所に非同期性が隠れている現代までの話である。この非同期性すべてを処理するために、私たちはうまく準備していた。
伝統的に、JavaはブロッキングでIOバウンドな処理 (たとえばリモートコール) の並行実行にスレッドプールを使ってきた。表面上は単純に見えるが、見かけによらず複雑であり、それには2つの理由がある。1つ目は、同期と共有構造で大変な思いをしているとき、正しくアプリケーションを動作させるのが難しいということだ。2つ目は、全ブロッキング処理が単にボーっと待機するためだけにスレッドをさらに要求するとき、効果的にスケールさせるのが難しいということだ。そして制御できないレイテンシ (たとえばリモートクライアントやサービスの遅さ) のなすがままとなる。
当然さまざまな解決策 (コルーチン、アクターなど) が発展し、フレームワークまたは言語へ並行性を追いやろうとした。今JavaでProject Loomと呼ばれる軽量スレッドモデルの提案があるが、本番環境でこれを見るのはまだ数年かかるだろう。今日できることは非同期性をうまく扱うことだ。
よくある誤解は、とくにJava開発者にあるのがスケールには多くのスレッドが必要というものだ。これは命令プログラミングとブロッキングIOのパラダイムでは真実かもしれないが、一般ではそうではない。アプリケーションが完全にノンブロッキングであるとき、少数の固定したスレッドでスケールできる。Node.jsがスレッド1つだけでスケールできるというのが証明だ。Javaでは1スレッドに限定する必要はないのですべてのコアが稼働し続けるよう十分な数のスレッドを使える。まだ原則が残っている -- より高い並行性のためにスレッド追加に頼らない、ということだ。
アプリケーションをどのようにノンブロッキングにするのか。何よりもまず命令プログラミングと関連づいた伝統的なシーケンシャルロジックを捨て去らなければならない。非同期APIを選択して、それらが生成するイベントに反応することを学ばなければならない。もちろん、非同期コールバックを扱うとすぐに扱いづらくなる。よりよいモデルのためにJava 8で導入されたCompletableFutureに目を向けよう。これは継続スタイルの流れるようなAPIを与えてくれ、そこではロジックはネストしたコールバックではなくシーケンシャルなステップで宣言される。
CompletableFuture.supplyAsync(() -> "stage0")
.thenApply(s -> s + "-stage1")
.thenApplyAsync(s -> {
// insert async call here...
return s + "-stage2";
})
.thenApply(s -> s + "-stage3")
.thenApplyAsync(s -> {
// insert async call here...
return s + "-stage4";
})
.thenAccept(System.out::println);
これはよりよいモデルだが、単一の値しかサポートしない。非同期シーケンスを扱うのはどうだろう?Java 8 Stream APIは要素のStreamに対して関数スタイルの操作手段を提供してくれるが、コレクション向けに構築されており、そこではコンシューマが利用可能なデータをプルする。プロデューサが要素をプッシュする、その中間に潜在的なレイテンシがある"ライブな"Stream向けではない。
そこにRxJavaやReactor、Akka Streamsなどのリアクティブライブラリが入ってきている。Java 8 Streamのように見えるが非同期シーケンス向けに設計されReactive Streamsのバックプレッシャーを追加している。これはコンシューマにパブリッシャの速度を制御させるためだ。
命令型から関数型、宣言型なスタイルへの切り替えは初めは"自然"とは感じず、適応まで時間がかかる。人生での他の物事でも同様に真実である。たとえば自転車への乗り方を学んだり、新しい言語を学んだりといったことである。立ち止まってはいけない。慣れていけばとてもよいことがやってくる。
命令型から宣言型への移行は明示的なループからJava 8のStream APIへの書き換えと同じような利点で比較できる。Java 8 Stream APIは"何が"なされるべきかを宣言する。"どのように"は宣言しないので、コードがより読みやすくする。リアクティブライブラリでも同様に、何がなされるべきかを宣言し、並行性やスレッド、同期をどのように扱うかは宣言しない。そのためコードはより少ないハードウェアリソースで効率的にスケールする。
大事なことを言い忘れていたが、この変更へのさらなる動機はJava 8でのラムダのシンタックスである。これは関数型、宣言型APIとリアクティブライブラリには極めて重要である。その上新しいプログラミングモデルを心に描かせてくれる。アノテーションがアノテーションを付与したRESTエンドポイントを構築させてくれたように、Java 8でのラムダ構文は関数型スタイルのルーティングとリクエストハンドラを構築させてくれる。
スタックの選択
Spring Frameworkは非同期でノンブロッキングなweb領域での最初のものではない。しかしあらゆる水準でJavaのエンタープライズアプリケーションの全体像と選択肢をもたらしている。選択肢は重要である。なぜならすべての既存のアプリケーションが変われるわけではなく、すべてのアプリケーションが変わる必要があるわけではないからだ。選択肢、そして一貫性と継続性は、マイクロサービスのアーキテクチャで役に立つ。そこでは各アプリケーションで独立した決定ができるからだ。
どんな選択肢があるのか見てみよう。
サーバ
長い間サーブレットAPIがサーバから依存性を取り除くデファクトスタンダードだった。しかし時間とともに代替案が出現し、イベントループの並行性とノンブロッキングI/Oの効率的なスケールを求めるプロジェクトはすでにサーブレットAPIとサーブレットコンテナの先を思い描いていた。
たしかにTomcatとJettyはより内部が効率的になるよう長年非常に進化してきた。20年間同じように変化しなかったものはサーブレットAPIを通じてブロッキングI/Oで使われているというその手段の方である。ノンブロッキングI/OはサーブレットAPI 3.1で導入されたが、大きな変更やブロッキングI/Oで構築されたコアやwebフレームワーク、アプリケーション仕様を変更する必要があるために採用されなかった。実際これはブロッキングI/OでのサーブレットAPIと、サーブレットAPIに依存しないNettyのような代替の非同期ランタイムとの選択では意味を持っていた。
Spring Framework 5でのリアクティブスタックはこの決定を先送りする。ブロッキングもリアクティブも持つことができる。Spring WebFluxアプリケーションはサーブレットコンテナ上で実行できるし、他のネイティブなサーバAPIにも適応できる。Spring Boot 2ではWebFlux starterはデフォルトでNettyを使うが、数行の設定で簡単にTomcatもしくはJettyに変更できる。リアクティブスタックはかつてはサーブレットAPIを通じてしかできなかった選択の度合いを復活させている。
アノテーションが付与されたコントローラ
Spring MVCのアノテーションベースのプログラミングモデル、多くの方が慣れ親しんでおられるが、これがサーブレットスタック (Spring MVC)とリアクティブスタック (Spring WebFlux) の両方でサポートされる。これが意味することはブロッキングとノンブロッキング、イベントループの並行性から選択できるにもかかわらず、webフレームワークのプログラミングモデルを維持できるということである。
リアクティブクライアント
リアクティブクライアントの利用でアプリケーションはリモートサービスの呼び出しを効率的に、同時に組み合わせられる。にもかかわらず明示的にスレッドを扱わなくて良い。この利点はサーバサイドで高い並行性があると非常に増幅される。
リアクティブクライアントはリアクティブスタックやSpring WebFluxでなくても使える。以下のコードはサーブレットスタックでもサポートされており、Spring MVCコントローラがリクエストを処理しリアクティブな型でレスポンスを返せることを示している。
@RestController
public class CarController {
private final WebClient carsClient =
WebClient.create("http://localhost:8081");
private final WebClient bookClient =
WebClient.create("http://localhost:8082");
@PostMapping("/booking")
public Mono<ResponseEntity<Void>> book() {
return carsClient.get().uri("/cars")
.retrieve()
.bodyToFlux(Car.class)
.take(5)
.flatMap(this::requestCar)
.next();
}
private Mono<ResponseEntity<Void>> requestCar(Car car) {
return bookClient.post()
.uri("/cars/{id}/booking", car.getId())
.exchange()
.flatMap(response -> response.toEntity(Void.class));
}
}
上記のサンプルで、コントローラはリアクティブな、ノンブロッキングなWebClient
を1つ目のリモートサービスからcarを取得するために使っている。それから2つ目のリモートサービスを通じてその中の1台を予約しようと試みている。最後にクライアントにレスポンスを返している。簡単で表現力が高いことに注目してほしい。非同期のリモート呼び出しが宣言でき、並行に (イベントループスタイルで) 実行できる。しかもスレッドや同期を扱う必要なくだ。
リアクティブライブラリ
アノテーションプログラミングモデルの1つの利点はコントローラメソッドのシグネチャが柔軟になることだ。アプリケーションはサポートされた広い範囲のメソッド引数と戻り値から選択できる。フレームワークは望まれた利用方法に適応する。これで複数のリアクティブライブラリをサポートするのが簡単になる。
サーブレットスタックとリアクティブスタックの両方で、ReactorとRxJavaの型がコントローラメソッドのシグネチャで使えるようサポートされている。これは設定可能なので、他のリアクティブライブラリも同様に組み込める。
関数型のwebエンドポイント
アノテーションを付与したコントローラを通じてコントローラのロジックを表現できること、これはJavaのwebフレームワークでは一般的な選択肢なのだが、これに加えて、Spring WebFluxはリクエストのルーティングとハンドリングに対してラムダベースで軽量な関数型プログラミングモデルもサポートする。
関数型のエンドポイントはアノテーションを付与したコントローラとはまったく異なる。アノテーションを使う場合、フレームワークに何がなされるべきかを記述し、あなたのためにできるだけ多くの作業をさせる。ハリウッドの原則を思い出してほしい。"連絡してこないで、こちらから連絡するから"である。一方、関数型プログラミングモデルはアプリケーションに対して利用できるヘルパーユーティリティの小さなセットから成る。リクエスト処理を開始から終了まで運ぶためだ。リクエストのルーティングとハンドリングの短いスニペットは以下のような感じに見えるだろう。
RouterFunction< > route = RouterFunctions.route(POST("/cars/{id}/booking"),
request -> {
Long carId = Long.valueOf(request.pathVariable("id"));
Long id = ... ; // Create booking
URI location = URI.create("/car/" + carId + "/booking/" + id);
return ServerResponse.created(location).build();
});
たとえばReactor NettyでのNettyを動作させる方法の1つはこうだ。
HttpHandler handler = RouterFunctions.toHttpHandler(route);
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer server = HttpServer.create("localhost", 8080);
server.startAndAwait(adapter);
関数型のエンドポイントとリアクティブライブラリは相性がいい。2つとも関数型で宣言型スタイルのAPIで構築されているからだ。
Webスタックのアーキテクチャ
サーブレットとリアクティブスタックの詳細を見ていこう。
サーブレットスタックはクラシックなサーブレットコンテナとサーブレットAPIである。Spring MVCをwebフレームワークとしている。初期ではサーブレットAPIは"リクエストごとにスレッドがある"というモデルで構築されていた。すなわちシングルスレッドでフィルタ-サーブレットのチェーンをすべて通過するようにしていた。必要に応じて途中ブロッキングしていた。そのうちにwebアプリケーションのニーズと期待の変化に適応し、いくつかオプションが追加された。
1997 | 1.0 | Initial version |
… | ||
2009 | 3.0 | Async Servlet |
2013 | 3.1 | Servlet Non-Blocking I/O |
3.0にある非同期サーブレット機能でフィルタ-サーブレットチェーンをすべて通過したあとにさらに処理ができるようレスポンスを開いたままにすることができるようになった。Spring MVCはこの機能の上に大規模に構築されておりこれがアノテーションを付与したコントローラでリアクティブな戻り値の型が可能になった理由である。
あいにく、3.1でのノンブロッキングI/Oの機能は命令型のブロッキングな構文を中心に構築された既存のwebフレームワークの仕様に統合できなかった。Spring MVCでサポートされておらず、代わりにサーブレットAPIと他のサーバの両方をサポートするノンブロッキングなwebフレームワークの仕様の土台の上にSpring WebFluxが作られた理由はこのためである。
リアクティブスタックはTomcatでもJettyでも、サーブレット3.1のコンテナで動作する。各サーバには普及しているリアクティブストリームAPIが採用されており、HTTPリクエストをハンドリングする。その土台の頂点に少し高レベルではあるが、依然として汎用のWebHandler APIがあり、これはサーブレットAPIと比較できるが非同期でノンブロッキングな仕様である。
以下の図は2つのスタックを並べている。
TomcatとJettyは2つともサポートしているが、それぞれ違うAPIを通じて利用されていることに注目してほしい。サーブレットスタックではブロッキングI/OでのサーブレットAPIを通じて利用されている。リアクティブスタックではサーブレットAPIをまったく露出させずにサーブレット3.1のノンブロッキングI/Oを通じて利用している。このうちの大部分がアプリケーションでの利用ではブロッキングで同期のままである (たとえばリクエストパラメータ、マルチパートリクエストなど) 。
リアクティブでノンブロッキングなバックプレッシャー
サーブレットスタックとリアクティブスタックの両方でアノテーションを付与したコントローラをサポートしているが、並行モデルと前提に重大な違いがある。
サーブレットスタックではアプリケーションにおいてブロックは許されている。サーブレットコンテナがアプリケーションで起こり得るブロッキングを緩和するために大きなスレッドプールを使うのはこれが理由だ。この前提はFilter
とServlet
の仕様に反映されており、両方とも命令型でvoid
を返す。ブロッキングのInputStreamとOutputStreamにおいても同様だ。
リアクティブスタックではアプリケーションは決してブロックしてはならない。イベントループ上で少量の固定されたスレッドの1つで呼び出されるからだ。すぐにサーバ全体をブロックしてしまうことになる。この前提はWebFilter
とWebHandler
の仕様に反映されている。これらはMono<Void>
を返す。これは0または1の非同期の値のためのReactorの型だ (ここでは値がないので、単に成功したか失敗したかだ)。リクエストとリクエストボディに対するリアクティブな型においても同様だ。
リクエストボディはFlux<DataBuffer>
を通じてアクセスする。これは非同期シーケンスのためのReactorの型だ。これが意味することは、データチャンク全体を利用できるようになったときに一度に処理する準備ができていなければならないということだ。恐ろしいことのように聞こえるかもしれないが、バイトストリームをオブジェクトストリームに変換するビルトインのコーデックがある。
たとえば、クライアントサイドからJSONストリームをアップロードするとする。
// 毎秒新しいcarを製造する
Flux<Car> body = Flux.interval(Duration.ofSeconds(1)).map(i -> new Car(...));
// サーバにライブストリーミングデータをpostする
WebClient.create().post()
.uri("http://localhost:8081/cars")
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(body, Car.class)
.retrieve()
.bodyToMono(Void.class)
.block();
サーバサイドではSpring WebFluxのコントローラがストリームを取り込める。そしてSpring Dataリアクティブリポジトリを使いそれをデータストアに挿入できる。
// サーバはストリームにされたデータをそれが来たときにpostする (下の議論を参照)
@PostMapping(path="/cars", consumes = "application/stream+json")
public Mono<Void> loadCars(@RequestBody Flux<Car> cars) {
return repository.insert(cars).then();
}
ストリームは長期間継続できる。必要であれば数日も。効率的に処理され、余分なスレッドやメモリは保持しない。このシナリオでSpring WebFluxとSpring Dataはリアクティブストリームをサポートている。そのため上記のコードはデータストアからHTTPランタイムまで流れているリアクティブストリームのバックプレッシャーシグナルとともに処理パイプラインを展開する。データストアはデータチャンクをHTTPリクエストから読み込みオブジェクトに変える速度を効率的に制御する。
carを取り込むコントローラが制限のある追尾可能なMongoDBのコレクションに登録すると仮定しよう。他のクライアントはコレクションを監視するためJSONストリームを要求できる。
WebClient.create().get()
.uri("http://localhost:8081/cars")
.accept(MediaType.APPLICATION_STREAM_JSON)
.retrieve()
.bodyToFlux(Car.class)
.doOnNext(car -> {
logger.debug("Received " + car));
//...
})
.block();
サーバサイドではコントローラは以下のようにJSONストリームを供給できる。
@GetMapping(path = "/cars", produces = "application/stream+json")
public Flux<Car> getCarStream() {
return this.repository.findCars();
}
今回リアクティブストリームのバックプレッシャーシグナルは反対方向に流れている。HTTPランタイムからデータストアに向かっている。HTTPランタイムはオブジェクトをデータストアから取得し、JSONにシリアライズし、HTTPレスポンスにデータチャンクを書き込む速度を効率的に制御する。
上記のシナリオはリアクティブスタック上ではFlux<Car>
を受け取るもしくは返すことでとても自然にデータを取り込んだりストリームにしたりできることを描いている。通常の (有限な) コレクションを扱うのではどうだろう?Flux
は有限か無限かにかかわらずどんなデータシーケンスでもサポートする。なので変更の必要はない。
依然としてFlux
を返し、webフレームワークはメディアタイプをチェックして適切に対応する。
以下はJSON配列をコンテントタイプ"application/json"でレンダリングしている。
@GetMapping(path = "/cars", produces = "application/json")
public Flux<Car> getCars() {
return this.repository.findAll();
}
"application/json"であり、これはストリーミングでないメディアタイプなので、webフレームワークはFlux
が有限のコレクションを表していると仮定し、全アイテムを要求してリストにそれらを集めるFlux.collectoToList()
を使い、レスポンスにそのコレクションを書き込む。
今までこの節ではリアクティブスタックについて話していた。サーブレットスタックはブロッキングI/Oを当てにしており、それゆえノンブロッキング、もしくはストリーミングの@RequestBody
はサポートしていない。しかしコントローラメソッドで非同期処理はできる。サーブレット3.0の非同期リクエスト機能のおかげだ。そのためSpring MVCのコントローラはリアクティブクライアントを呼び出しレスポンスハンドリングへリアクティブな型を返せる。
"application/json"では、サーブレットスタックではSpring MVCもリストにFluxアイテムを集め、結果をJSON配列としてレスポンスに書き込む。
@GetMapping("/cars")
public Flux<Car> getCars() {
return this.repository.findAll();
}
"application/stream+json"や他のストリーミングメディアタイプでは、Spring MVCもリアクティブストリームのバックプレッシャーをアップストリームソースに適用する。
@GetMapping(path = "/cars", produces = "application/stream+json")
public Flux<Car> getCarStream() {
return this.repository.findCars();
}
しかし、リアクティブスタックと異なり、サーブレットスタック上での書き込みはブロッキングであり、別のスレッドで実行される。書き込みの完了はアップストリームソースへ要求を伝えるために使われる。
サーブレットスタック上でも効率的となるよう、Spring MVCはリアクティブと非同期のリクエストハンドリングの利点を既存のアプリケーションに対してできる限り拡大している。
選択
Spring MVCとSpring WebFlux、どちらを使うべきか
まったくもっともな質問であるのだが、完全な二者択一ではない。両方ともメンテナンスされ、ともに進化し、共存させられる。一貫性と継続性を考慮して設計されており、一方からのアイデアとフィードバックは他方に利点をもたらす。Spring MVCとSpring WebFlux両方あるものとして考えるというのがより正確であり、一緒に可能性を広げていくものだ。
Spring MVCはクラシックなサーブレットスタック基盤上にシンプルで命令型のモデルを提供する。ブロッキングに依存するものでもノンブロッキングに依存するものでも一緒に使える。Spring WebFluxはイベントループの並行性と代替である関数型プログラミングモデルの利点を提供する。その途中に一貫性と継続性のため私たちが目指している場所が共通点としてある。
アプリケーションがスケールの問題に悩まされておらず、水平スケールのコストが予算の範囲内なら、変更する必要はない。命令型はコードを書くもっとも簡単な方法であり、大部分のアプリケーションはこの領域の境界には近づくがしばらくの間そのままであると私は考えている。
もちろん、命令型は簡単である。簡単でなくならない限りそうだ。モダンなアプリケーションではリモートサービスを呼び出すというのはよくある。いくつかのケースでそれを命令型のスタイルで同期的に実行するのは大丈夫かもしれないが、より少ないハードウェアでよりスケールさせたいなら並行性と非同期性を受け入れる必要がある。一度その世界に入ったなら、リアクティブプログラミングは総合的で効果的な解決策を提供する。多くのアプリケーションがSpring MVCでWebClientや他のリアクティブライブラリ (たとえばSpring Dataリアクティブリポジトリ) を使うと私は考えている。比較的小さな変更で済むがかなり利点があるからだ。
リアクティブスタックとSpring WebFluxはさまざまな方法で訴えてくるかもしれない。みなさんは伝統的なブロッキングI/Oが高い並行性と変更可能なレイテンシがあるシナリオに対して提供できないようなスケールの類いを求めているのだろう。もしくは水平スケールと垂直スケールの価格差があまりにあるのだろう。たとえばSpring Cloud Gatewayでは、Spring WebFlux上でビルドする選択をするというのが明白である。高い並行性と効率性のために最適化する必要があるからだ。
リアクティブスタックは、そのリアクティブな処理パイプラインを使うと、非常にストリーミングのシナリオに適したものとなる。これは多くのアプリケーションで要求されることだ。リクエストとレスポンスの内容の両方のため、リアクティブストリームのバックプレッシャー、そしてノンブロッキングな書き込みによってこうしたことがサポートされている。
Spring WebFluxでのみ利用できる関数型webプログラミングモデルを単純に提供するためにリアクティブスタックを選択したという人もいる。関数型エンドポイントはKotlinのアプリケーションでとても評判がよく、それらのためにWebFluxはリクエストのルーティングでKotlin DSLを提供している。その単純さと透過性 (12個ほどのクラスしかない) のおかげで、関数型webプログラミングモデルは比較的小さく焦点を絞ったマイクロサービスによくフィットするかもしれない。
demo-reactive-springリポジトリにはこの記事で使ったソースコードがある。
著者について
Spring Frameworkチームのコミッタとして、Rossen Stoyanchev氏のコントリビュートは3世代に渡るSpring MVCの進化とSpring WebFluxの開始からリリースまでの構築にといったことにまで及んでいる。