モノリスのマイクロサービスへのリファクタリング

このリファレンス ガイドは、マイクロサービスの設計、構築、デプロイに関する 4 部構成シリーズの第 2 部です。このシリーズでは、マイクロサービス アーキテクチャのさまざまな要素について説明します。このシリーズには、マイクロサービス アーキテクチャ パターンの利点と欠点、およびその適用方法に関する情報が含まれています。

  1. マイクロサービスの概要
  2. モノリスのマイクロサービスへのリファクタリング(このドキュメント)
  3. マイクロサービス設定でのサービス間通信
  4. マイクロサービス アプリケーションの分散トレース

このシリーズは、モノリシック アプリケーションをマイクロサービス アプリケーションにリファクタリングするための移行を設計して実装するアプリケーション デベロッパーとアーキテクトを対象としています。

モノリシック アプリケーションをマイクロサービスに変換するプロセスは、アプリケーションのモダナイゼーションの方法の一つです。アプリケーションのモダナイゼーションを実現するには、すべてのコードを同時にリファクタリングしないことをおすすめします。代わりに、モノリシック アプリケーションを段階的にリファクタリングすることをおすすめします。アプリケーションを段階的にリファクタリングする際は、マイクロサービスで構成される新しいアプリケーションを段階的に構築し、モノリシック アプリケーションとともに実行します。このアプローチは、ストラングラー フィグ パターンとも呼ばれます。時間が経つと、モノリシック アプリケーションによって実装される機能の量が減少し、完全になくなるか、別のマイクロサービスになります。

機能をモノリスから分離するには、機能のデータ、ロジック、ユーザー向けコンポーネントを慎重に抽出し、新しいサービスにリダイレクトする必要があります。ソリューション空間に移行する前に、問題空間を十分に把握しておく必要があります。

問題空間を把握することで、ドメイン内で適切な分離レベルを提供する自然な境界を理解できます。ドメインを完全に理解するまでは、小規模なサービスではなく、より大きなサービスを作成することをおすすめします。

サービスの境界の定義は反復的なプロセスです。このプロセスは大変な作業であるため、メリットに対する分離コストを継続的に評価する必要があります。モノリスの分離方法を評価する際に役立つ要素を以下に示します。

  • すべてのリファクタリングを一度に行わないでください。サービス分離の優先順位を決めるため、費用対効果を評価します。
  • マイクロサービス アーキテクチャ内のサービスは、技術的な懸念事項ではなく、ビジネス上の懸念に基づいて編成されます。
  • サービスを段階的に移行する場合、明確に定義された API コントラクトを経由するように、サービスとモノリス間の通信を構成します。
  • マイクロサービスでは多くの自動化が必要になります。継続的インテグレーション(CI)継続的デプロイ(CD)、一元的なロギング、モニタリングを事前に検討してください。

以下のセクションでは、サービスを分離してモノリシック アプリケーションを段階的に移行するためのさまざまな戦略について説明します。

ドメイン ドリブン設計による分離

マイクロサービスは、データアクセスやメッセージングなどの水平レイヤではなく、ビジネス機能を中心に設計する必要があります。マイクロサービスには疎結合と高度な機能的結束性が必要です。他のサービスを同時に更新することなく 1 つのサービスを変更できる場合、マイクロサービスは疎結合になります。ユーザー アカウントの管理や支払い処理などの単一の明確な目的がある場合、マイクロサービスに結束性があります。

ドメイン ドリブン設計(DDD)を行うには、アプリケーションを作成するドメインを十分に理解する必要があります。アプリケーションをよく知っている人物(ドメイン エキスパート)は、アプリケーションの作成に必要なドメインについてよく理解しています。

DDD の手法を既存のアプリケーションに遡って適用するには、次のようにします。

  1. ユビキタスな言語(すべての関係者の間で共有される一般的な言葉)を特定します。デベロッパーは、技術者ではないユーザーが理解できる用語をコードで使用する必要があります。コードで達成しようとしていることには、会社のプロセスが反映されていなければなりません。
  2. モノリシック アプリケーションに関連するモジュールを特定し、それらのモジュールに共通の表現を適用します。
  3. 制限付きコンテキストを定義し、特定のモジュールに責任が明確に定義された明示的な境界を適用します。この制限付きコンテキストは、より小さなマイクロサービスにリファクタリングされる候補となります。

次の図は、既存の e コマース アプリケーションに制限付きコンテキストを適用する方法を示しています。

