Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

マイクロサービス時代のアプリケーションサーバ実装について

4年前の下書きが出てきたので、供養のために置いておきます。


SpringOne Platform 2016KeynoteとSessionを、特にProject Reactor周りについて確認した。

これらに最近考えていたことを加えて、マイクロサービスやサーバサイドリアクティブについて(利点や必要性は一旦論点から外したうえで)実現するためのあるべき論をざっくり整理したい。プライベートプロジェクトで検証済みではあるがチーム開発でも通用するかは不明である。

なお簡単のためRxJava用語のみ記述するが、Project Reactor等の用語で置き換えても良い(SingleMono, ObservableFlux)。

あるべき論

APIサーバ&BFF共通

  • Repository(DAO)
    • 境界を超えないデータストアに対するアクセスを担うレイヤのこと。
    • Repositoryのメソッドは非同期I/Oを呼び出すことが期待される。よってSingleあるいはObservableを返すべき。
    • 戻り値がない場合(例えば更新処理)でも、内部の処理が正常に終了したかどうかを表現するために Single<Void> を返すべき。
    • 非同期I/Oを呼び出さないことが明らかな場合のみ、同期的にインスタンスを返しても良い。
  • Service(ビジネスロジック
    • ServiceのメソッドはDAOを通じて、あるいは境界外へアクセスするクライアントを通じて非同期I/Oを呼び出すことが期待される。よってSingleあるいはObservableを返すべき。
    • 戻り値がない場合(例えばジョブキューにジョブを登録する場合)でも、内部の処理が正常に終了したかどうかを表現するために Single<Void> を返すべき。
    • Repositoryから渡されたSingle/Observableのエラー処理(ログ出力等)は、
      • その Single/Observable がControllerから利用されるのであれば、Service内で行っても行わなくても良い。
      • その Single/Observable がControllerから利用されないのであれば、Service内で行う。
    • Serviceは冪等性を保つ必要がある。UUID version 1でID採番する場合などは、処理結果にランダム失敗した性が含まれるので扱いに注意する(Eventual Consistencyを意識する)。
  • Client(境界外アクセス)
    • RestTemplate等を直接呼び出す実装が多く見受けられるが、サーキットブレーカーや監視や自動テスト容易性を考えると一枚抽象化を入れた方が良いと思われる。
    • 各マイクロサービス提供者がEntityと共にユーザに提供する。Serviceから呼び出され、HTTP等によるRPCを行う。よってSingleあるいはObservableを返すべき。
    • コレオグラフィ優先採用するならば、基本的には使用しない方が良い。
  • EventBus
    • Serviceによって購読ないしpublishされる。
  • Controller
    • モデルとしてSingleObservableをテンプレートエンジンに渡す。テンプレートエンジンの機能に応じて、SingleObservableを変換するかもしれない。CompletableFutureあたりが有望か。
    • 通信先サービスが正常動作しなかった場合、CircuitBreakerが落ちている場合のUIも考えておく。

BFFサーバ の実装

WebAPIを多数コールして1つのレスポンスにまとめるには、大きく分けて2つ実装パターンが考えられる。

  1. サーバ内ですべてのサービスからのレスポンスを束ね、1枚のHTMLページやJSONデータをつくり上げる
  2. サービスから逐次受け取ったデータをクライアントに横流しして、クライアント側で組み立てる(Server-sent Event, WebSocket, Big pipe etc.)
    • 2020年夏時点ではJSON listをうまく扱うJS手法はまだなさそう?Streams APIの安定化を待つ必要があるのでは。

完全な2だとBFFを作る意味が無いので、1を主体として見た目に影響の薄い部分で2のような遅延処理をを採用することが多いはず。しかし1のレスポンスを束ねる部分で各サービスに線形的に問い合わせてはレイテンシ低下が容易に発生するため、非同期I/Oを使った並行問い合わせが必要になる。

サーバ間連携

  • メッセージキューには永続性が必要
    • コレオグラフィを志向してサービスを実装するためには「イベントを永続化してサブスクライバが何度も読みに行く」か「サービスを冪等にしてパブリッシャが何度もリトライする」かのどちらかになる。後者はパブリッシャがイベント発火によって行われる処理を知っている必要があるので、基本的には前者が好ましいと思われる(Webアプリだとパブリッシャが何度もリトライするような書き方だとユーザへのレスポンスが遅れるのも痛い)。
      • 2020年追記:一見意味不明だが、ここで言うサブスクライバとパブリッシャはサービス単位を指している(同一JVM内にあるインスタンスを比較しているわけではない)と思われる。前者はイベントが永続化することで、同じイベントを何度も聞きに行くケースのこと。複数種類のサブスクライバがいたり、サブスクライバが常時起動でなかったりするとこの必要性が生じるはず。後者は非同期処理を依頼する、例えば送信処理や投機的実行のケースで事実上リアルタイム性を求めているケースのこと。当時使ってたVertxがデフォルト設定で利用するhazelcastのイメージに引っ張られてそう。
    • このため、キューに記録されたメッセージは1回以上必ず読まれること、つまりロストがないことを保証したい。そのためにはキューに永続性が必要。
  • サブスクライバごとにイベントの既読管理を行う
    • コレオグラフィを採用するならば、1イベントにサブスクライバが多数いることも予想されるため、メッセージ(イベント)がConsumeされたかどうかはサブスクライバごとに管理する必要がある。
    • 例えばKafkaなら、イベント種別=Topic、サブスクライバ=Consumer Groupとすることでサブスクライバごとの管理が可能。

備考

RxJava v2, reactive-streams そして Java9 Flow API について

現状では気にしないので良さそうな印象。

operatorすら用意されていないシンプルなreactive-streamsが、それだけ見ていれば詳細実装を気にしないで良いレベルのインタフェースになることは当面ないだろうし、そもそもそこを目指していないように見受けられる。ライブラリ提供者にとっては各プラットフォームを公平にサポートするための良いインタフェースになるだろうが、サービス開発者にとってはRxJavaやProject Reactorに直接依存・利用したほうがコードもシンプルになるし利用できる機能も多く便利なはず。

参考: