本文へスキップ
マイクロサービス設計パターンの実践ガイドのアイキャッチ画像
Architecture

マイクロサービス設計パターンの実践ガイド

公開: 更新: 約10分で読めます

マイクロサービスは、サービスを小さく分けること自体が目的ではありません。独立したデプロイと自律的なチーム運営を実現し、変更のリードタイムを短縮するための手段です。一方で、分散システムには同期呼び出しの連鎖、データ整合性、部分障害といった固有の難所があり、これらに対処する定番の設計パターンが体系化されてきました。

本稿では、サービス分解からエッジ、サービス間通信、データ整合性、回復性、そして移行までを、.NET と Azure の文脈で整理します。あわせて、それぞれのパターンを「いつ採用し、いつ避けるべきか」という判断基準を示します。パターンは万能薬ではなく、解こうとしている問題に対して選ぶものである、という前提で読み進めてください。

サービスをどう分解するか

設計の出発点はサービスの境界です。境界を誤ると、あらゆるパターンが後追いの補修になります。分解の指針は主に二つあります。

ビジネスケイパビリティとサブドメイン

ビジネスケイパビリティによる分解は、組織が提供する機能単位(受注、在庫、決済、配送など)にサービスを対応させる方法です。これはドメイン駆動設計のサブドメイン、およびその実装単位である境界づけられたコンテキストと自然に重なります。目安は次のとおりです。

  • 一つのサービスが一つの業務責務を持ち、独立してデプロイできる
  • 頻繁に一緒に変更されるコードとデータが同じサービス内に収まっている
  • サービス境界がチーム境界と一致し、外部との調整なしにリリースできる

逆に、単一の業務フローを完了するのに複数サービスへの同期呼び出しが常に必要になるなら、境界の切り方が細かすぎるサインです。最初から粒度を小さくしすぎると、分散トランザクションと運用コストだけが増えます。モジュール化されたモノリスから始め、変更頻度やスケール要件が明確に異なる部分を切り出していく順序が、多くの場合は現実的です。

API Gateway と BFF の背後に複数サービスを配置し、同期(REST/gRPC)と非同期(メッセージバス)の通信、データ整合性のための Saga と Outbox、回復性のサーキットブレーカー、サービスディスカバリを示したマイクロサービス設計パターンの全体図
主要な設計パターンがマイクロサービス構成のどこに位置するかを示した全体図です

エッジのパターン

クライアントと内部サービスの間には、ルーティングや認証を集約する層を置きます。ここで用いる代表的なパターンが API Gateway と BFF です。

API Gateway と BFF

API Gateway は、外部からの入口を一本化し、ルーティング、認証・認可、レート制限、TLS 終端といった横断的関心事を引き受けます。クライアントが各サービスの所在を知る必要がなくなり、内部のサービス構成を隠蔽できます。

BFF(Backend for Frontend)は、この考えをクライアント種別ごとに特化させたものです。Web、モバイル、パートナー向け API では必要なデータ形状や呼び出し回数が異なるため、それぞれに専用のゲートウェイを用意し、画面に合わせた集約を担わせます。判断基準は次のように整理できます。

  • クライアントが一種類で要件も単純なら、単一の API Gateway で十分
  • クライアント種別ごとに集約ロジックやレスポンス形状が大きく異なるなら BFF を導入
  • ゲートウェイに業務ロジックを持ち込みすぎない。あくまで集約と横断的関心事にとどめる

Azure では API Management をマネージドのゲートウェイとして利用でき、BFF は ASP.NET Core の最小 API や YARP によるリバースプロキシで実装するのが一般的です。

サービスディスカバリ

サービスの所在は動的に変わるため、固定 URL ではなく発見の仕組みが要ります。Kubernetes では DNS ベースのサービス名解決が標準で備わり、多くの場合これで足ります。Azure Container Apps でも内部の名前解決が提供されます。オーケストレータの外で運用する場合や、より細かな制御が必要な場合に、専用のレジストリを検討します。いずれにせよ、アプリケーションコードに接続先をハードコードせず、環境に応じた解決に委ねる設計にします。

サービス間の通信

通信方式の選択は、結合度と回復性に直結します。同期と非同期を、目的に応じて使い分けます。

同期通信 — REST と gRPC

要求に対して即座に応答が必要な場合は同期呼び出しを使います。外部公開 API や広い相互運用性が求められる箇所には REST/JSON が適し、サービス間の内部通信で低レイテンシと型安全性を重視するなら gRPC が有力です。ただし同期呼び出しはサービス同士を時間的に結合します。A が B を、B が C を呼ぶ連鎖は、レイテンシが加算されるうえ、末端の障害が呼び出し元へ伝播します。同期の呼び出しには必ずタイムアウトと後述の回復性パターンを組み合わせ、深い呼び出し連鎖は設計段階で避けます。

非同期メッセージング

即時の応答が不要な処理や、複数の受け手に事実を伝えたい場合は、メッセージングで疎結合にします。送信側はメッセージを発行して処理を返し、受信側が自分のペースで消費します。これにより時間的結合が外れ、受信側の一時的な停止が送信側を巻き込まなくなります。Azure では Service Bus がキューとトピックによる信頼性の高い配信を提供し、Event Grid や Event Hubs はイベント通知やストリーム処理に向きます。設計上の要点は次のとおりです。

  • メッセージは少なくとも一度は届く前提とし、受信処理を冪等に保つ
  • 処理が完了してから確認応答を返し、途中失敗時は再配信させる
  • 繰り返し失敗するメッセージはデッドレターへ退避し、本線を詰まらせない

データ整合性を保つ

サービスごとにデータストアを分けると、複数サービスにまたがる更新を単一のデータベーストランザクションで括れなくなります。ここが分散システムで最も難しい領域であり、いくつかのパターンで対処します。

Saga による分散トランザクション

複数サービスにまたがる業務処理は、各サービスのローカルトランザクションの連なりとして表現します。途中で失敗した場合は、それまでに成功したステップを打ち消す補償トランザクションを逆順に実行します。実装形態は二種類あります。

  • コレオグラフィ — 各サービスがイベントを購読し、次の処理を自律的に進める。参加者が少なく流れが単純なときに向く
  • オーケストレーション — 中央のオーケストレータが手順と補償を管理する。ステップが多く、流れの可視化や制御が重要なときに向く

補償はビジネス上の打ち消し(決済の返金、確保した在庫の解放)であり、技術的なロールバックとは異なります。補償自体が失敗する場合に備え、再試行と手動対応の導線を用意しておきます。

Outbox パターン

「データベースを更新し、同時にメッセージを発行する」という操作は、二つのリソースをまたぐため片方だけが成功する危険があります。Outbox パターンでは、業務データの更新と送信予定メッセージの記録を同一のローカルトランザクションで書き込み、別プロセスが Outbox テーブルを読んでメッセージを発行します。これにより、更新とメッセージ発行の原子性が保たれます。少なくとも一度の配信になるため、受信側の冪等性と組み合わせて用います。

CQRS とイベントソーシング

CQRS は、更新(コマンド)と参照(クエリ)のモデルを分離する手法です。複雑な集計や高い読み取り負荷を、書き込みモデルとは別の読み取り専用モデルで最適化できます。分離した分だけ整合性は結果整合になり、実装も増えるため、読み書きの要件が明確に非対称なサービスに限って適用するのが妥当です。

イベントソーシングは、状態そのものではなく、状態を変化させたイベントの列を記録し、現在の状態はイベントの再生で導く手法です。完全な変更履歴と監査可能性が得られる反面、スキーマ変更やイベントの版管理は難度が高くなります。CQRS と併用されることが多いものの、両者は独立したパターンであり、監査要件や時系列の再現が本質的に必要なドメインに絞って採用します。全社一律で導入するものではありません。

部分障害に耐える

分散システムでは、どこかが遅くなる、応答しないという事態が常態です。回復性パターンは、こうした部分障害を全体障害へ拡大させないための仕組みです。.NET では標準ライブラリの Microsoft.Extensions.Resilience(内部で Polly を利用)により、これらをまとめて構成できます。

タイムアウト、リトライ、サーキットブレーカー

タイムアウトは最も基本的な防御です。応答しない相手を待ち続けるとスレッドや接続が枯渇するため、すべての外部呼び出しに上限を設けます。リトライは一時的な失敗に有効ですが、無条件の再試行は障害中の相手へ負荷を集中させます。指数バックオフとジッタを加え、再試行対象を一時的な失敗に限定します。

サーキットブレーカーは、連続する失敗を検知すると呼び出しを一定時間遮断し、相手に回復の猶予を与えます。遮断中は即座に失敗を返すか、キャッシュなどの代替手段(フォールバック)に切り替えます。一定時間後に試験的な呼び出しで回復を確認し、成功すれば通常状態へ戻します。リトライとサーキットブレーカーは組み合わせて使い、リトライで回復しない障害はブレーカーで遮断する、という役割分担にします。

バルクヘッド

バルクヘッドは、船の隔壁になぞらえたパターンです。特定の下流サービス向けの接続やスレッドを区画ごとに分離し、一つの区画が飽和しても他へ波及させません。ある依存先の遅延が、無関係な機能まで巻き込んで枯渇させる事態を防ぎます。

サイドカー

回復性やテレメトリ、暗号化通信といった横断的関心事を、アプリケーションと同じ実行単位に並走する別プロセス(サイドカー)へ切り出す構成です。サービスメッシュはこの考えに基づき、通信の制御を言語非依存でインフラ側に集約します。多言語のサービスが多数あり、通信ポリシーを統一したい規模で効果を発揮します。一方で運用の複雑さも増すため、サービス数が限られる段階ではライブラリによる実装で足りることが多く、導入は規模と必要性を見極めてから判断します。

既存システムからの移行とパターン選択

多くの現場では、更地からの構築ではなく既存モノリスからの移行が課題になります。ここで有効なのが Strangler Fig パターンです。

一度に全面刷新するのではなく、モノリスの前段にルーティング層を置き、機能を少しずつ新しいサービスへ移していきます。移行済みの機能はサービスへ、未移行の機能は既存モノリスへ振り分け、範囲を段階的に広げます。最終的に元のシステムを縮小・撤去する、という進め方です。稼働を止めずに移行でき、各段階で効果を検証しながら後戻りも可能になります。まずは変更頻度が高く、他との結合が緩い機能から切り出すと、リスクを抑えつつ効果を早く得られます。

最後に、パターン選択の判断基準を整理します。マイクロサービス自体、独立デプロイや組織のスケールが本当に必要になってから採用すべきものであり、小規模なうちはモジュラーモノリスのほうが総コストは低くなります。個々のパターンについても、同期呼び出しの連鎖が問題なら通信の非同期化を、複数サービスにまたがる更新があるなら Saga と Outbox を、部分障害の伝播が懸念なら回復性パターンを、というように、解こうとしている具体的な問題に紐づけて選びます。CQRS やイベントソーシングのように複雑さの代償が大きいパターンは、要件が明確な範囲に限定します。パターンを増やすほど運用負荷も増えるため、必要になった時点で足していく姿勢が、長く保守できるシステムにつながります。

エンハンスド株式会社では、マイクロサービスの設計とレガシーからの移行を、.NET と Azure を軸に支援しています。サービス境界の設計、Saga や Outbox による整合性の確保、回復性の作り込み、Strangler Fig による段階的移行まで、現状の課題に合わせて伴走します。マイクロサービス化の可否判断や既存アーキテクチャの見直しをご検討の際は、お気軽にご相談ください。

この記事をシェア

コピーしました

関連記事