制限付きコンテキストのアプリケーションへの適用

図 1. アプリケーションの機能が、サービスに移行する制限付きコンテキストに分割されている。

図 1 では、e コマース アプリケーションの機能が制限付きコンテキストに分割されています。これらのコンテキストは次のようにサービスに移行されます。

  • 注文管理とフルフィルメント機能は、次のカテゴリにバインドされます。
    • 注文管理機能は注文サービスに移行されます。
    • ロジスティクスの配送管理機能は配送サービスに移行されます。
    • インベントリ機能はインベントリ サービスに移行されます。
  • 会計機能は 1 つのカテゴリにバインドされます。
    • ユーザー、販売者、サードパーティの機能は 1 つにバインドされ、アカウント サービスに移行されます。

移行するサービスの優先順位を決める

サービスを切り離すための出発点として、モノリシック アプリケーションの疎結合モジュールを特定します。疎結合モジュールは、マイクロサービスに変換する最初の候補として選択できます。各モジュールの依存関係を分析するため、次の点を確認します。

  • 依存関係の種類 - データや他のモジュールとの依存関係
  • 依存関係の規模: 特定されたモジュールの変更が他のモジュールに与える影響

データへの依存度が高いモジュールの移行は簡単ではありません。機能を移行してから関連データを移行する場合は、複数のデータベースに対して一時的にデータの読み書きを行う可能性があります。そのため、データの整合性と同期の問題について検討する必要があります。

必要なリソース要件がそれぞれ異なるモノリシック モジュールを含むモジュールを抽出することをおすすめします。たとえば、モジュールにインメモリ データベースがある場合、それをサービスに変換して、より多くのメモリを搭載したホストにデプロイできます。特定のリソース要件を持つモジュールをサービスに変換すると、アプリケーションのスケーリングを大幅に簡素化できます。

運用の観点からは、モジュールを独自のサービスにリファクタリングすることは、既存のチーム構造を調整することを意味します。アカウンタビリティを明確にする最善の方法は、サービス全体を所有している小さなチームを強化することです。

移行におけるサービスの優先順位付けに影響を与える要因としては、ビジネス上の重要性、包括的なテスト カバレッジ、アプリケーションのセキュリティ体制、組織の同意などもあります。このシリーズの最初のドキュメントで説明されているように、評価に基づいてリファクタリングによって得られるメリットでサービスのランク付けを行うことができます。

モノリスからのサービス抽出

理想的なサービス候補を特定したら、マイクロサービスとモノリシック モジュールの両方を共存させる手段を特定する必要があります。この併用を管理する方法の 1 つは、モジュール間の連携に役立つプロセス間通信(IPC)アダプタを導入することです。時間の経過とともに、マイクロサービスが引き受ける負荷が増えていき、モノリシック コンポーネントが少なくなっていきます。これにより、バグやパフォーマンスの問題を段階的に検出できるので、モノリシック アプリケーションから新しいマイクロサービスに移行する際のリスクを軽減できます。

次の図は、IPC アプローチの実装方法を示しています。

モジュール間の連携に役立つ IPC のアプローチの実装。

図 2. IPC アダプタがモノリシック アプリケーションとマイクロサービス モジュール間の通信を調整。

図 2 では、モジュール Z がモノリシック アプリケーションから抽出するサービスの候補です。モジュール X と Y はモジュール Z に依存します。マイクロサービス モジュール X と Y は、モノリシック アプリケーション内の IPC アダプタを使用して、REST API 経由でモジュール Z と通信します。

このシリーズの次のドキュメントであるマイクロサービスの設定におけるサービス間通信では、ストラングラー フィグ パターンと、モノリスからのサービスの解体方法を示しています。

モノリシック データベースの管理

通常、モノリシック アプリケーションには独自のモノリシック データベースがあります。マイクロサービス アーキテクチャの原則の 1 つは、マイクロサービスごとに 1 つのデータベースを作成することです。したがって、モノリシック アプリケーションをマイクロサービスにモダナイズする場合は、識別したサービス境界に基づいてモノリシック データベースを分割する必要があります。

