マイクロサービスと疎結合

マイクロサービスにおける依存関係の複雑さと問題点
マイクロサービスアーキテクチャでは、本来サービス同士が独立していることが理想ですが、設計や運用によっては 「分散モノリス」 と呼ばれるアンチパターンに陥る危険があります。
分散モノリスとは、各マイクロサービスが共通のエンジンや多数の共有ライブラリに依存しすぎた結果、ネットワーク越しに一枚岩(モノリシック)のシステムを実装してしまっている状態です。
例えば、新しい機能を追加するたびに複数のサービスで使われている共通コンポーネント(プラットフォームやエンジン)を修正しなければならない場合、サービス間の独立性は失われています。このような状態ではマイクロサービスの利点である独立した開発・デプロイが損なわれ、実質的に複数のサービスが密接に結合した 「分散されたモノリス」 となってしまいます。
共有ライブラリやエンジンに強く依存したマイクロサービス群は、異なる技術スタックをサービスごとに採用できるという柔軟性を失います。さらに、組織的・技術的な疎結合も達成できず、各サービスのチームは権限なしには技術スタックを自由に変更できなくなるのです。FacebookエンジニアのBen Christensen氏は、サービスの動作に必須な共有ライブラリに依存しすぎた状態を指して「分散モノリス」と呼び、「ネットワーク越しにモノリスをばらまき、分散システムのコストだけ払ってマイクロサービスの利点を享受できない状態」だと指摘しています。
このような密結合状態では、マイクロサービス導入のメリットが薄れてしまいます。
具体的な問題点として、新機能の追加時に発生する影響範囲の広さが挙げられます。あるサービスに新機能を実装しようとした際、それが共通エンジンや他のサービスの変更も伴うようであれば、開発スピードは落ち、リリースも各サービスの足並みを揃える必要が出てきます。このようにサービス間の依存が強いと、一部の変更が全体に波及し、せっかくサービスを分割した意味が薄れてしまいます。また、バージョン互換性の問題も生じやすく、一つのサービスのアップデートが他のサービスとの整合性を崩すリスクがあります。
下位レイヤー(LSL)のバージョンアップによるリスク
マイクロサービス環境でも、各サービスが利用する共通のライブラリや基盤(LSL: ライブラリやサブシステムレイヤー)のバージョンアップには注意が必要です。例えば、複数のサービスで共通のデータアクセスライブラリを使っている場合、そのライブラリを新バージョンに上げると、他のサービスでも同様に更新しなければ上位のアプリケーションが動作不良を起こす可能性があります。共有コードにビジネスロジックが含まれている場合は特に、あるサービスでそのコードを変更すると、そのコードを使っている全てのサービスに影響が及びます。
このリスクを避けるためには、サービス間でコードを共有しすぎないことが重要です。Christensen氏は「サービスの境界の外に共有コードが漏れたら、それは潜在的な密結合だ」と述べており、必要以上のコード共有を戒めています。彼はまた、Sam Newman氏の言葉として「サービス間の過度な密結合による問題は、コードの重複による問題よりも悪質」であると紹介しています。つまり、同じコードを複数サービスで共有してDRY(Don’t Repeat Yourself)を徹底するよりも、多少のコード重複を許容してでもサービス間の依存を減らす方がトラブルを防げるという考え方です。
下位レイヤーの変更で上位サービスが壊れるのを防ぐには、各サービスが自分のペースでライブラリを更新できる体制を整えることが望ましいです。たとえば、共有ライブラリに大きな変更がある場合でも、後述するAPIのバージョニング戦略やリリース管理によって、旧バージョンをしばらく併存させ、各サービスが順次新バージョンに移行できるようにします。また、共有ライブラリ自体も、可能であれば後方互換性を保つよう設計・バージョン管理することが重要です。要するに、「ライブラリを最新版に上げたら他サービスが壊れた」という事態を避けるため、緩衝策(ブレイクしない工夫) を講じて依存関係を緩めておく必要があります。
真の疎結合とは何か?定義と基本原則
疎結合(loose coupling) とは、ソフトウェアコンポーネント同士の結びつきをできるだけ弱くし、それぞれの独立性を高めた状態を指します。IT用語辞典では「疎結合とは、細分化された個々のコンポーネント同士の結びつきが比較的緩やかで、独立性が強い状態のこと」であると定義されています。マイクロサービスはまさに「超疎結合」なアーキテクチャスタイルであり、各サービスが可能な限り独立して動作・変更できることを目標とします。
真の意味で疎結合なマイクロサービスでは、あるサービスの変更や障害が他のサービスにほとんど影響を与えません。各サービスは明確な境界(インターフェース)を持ち、その内部実装やデータストレージは他サービスから隠蔽されています。依存が完全になくなるわけではありませんが、依存は明示的かつ安定した契約(APIやイベント) に限定されます。具体的には、サービスAがサービスBに依頼(コール)するとしても、その契約となるAPI仕様やイベントスキーマだけに依存し、サービスBの内部構造やデータベーススキーマには依存しない状態です。
疎結合を実現するには、高凝集(high cohesion) も必要です。つまり、一つのサービスの内部は、そのサービスが担う機能に関連するものだけでまとまっている(凝集している)状態が望ましいです。高凝集であれば他のサービスとのインターフェースがシンプルになり、結果として疎結合が促進されます。逆に、一つのサービスが多くの異なる責務を抱えていると、そのサービス変更時に様々な他サービスとの調整が必要となり、密結合を招きます。
真の疎結合を図る基本原則としては次のようなものがあります。
-
契約による通信: サービス間通信はすべて明示的なAPIコールかメッセージング(イベント)で行い、内部実装への直接アクセスは禁止します(後述するベゾスの例のように)。
-
シンプルで安定なインターフェース: 一度公開したサービスのAPI契約は極力変更せず、変更する場合も後方互換性を保つかバージョンを上げて互換性を切り離す(詳細は後述)ことにより、依存するクライアントへの影響を最小化する。
-
データベースの分離: 各サービスは自前のデータベースを持ち、他サービスとデータストアを共有しない(後述)ことで、データスキーマの変更による影響をサービス内に閉じ込めます。
サービス内部に留める依存: ロギングや認証など共通機能は各サービス内で独立して実装するか、どうしても共通ライブラリを使う場合はそれを強制ではなく選択可能な独立ライブラリとし、利用側に柔軟性を残す。要はサービス境界を越えた依存を作らないことが大切です。
これらを実践することで、「あるサービスを変更しても他はその変更に気付かず動き続けられる」状態、すなわち真の疎結合に近づくことができます。
マイクロサービス依存関係を管理するベストプラクティス
マイクロサービス間の依存を適切に管理・緩和するために、以下のようなベストプラクティスが知られています。
1. APIゲートウェイパターンの活用
APIゲートウェイは、クライアントからの要求を一手に引き受けて内部の適切なマイクロサービスへ振り分ける集中エントリーポイントです。このパターンを用いると、クライアント(例えばフロントエンドや他の外部システム)はマイクロサービスの詳細を意識せずに単一のエンドポイントにリクエストを送るだけで済みます。ゲートウェイはリバースプロキシとして動作し、リクエスト内容に応じて内部のサービス群に適切にルーティングします。また、APIゲートウェイは認証・認可、リクエスト/レスポンス変換、ログ記録、レート制限など横断的な機能を担うこともできます。
APIゲートウェイによる利点は、サービスとクライアントの疎結合です。内部でサービス構成が変わっても、ゲートウェイがインターフェースを保っていればクライアント側への影響を抑えられます。また、複数のマイクロサービスから集約したデータを一回の呼び出しで提供するなど、きめ細かなAPI設計が可能になります。Netflixの例では、ZuulというAPIゲートウェイを導入し、モバイルアプリやウェブからの大量のリクエスト(1日20億件以上)を約500以上のマイクロサービス群に振り分けていました。このようにゲートウェイが外部と内部の橋渡し役となることで、サービス個々はクライアント固有の要求形式や認証方式から切り離され、自分の本来の責務に注力できます。
ただし、APIゲートウェイは主に外部との通信の簡素化と集約に有効であり、内部サービス間の依存には別の対策も必要です。内部のサービス間通信には、ゲートウェイを経由せず直接通信やサービスメッシュを使う方が適切な場合もあります。要件に応じてAPIゲートウェイを使いつつも、内部では後述するイベント駆動や直接のAPIコールで効率化と疎結合を図ります。
2. イベント駆動アーキテクチャ(非同期メッセージング)の採用
イベント駆動型アーキテクチャは、疎結合を実現する強力な手段です。サービス間を直接呼び出すのではなく、イベントメッセージを介して連携することで、お互いの存在を意識せず動作できます。各サービスはイベントブローカー(メッセージルーター)のみを知り、他のサービスの詳細やアドレスを知りません。その結果、あるサービスがイベントを発行し、他のサービスがそれを購読・処理する形となり、サービス同士は 「イベント」という契約以外何も知らない状態になります。
イベント駆動により得られる効果は、高い独立性と耐障害性です。例えば、サービスAが「注文完了」というイベントを発行し、サービスB(在庫管理)やサービスC(メール通知)がそれを受け取る場合、Aは受け手が誰で何個あるかを知りません。仮にBやCが一時停止していても、イベントブローカー(例えばKafkaなど)がバッファとなって溜めておき、後で処理できます。また、あるサービスがダウンしていても他のサービスは影響を受けにくく、全体として障害に強いシステムになります。各サービス開発チームは自サービスのイベント契約だけ管理すればよく、他チームとの調整コストも下がります。
イベント駆動アーキテクチャを導入する際は、イベントの設計(イベントの種類やペイロードのスキーマ)と最終的な一貫性に留意する必要があります。同期的な取引に比べ、非同期メッセージングでは処理完了のタイミングにズレが生じるため、結果整合性の考え方を取り入れることになります。例えば、分散トランザクションを避けて各サービスが独立にデータを更新し、整合性はイベントを通じて最終的に保証する(Sagaパターンなどを活用する)といった設計が必要です。これらを適切に設計すれば、イベント駆動はスケーラブルで疎結合なマイクロサービス連携を実現できます。
3. APIのバージョニング戦略と後方互換性
マイクロサービス間の依存を管理する上で、API(サービスインターフェース)のバージョン管理は避けて通れません。理想的にはサービスのAPIは安定して変更しないのが望ましいですが、ビジネス要件の変化に応じてAPIを進化させる必要が出てきます。重要なのは、新しいAPIに変更する際に既存の利用者(クライアントや他サービス)を即座に壊さないことです。
後方互換性を保つ変更であれば可能な限り互換性維持します。例えば、APIのレスポンスに新しいフィールドを追加する場合、旧クライアントがそのフィールドを無視できればサービス全体を一度にバージョンアップする必要はありません。また、リクエストに新パラメータを追加する場合も、旧クライアントにはデフォルト値やオプション扱いとし、即座にエラーとならないように配慮します。
しかし、後方互換性を保てない大きな変更(スキーマの抜本的な変更や機能削除など)の場合は、バージョンを上げた新しいAPIを提供する必要があります。一般的には、新旧2つのAPIバージョンをしばらく並行稼働させ、徐々に旧バージョン利用者を新バージョンに移行させるという段階的展開を行います。RESTfulなサービスでは、URLパスにバージョン番号を含める(例: /api/v1/... と /api/v2/...)か、HTTPヘッダーでバージョンを指定する方式がよく取られます。そしてサーバ側では、同じサービスプロセス内で両バージョンのハンドラを実装するか、あるいは別のサービスインスタンスを立ち上げてそれぞれのバージョンを受け持つかの選択があります。
いずれにせよ、新バージョンリリース時には旧バージョンを一定期間サポートすることが欠かせません。この戦略により、サービス間の依存は緩和されます。すなわち、サービスAがAPIをv2に変更しても、サービスBはしばらくv1を使い続けられるため即時対応を強いられない、という状態を作るのです。組織としては、古いバージョンを利用しているサービスやクライアントに対して移行ガイドを提示し、十分な移行期間を設けて互換性を切り替えます。最後に利用者がいなくなった段階で旧バージョンのサポートを終了すればよいでしょう。
このようなバージョニング戦略に加え、**契約テスト(APIコントラクトテスト)**を導入することも依存管理のベストプラクティスです。サービス提供側と利用側で事前にインターフェースの契約をテストしあう(例: Pactなどのツールを用いる)ことで、変更による破壊的影響をデプロイ前に検知できます。これもまた、疎結合を保ちながら安心してサービスを進化させる助けになります。
4. データベースの分離とデータ管理戦略
各マイクロサービスは自身専用のデータベースを持つのが原則です。これにより、あるサービスのデータスキーマ変更が他サービスに伝播することはなくなり、サービス間の独立性が保たれます。共通のデータベースやテーブルを複数サービスで直接共有してしまうと、データの読み書きやスキーマ変更を巡って調整が必要になり、サービスの独立性が崩れてしまいます。データの境界をサービス境界と一致させることが疎結合化には重要です。
もちろん、サービス間でデータをやり取りする必要はあります。しかしその際も、直接他サービスのDBを読むのではなくそのサービスの公開API経由か、前述のイベント経由でデータ連携します。Amazonのジェフ・ベゾスが社内に発した有名な「APIマンダイト(命令)」では、「他チームのデータストアへ直接アクセスしてはならない。必ずサービスインターフェース越しに通信せよ」 と厳命しています。これはまさにデータベースをサービスごとに独立させ、直接の結合を排除する思想です。
ただし、データベースを分けるとトランザクション管理や集計処理に課題が出ます。分散したデータから一貫性ある集計結果を得るには、各サービスから集めた情報を最終的に整合させる仕組みが必要です。一般には、Sagaパターンで各サービスのローカルトランザクションを調整したり、データレイクやデータウェアハウスに各サービスのデータを定期的に集約して分析用途に供する、といった形で対応します。つまり、運用面でデータ統合の仕組み(非同期同期化やレプリケーション) を別途用意しつつ、オンライン処理の系ではサービスごと独立したDBを守るのです。
データベース分離のもう一つの利点は、サービスごとに最適なデータストレージ技術を選べることです。あるサービスはリレーショナルDB、別のサービスはドキュメントDBやグラフDBが向いている、といった場合でも、疎結合であれば各サービスが自分の技術スタックを自由に採用できます。これもマイクロサービスの強みであり、依存関係が薄いほど発揮しやすくなります。
5. 依存関係を減らすためのその他のプラクティス
上記以外にも、マイクロサービスの依存管理には様々な工夫が考えられます。
-
サービスディスカバリとレジストリ: サービスの所在地(ホストやポート)を動的に管理することで、呼び出し元が呼び先に執着しないようにします。例えばサービスAがサービスBを呼ぶ際、固定のURLではなくサービス名で呼び出し、サービスレジストリが現在の実体に解決するようにすれば、Bの配置変更やスケールアウトもAに意識させずに済みます。
-
リトライとフォールトトレランス: 依存先が一時的に不安定でも呼び出し元が耐えられるよう、サーキットブレーカーやリトライ機構を入れておきます。Netflixは各サービス間呼び出しにサーキットブレーカー(Hystrix)を導入し、あるサービスが失敗してもシステム全体へ波及しにくくすることで故障伝播による密結合を緩和しました。
-
共通契約の策定: 完全にコードを共有しないまでも、サービス間で共通のデータフォーマットやインフラ基盤に関する契約(約束事)を設けることも有効です。例えばログのフォーマットやトレーシングのやり方は各サービスで異なっていると運用が大変なので、標準ライブラリを用意して各チームが自由に採用できるようにする、といった形です。ポイントは「強制ではなく各サービスが自発的に利用できる独立ライブラリ」とすることで、必要なら外して独自実装も可能、すなわち最悪の場合でもサービスを止めない自由度を残すことです。
テストとCI/CDの整備: 依存関係が複雑になるほど、変更による副作用を早期に発見する仕組みが重要です。各サービスがモックや契約テストで他サービスとのインターフェースを確認し、CIパイプラインで自動検証することで、密結合になりかけている部分を検知できます。また、CI/CDにより各サービスを小さな変更単位で頻繁にリリースできるようにすると、一度に大規模な変更を入れる必要が減り、結果として依存の調整コストも下がります。頻繁なデプロイに耐えうるアーキテクチャかどうかが、実は疎結合の度合いを示す指標にもなります。
大規模開発チームにおける疎結合実現のための工夫
マイクロサービスによる疎結合を成功させるには、技術面だけでなく組織運営面での工夫も不可欠です。コンウェイの法則が示すように、「システムのアーキテクチャは、そのシステムを設計する組織のコミュニケーション構造を写す」 と言われます。そのため、組織構造と開発プロセス自体を疎結合型に最適化することが重要です。
まず、チーム編成です。マイクロサービス推進者たちは、ビジネス上のある機能領域を丸ごと担当する小規模で自律的なチームを編成することを推奨しています。各チームには必要な開発スキル(フロントエンド、バックエンド、DBなど)を全て揃え、他チームに依存せず完結したサービス開発ができるようにします。Amazonではこれを「2ピザチーム」(ピザ2枚で満腹になる程度の少人数チーム)と呼び、一つのサービス(またはドメイン)に専念させました。このようなクロス機能チームにサービスのオーナーシップを持たせることで、サービスと同様にチームも自律的に動けるようになります。結果として各サービスは独立して改良・デプロイ可能となり、組織とアーキテクチャが整合した疎結合状態が促進されます。
次に、コミュニケーションとガバナンスです。疎結合を維持するためには各チームが自由に開発できることが望ましい一方で、全体として齟齬が出ないよう最低限の取り決めも必要です。例えば、他サービスのAPIを呼ぶ場合の合意(どういう場合に相談・レビューが必要か)、共通ライブラリの変更プロセス(誰が承認するか、影響調査はどうするか)などをあらかじめ定めます。しかし過度な中央統制はチームの自主性を損なうため、ガイドラインは軽量に保ちつつ、各チームのテックリード同士の情報共有や設計レビュー会を定期開催する、といった文化面のフォローが有効です。Amazonでは2002年にジェフ・ベゾスが「全チームはサービス経由でコミュニケーションせよ」という社内API命令を発し、この文化を徹底しました。その内容は「全てのチームはデータと機能をサービスインターフェースとして公開し、チーム間はそれを通じてのみ連携すること。他チームのデータベースに直接アクセスすることなどは一切禁止」というものです。これにより組織横断でサービス思考が根付いたと言われます。
さらに、大規模チームではプラットフォームチームの整備も効果的です。各サービスチームが共通に必要とするインフラやCI/CD基盤、監視・ログなどのツールを専門に提供するプラットフォーム部門を設けることで、各プロダクトチームは安心して自サービスの開発に注力できます。プラットフォームチームはサービス間の共通課題を解決するライブラリやサービス(例えばサービスメッシュや共通認証基盤)を提供しますが、前述のとおりそれらはできる限り疎結合を損なわない形(任意利用可能なサービスやライブラリ)で提供されるべきです。大規模組織では、このように共通基盤整備とドメインごとの自律チームの二軸で全体最適を図ります。
最後に、段階的な導入と継続的改善の姿勢も重要です。初めから完璧な疎結合は難しいため、まずはモノリスから切り出したサービス同士の関係を観察し、問題のある依存を発見したら改善するといったリファクタリングを繰り返します。サービスの粒度が細かすぎて運用が混乱していれば統合し直す勇気も必要ですし、逆に依存が強すぎる部分はさらに分割やインターフェース調整を検討します。マイクロサービスは一度設計したら終わりではなく、組織とシステムの進化に合わせて柔軟にアーキテクチャを適応させていくプロセスそのものだと考えると良いでしょう。
疎結合を実現した企業の具体例
実際にマイクロサービスを大規模に活用し、疎結合のメリットを享受している企業の例を紹介します。
-
Amazon(アマゾン): 早くからサービス指向アーキテクチャに舵を切り、大規模モノリスを分解して数百以上のサービスに移行した代表例です。従来のモノリスだったAmazon.comは、機能追加やスケーリングのたびに依存関係の解きほぐしが必要で「アップデートにも苦労し、開発生産性が大きく低下していた」そうです。そこで各機能を小さな独立サービスに分割し、それぞれを専任の小さなチームに所有させました。この再編により、一つのサービスを変更しても他に波及しないので安全かつ迅速に機能改善できるようになり、結果としてAmazonは急成長するビジネスニーズに対応できたのです。Amazonはこの経験からAWSなどマイクロサービス支援技術も生み出し、現在の巨大企業へ躍進しました。2008年時点のAmazonのマイクロサービス相関図(通称「Death Star」)は無数のサービスとその通信が描かれており、一見複雑ですが、その分依存が細分化されスケーラブルになったことを示しています。
-
Netflix(ネットフリックス): 動画ストリーミングで知られるNetflixも、マイクロサービスを極限まで推し進めた企業です。2008年頃、Netflixは度重なるサービス停止とスケーラビリティの限界に直面し、モノリシックなデータベースが単一障害点になっていたことを痛感しました。そこでクラウド(AWS)への移行と並行してシステムを細分化し、2012年までにほぼ全ての機能をマイクロサービスとして切り出しました。その結果、2013年にはAPIゲートウェイ経由で1日20億のリクエストを500以上のマイクロサービスで処理するまでになり、2017年には700以上の疎結合なマイクロサービスに拡大したと報告されています。Netflixは各サービスが独自にスケール・デプロイできる体制を築き、世界中の利用者に途切れない配信を実現しました。また、サービス間連携にHTTP/RESTやgRPC、非同期メッセージングを適材適所で使い分け、依存によるボトルネックを最小化しています。例えば視聴履歴サービスが落ちても推薦サービスは動き続けるなど、部分的な障害に強い設計となっています。Netflixの成功は、疎結合なマイクロサービスが巨大スケールでのアジリティと信頼性を両立できることを示す好例です。
-
国内企業の例 : 日本でもマイクロサービスを採用する企業が増えてきました。たとえばメルカリは急成長に伴う開発の停滞を打破するため2017年頃からモノリスをマイクロサービスへ段階的に移行し始め、各サービスにオーナーチームを置いて開発のボトルネックを除去しました(※詳細は社内技術ブログなどで公開されています)。楽天やサイボウズなどもドメイン駆動設計と組織改編を組み合わせてサービス分割を進めた例があります。それぞれの企業に事情は異なりますが、「サービスの独立性を組織の独立性で支える」という点は共通しており、疎結合実現の鍵となっています。
以上のように、理論と実践の両面からマイクロサービスの依存関係と疎結合について解説しました。ポイントは、単にサービスを細かく分ければ良いのではなく、そのインターフェースの持ち方やチーム体制まで含めて初めて真の疎結合が達成されるということです。適切に依存を管理できれば、新機能の素早いリリースや部分的な障害の局所化など、マイクロサービスの恩恵を最大限享受できるでしょう。ぜひ紹介したベストプラクティスや事例を参考に、自社システムのアーキテクチャと組織を見直してみてください。