本文へスキップ
Starone

アーキテクチャ

マイクロサービスとモジュラーモノリスの比較

マイクロサービスは、分散システムという代償を払って組織とスケーリングの問題を解決します。モジュラーモノリスは、その代償なしに設計上の利点の大半を保てます。具体的な圧力が切り出しを正当化するまでは、これが私たちの標準の出発点です。

· 4分で読めます · Starone エンジニアリング

この議論は、古いものと新しいものの対立として語られがちです。しかし、境界をどこで強制するかという問いとして捉えるほうが適切です。モジュラーモノリスでは、業務機能間の境界はコンパイラとビルドによって強制されます。マイクロサービスでは、ネットワークによって強制されます。ネットワークはより強固な壁ですが、はるかに高くつく壁でもあります。

新規システムに対する私たちの標準は、モジュラーモノリスです。厳格な内部モジュールを持つ一つのデプロイ単位として構成し、後からモジュールを切り出せるように作ります。モジュールを独立したサービスに移すのは、名指しできる圧力がそれを求めたときであり、それより前ではありません。

マイクロサービスで実際に得られるもの

利点は現実のもので、かつ具体的です。各サービスは独自のスケジュールでデプロイでき、チーム間のリリース調整が不要になります。各サービスはそれぞれの負荷特性に応じてスケールできます。呼び出し側が障害を許容するように作られていれば、障害を封じ込められます。ワークロードが求める場合には、サービスごとに異なるランタイムやデータストアを使えます。

注目すべきは、これらの大半が組織に関するものだという点です。独立したデプロイが意味を持つのは、そうしなければ多数のチームが一つのリリーストレインの後ろに並ぶことになる場合です。コンウェイの法則がその仕組みを説明しています。システムは、それを作る組織のコミュニケーション構造を映すようになります。マイクロサービスはかなりの部分において、多数のチームが互いをブロックせずに作業するための道具です。

その代償

関数呼び出しだったものがすべてネットワークリクエストになり、遅くなることも、失敗することも、呼び出し元が知らないうちに成功していることもあります。このたった一つの変化が、一連の責務を持ち込みます。

  • すべてのサービス間呼び出しにおけるタイムアウト、リトライ、サーキットブレーカー、冪等性
  • サービスをまたぐトランザクションは使えないため、整合性はSaga、アウトボックス、補償処理で組み立て直します。
  • すべてのインターフェースに対する、バージョン管理された契約と後方互換性、そしてサービス間でのテスト
  • 一つのリクエストに何が起きたかを知るためだけに必要な、分散トレーシング、ログの集約、サービスごとのダッシュボード
  • サービスごとのデプロイパイプライン、ランタイム設定、シークレット、オンコールの責任

分散モノリス

最悪の結果は、これらのコストを払いながら利点を何も得られないシステムです。兆候はよく知られています。一緒にデプロイしなければならないサービス群、同じデータベーステーブルを読み書きする複数のサービス、アップグレードのたびに全チームに一斉リリースを強いる共有ライブラリ、そして一つのエンドポイントの可用性が他の十数個すべての稼働に依存するほど深いリクエストチェーンです。

これはたいてい、ドメインを理解する前に分割した場合に起こります。早い段階で引いた境界は誤った場所に引かれます。そしてサービス間の境界を動かすことは、モジュール間の境界を動かすよりはるかに困難です。データ移行と、チームをまたぐ契約変更を伴うからです。モノリスであれば、変更コストがまだ低いうちに、リファクタリングで境界を修正できます。

モノリスをモジュラーにするもの

モジュラーモノリスは、フォルダが整理された大きなコードベースのことではありません。モジュールは強制されなければならず、そうでなければデリバリーの圧力のもとで崩れていきます。

  • 各モジュールは小さな公開インターフェースだけを公開し、それ以外はすべて隠蔽します。手段はパッケージの可視性、ビルドモジュールの分離、言語のモジュールシステムです。
  • 依存関係のルールはビルドでチェックします。Javaエコシステムであれば ArchUnitやSpring Modulithといったツールを使い、不正なimportがあればパイプラインを失敗させます。
  • 各モジュールは自身のテーブルを所有し、できれば専用のスキーマに置きます。他のモジュールのデータを直接クエリするモジュールがあってはなりません。
  • モジュール間の通信は、明示的なインターフェースかインプロセスイベントを介します。モジュールを切り出せば、これらはネットワーク呼び出しやブローカーのメッセージになります。

単一デプロイ単位の限界

モノリスには現実の制約があり、それは明言しておくべきです。すべてのモジュールがリリースを共有するため、フィーチャーフラグと十分なテストカバレッジがなければ、一つの不良な変更が全員の作業をロールバックさせかねません。すべてのモジュールがプロセスを共有するため、一つのモジュールのメモリリークや暴走したクエリが他に影響します。ビルドとテストの時間はコードベースとともに伸び、継続的な手入れが必要です。全体が一緒にスケールするため、一つのモジュールが負荷の大半を担っている場合にはリソースが無駄になります。

それでも水平スケーリングは可能です。ロードバランサーの背後に置いたステートレスなモノリスは相当なトラフィックを処理でき、通常はアプリケーション層よりずっと先にデータベースが限界になります。これは、同じデータベースを共有するマイクロサービスにもそのまま当てはまります。

切り出しを正当化する圧力

私たちがモジュールをサービスとして切り出すのは、次のいずれかが実証でき、かつ利点が運用上のオーバーヘッドを上回る場合です。

  • 負荷特性が他と大きく異なる場合。バースト性のある取り込み経路や、計算負荷の高い推論ワークロードなどです。
  • 異なるランタイム、ハードウェア、データストアを必要とする場合
  • 可用性の目標が異なる場合、またはコンプライアンス上の分離が必要な場合。カードデータを扱うコンポーネントが該当します。
  • システムに関わるチームが増え、リリース調整がボトルネックになっている場合
  • 変更の頻度が他と大きく異なり、そのリリースが不釣り合いに大きなリスクを伴う場合

サービスから始めるべき場合

マイクロサービスから始めるのが正しい判断となるのは、すでに多数のチームがあり、デプロイとオブザーバビリティの標準経路が整備された成熟したプラットフォームを持ち、ドメインをよく理解している組織です。長年運用してきたシステムを作り直す場合に、こうした条件が揃うことがよくあります。そのような状況では、コストはすでに支払い済みで、境界も分かっています。

それ以外のすべての組織にとって、モジュラーモノリスは選択肢を残しておく手段です。きれいなモジュール境界に沿った切り出しは、ストラングラーパターンに従います。一つの機能を新しいサービスにルーティングし、そのデータを移し、古いコードを廃止します。それを可能にする規律、つまりデータを所有する厳格なモジュールは、分割が実際に行われるかどうかにかかわらず持っておく価値があります。

同様の課題に取り組んでいる方へ

ぜひ意見を交換させてください。この記事を書いたエンジニアが直接お話しします。