モノリシック データベースを分割する場所を決定するには、まずデータベース マッピングを分析します。サービス抽出分析の一環として、作成する必要があるマイクロサービスについて、いくつかの分析情報を収集しました。同じ方法でデータベースの使用状況を分析し、テーブルやその他のデータベース オブジェクトを新しいマイクロサービスにマッピングします。このような分析では、SchemaCrawlerSchemaSpyERBuilder などのツールが役立ちます。テーブルなどのオブジェクトのマッピングは、想定されるマイクロサービスの境界をまたぐデータベース オブジェクトとの結合を理解する際に役立ちます。

しかし、データベース オブジェクト間を明確に分離できない可能性があるため、モノリシック データベースの分割は容易ではありません。また、データの同期、トランザクションの整合性、結合、レイテンシなどの問題も考慮する必要があります。次のセクションでは、モノリシック データベースの分割で、こうした問題に対応する際に役立つパターンについて説明します。

参照テーブル

モノリシック アプリケーションの場合、他のモジュールのテーブルに SQL で結合し、別のモジュールから必要なデータを取得するのが一般的です。次の図は、前述の e コマース アプリケーションの例を使って、この SQL 結合アクセスのプロセスを示しています。

1 つのモジュールが SQL 結合で別のモジュールのデータにアクセスしている。

図 3. 1 つのモジュールが別のモジュールのテーブルにデータを結合している。

図 3 では、注文モジュールが注文を商品テーブルと結合するために、注文モジュールで product_id 外部キーを使用しています。

ただし、モジュールを個別のサービスとして分解する場合は、注文サービスが商品サービスのデータベースを直接呼び出して結合オペレーションを実行しないようにすることをおすすめします。以降のセクションでは、データベース オブジェクトを分離するためのオプションについて説明します。

API によるデータの共有

コア機能またはモジュールをマイクロサービスに分割すると、通常は API を使用してデータを共有して公開します。参照されるサービスは、次の図のように呼び出し元のサービスが必要とする API としてデータを公開します。

データが API を介して公開される。

図 4. サービスが API 呼び出しを行い、別のサービスからデータを取得する。

図 4 では、注文モジュールが API 呼び出しを使用して、商品モジュールからデータを取得します。この実装では、ネットワークとデータベースの呼び出しが増加するため、パフォーマンス上の問題があることは明らかです。ただし、データサイズが制限されていれば、API によるデータ共有はうまく機能します。また、呼び出されたサービスから返されるデータの変化率がわかっている場合は、呼び出し元にローカル TTL キャッシュを実装し、呼び出されたサービスへのネットワーク リクエストを減らすことができます。

データの複製

2 つのマイクロサービス間でデータを共有するもう 1 つの方法は、依存するサービス データベースにデータを複製することです。データ レプリケーションは読み取り専用で、いつでも再構築できます。このパターンでは、サービスの一貫性が高まります。次の図は、2 つのマイクロサービス間のデータ レプリケーションの仕組みを示しています。

データがマイクロサービス間で複製される。

図 5. サービスのデータは、依存するサービス データベースに複製される。

図 5 では、商品サービス データベースが注文サービス データベースに複製されています。この実装により、注文サービスは、商品サービスを繰り返し呼び出すことなく、商品データを取得できます。

データ レプリケーションを構築するには、マテリアライズド ビュー、変更データ キャプチャ(CDC)、イベント通知などの手法を使用できます。複製されたデータは結果整合性になりますが、データの複製で遅れが生じるため、古いデータが取り込まれるリスクがあります。

構成としての静的データ

国コードやサポートされている通貨などの静的データは、変更されるまでに時間がかかります。このような静的データを構成としてマイクロサービスに挿入できます。最新のマイクロサービスとクラウド フレームワークでは、構成サーバー、Key-Value ストア、Vault を使用して、このような構成データを管理する機能が提供されています。こうした機能を宣言的に組み込むことができます。

共有可能な可変データ

モノリシック アプリケーションには、共有変更可能状態という共通のパターンがあります。共有変更可能状態の構成では、次の図に示すように複数のモジュールが単一のテーブルを使用します。

共有変更可能状態の構成により、1 つのテーブルが複数のモジュールで共有されている。

図 6. 複数のモジュールが 1 つのテーブルを使用する。

図 6 では、e コマース アプリケーションの注文、支払い、配送の各機能がショッピング ジャーニー全体で同じ ShoppingStatus テーブルを使用し、お客様の注文ステータスを維持しています。

共有変更可能状態のモノリスを移行する場合、ShoppingStatus データベース テーブルを管理する個別の ShoppingStatus マイクロサービスを開発できます。このマイクロサービスは、次の図のように顧客のショッピング ステータスを管理する API を公開します。

API が他のサービスに公開される。

図 7. マイクロサービスが API を複数のサービスに公開している。

図 7 では、支払い、注文、出荷のマイクロサービスで ShoppingStatus マイクロサービス API を使用しています。データベース テーブルがサービスと密接に関連する場合は、データをそのサービスに移動することをおすすめします。その後、他のサービスが使用するデータを API を介して公開できます。このように実装することで、サービスをより細かく分割して頻繁な呼び出しを行う必要がなくなります。サービスが適切に分割されていない場合は、サービス境界の定義を再度検討する必要があります。

分散型トランザクション

サービスをモノリスから分離すると、元のモノリシック システムのローカル トランザクションが複数のサービスに分散される可能性があります。複数のサービスにまたがるトランザクションは分散トランザクションとみなされます。モノリシック アプリケーションでは、データベース システムによってトランザクションがアトミックであることが保証されます。マイクロサービス ベースのシステムで複数のサービス間のトランザクションを処理するには、グローバル トランザクション コーディネーターを作成する必要があります。トランザクション コーディネーターは、このシリーズの次のドキュメント、マイクロサービス設定でのサービス間通信で説明するロールバック、代替アクション、その他のトランザクションを処理します。

データの整合性

分散トランザクションには、サービス間でデータの整合性を維持するという課題があります。すべての更新はアトミックである必要があります。モノリシック アプリケーションでは、クエリのプロパティによって、分離レベルに基づいてデータベースの一貫したビューが返されることが保証されます。

対照的に、マイクロサービスベースのアーキテクチャの場合はマルチステップ トランザクションを検討します。1 つのサービス トランザクションが失敗した場合、他のサービスで成功したステップをロールバックしてデータを調整する必要があります。そうしないと、アプリケーションのデータのグローバル ビューがサービス間で一致しません。

結果整合性を実装するステップが失敗したタイミングを特定することは困難です。たとえば、ステップが直ちに失敗せず、ブロックまたはタイムアウトする可能性があります。このため、なんらかのタイムアウト メカニズムの実装が必要になることがあります。呼び出されたサービスがデータにアクセスするときに、古いデータが古くなっていると、ネットワーク レイテンシを短縮するためにサービス間でキャッシュの使用やデータの複製を行っても、整合性のないデータになる可能性があります。

このシリーズの次のドキュメント、マイクロサービスの設定におけるサービス間通信では、マイクロサービス間で分散トランザクションを処理するパターンの例を示しています。

サービス間通信の設計

モノリシック アプリケーションの場合、コンポーネント(またはアプリケーション モジュール)は関数呼び出しによって直接呼び出しを行います。一方、マイクロサービス ベースのアプリケーションは、ネットワークを介して相互に通信する複数のサービスから構成されます。

サービス間通信を設計する際は、まず、サービスが相互に通信する方法を検討します。サービス インタラクションは次のいずれかになります。

  • 1 対 1 のインタラクション: 各クライアント リクエストは 1 つのサービスによって処理されます。
  • 1 対多のインタラクション: 各リクエストは複数のサービスによって処理されます。

インタラクションが同期型か非同期型かについても検討する必要があります。

  • 同期: クライアントがサービスからのタイムリーなレスポンスを期待しています。待機中にブロックされることもあります。
  • 非同期: クライアントはレスポンスを待機している間、ブロックされません。レスポンスはすぐに送信されるわけではありません。

次の表に、インタラクション スタイルの組み合わせを示します。

1 対 1 1 対多
同期 リクエストとレスポンス: サービスにリクエストを送信して、レスポンスを待ちます。 -
非同期 通知: サービスにリクエストを送信しますが、返信は期待されず、送信されません。 パブリッシュとサブスクライブ: クライアントが通知メッセージを公開し、0 個以上の関連サービスがそのメッセージを使用します。
リクエストと非同期レスポンス: サービスにリクエストを送信すると、非同期で応答します。クライアントはブロックされません。 パブリッシュと非同期のレスポンス: クライアントがリクエストを発行し、関連するサービスからのレスポンスを待ちます。

各サービスは通常、これらのインタラクション スタイルの組み合わせを使用します。

サービス間通信の実装

サービス間通信を実装するには、さまざまな IPC テクノロジーを使用できます。たとえば、サービスでは HTTP ベースの REST、gRPC、Thrift などの同期リクエスト / レスポンスベースの通信メカニズムを使用できます。あるいは、サービスでは AMQP や STOMP などの非同期のメッセージ ベースの通信メカニズムを使用することもできます。さまざまなメッセージ形式から選択することもできます。たとえば、JSON や XML などの人間が読めるテキスト形式を使用できます。また、サービスは Avro やプロトコル バッファなどのバイナリ形式も使用できます。

他のサービスを直接呼び出すようにサービスを構成すると、サービス間の結合は緊密になります。このため、メッセージングまたはイベントベースの通信の使用をおすすめします。

  • メッセージング: メッセージングを実装すると、サービスが相互に直接呼び出す必要がなくなります。すべてのサービスがメッセージ ブローカーを認識し、そのブローカーにメッセージを push します。メッセージ ブローカーは、これらのメッセージをメッセージ キューに保存します。他のサービスは、関心のあるメッセージをサブスクライブできます。
  • イベントベースの通信: イベント ドリブン処理を実装すると、サービス間の通信は個々のサービスが生成するイベントを介して行われます。個々のサービスは、イベントをメッセージ ブローカーに書き込みます。サービスは関心のあるイベントをリッスンできます。このパターンでは、サービスにペイロードが含まれないため、サービスは疎結合になります。

マイクロサービス アプリケーションでは、同期通信ではなく非同期サービス間通信を使用することをおすすめします。リクエスト / レスポンスは、よく理解されているアーキテクチャ パターンであるため、同期 API の設計は非同期システムの設計よりも自然に感じるかもしれません。サービス間の非同期通信は、メッセージングまたはイベント ドリブン通信を使用して実装できます。非同期通信には次のようなメリットがあります。

  • 疎結合: 非同期モデルは、リクエスト / レスポンスのインタラクションをリクエスト用とレスポンス用の 2 つのメッセージに分割します。サービスのコンシューマはリクエスト メッセージを開始し、レスポンスを待ちます。サービス プロバイダはリクエスト メッセージを待機し、それにレスポンス メッセージで応答します。この設定では、呼び出し元がレスポンス メッセージを待機する必要はありません。
  • 障害分離: 送信者はダウンストリーム コンシューマで障害が発生しても引き続きメッセージを送信できます。復旧すると、コンシューマはバックログを受け取ります。各サービスには独自のライフサイクルがあるため、この機能はマイクロサービス アーキテクチャで特に役立ちます。同期 API の場合、ダウンストリーム サービスが稼働している必要があります。稼働していないと、処理に失敗します。
  • 応答性: アップストリーム サービスは、ダウンストリーム サービスで待機しない場合には、より迅速に応答できます。サービス依存関係のチェーンがある場合(サービス A が B を呼び出し、C が呼び出されるなど)、同期呼び出しを待機すると、許容範囲を超えるレイテンシが発生することがあります。
  • フロー制御 : メッセージ キューはバッファとして機能するため、受信者もそれぞれのレートでメッセージを処理できます。

ただし、非同期メッセージを効果的に使用するには、いくつかの課題があります。

  • レイテンシ : メッセージ ブローカーがボトルネックになると、エンドツーエンドのレイテンシが大きくなる可能性があります。
  • 開発とテストのオーバーヘッド: メッセージングまたはイベント インフラストラクチャの選択によっては、メッセージの重複が発生する可能性があり、オペレーションをべき等にすることが難しくなります。また、非同期メッセージングを使用してリクエスト / レスポンス セマンティクスを実装してテストするのも難しい場合があります。リクエストとレスポンスのメッセージを関連付ける方法が必要です。
  • スループット: 中央のキューまたは他のメカニズムを使用した非同期メッセージ処理がシステムのボトルネックになる可能性があります。キューやダウンストリームのコンシューマなどのバックエンド システムは、システムのスループット要件に合わせてスケーリングする必要があります。
  • エラー処理が複雑になる: 非同期システムでは、呼び出し元はリクエストが成功したかどうかわからないため、エラー処理を帯域外で行う必要があります。このタイプのシステムでは、再試行や指数バックオフなどのロジックの実装が難しくなる場合があります。チェーン内の非同期呼び出しがすべて成功または失敗する必要がある場合、エラー処理はさらに複雑になります。

このシリーズの次のドキュメント、マイクロサービス設定内のサービス間通信では、上のリストにある課題に対処するリファレンス実装について説明します。

次のステップ