こんな場面は必要以上によく起きます:三人のスタートアップが初日にマイクロサービスを構築することを決めます。六ヶ月後、彼らは十五のサービス・誰も完全に理解していない Kafka クラスター、そして再現するためにリクエストを八つのサービスにまたがって追跡しなければならないバグを抱えています。チームは疲弊しています。プロダクトは半分しかできていません。そしてどこかの Slack スレッドで誰かが分散モノリスを構築してしまったという言葉をタイプします。
マイクロサービスは本当に強力です――適切なスケール・適切なチーム・適切な理由があれば。しかしそれは出発点ではなく、組織的スケールのために払う税金です。このガイドでは三つのアーキテクチャの形――モノリス・Modular Monolith・マイクロサービス――を正直にウォークスルーし、いつ(そして本当に)それらの間を移行すべきかのシグナルを示します。
モノリス:一つのデプロイで支配する
モノリスは単一のデプロイ可能なユニットです。一つのビルド・一つのバイナリ(またはコンパイルされたアセットのセット)・一つのデプロイステップ。すべての機能・すべてのモジュール・すべてのデータベースクエリが同じプロセスに住んでいます。
それは制限があるように聞こえますし、言葉自体が少し恥ずかしい響きを帯びてきました――モノリスを持っていることを認めるのは、現代のエンジニアリングに追いついていないことを認めるようなものだと。そのフレーミングは間違っており、多くの不必要な痛みを引き起こしてきました。
モノリスは正しいデフォルトです。その理由は:
- ローカル呼び出しは無料です。プロセス内の関数呼び出しはナノ秒です。サービス間のネットワーク呼び出しはミリ秒です――そして失敗・タイムアウト・部分的な結果を返す・完全に失われることがあります。その複雑さは必要になるまで持ち歩きません。
- トランザクションは簡単です。リレーショナルデータベースはデータモデル全体にわたるACIDトランザクションを提供します。モノリスでは、口座AからBへのお金の移動は一つのトランザクションです。マイクロサービスでは分散トランザクションまたはサガになります――本物の失敗モードを持つ本当に難しい問題です。
- デバッグは簡単です。一つのプロセス・一つのログストリーム・一つのスタックトレース。問題を見つけます。分散システムでは、単一のユーザーアクションが複数のサービスにわたる数十の非同期操作を生み出すことがあり、それらを相関付けるには専用の観測可能性ツールが必要です。
- デプロイはシンプルです。一つのアーティファクト・一つのパイプライン・一つのロールバック戦略。初日から自信を持って出荷できます。
- 大企業は大きなモノリスを出荷しています。Shopify は巨大なスケールで何年も Rails モノリスを動かしていました。Stack Overflow はモノリスを動かしている少数のサーバーから一日に何百万ものリクエストを処理しています。モノリスはスケールを妨げません――ただし異なる方法でスケールします。
モノリスの本物の痛みは本物ですが、人々が期待するよりも後に来ます。ビルド時間はコードベースが成長するにつれて増大します――50kラインで二分かかる完全なリビルドは500kラインで十二分かかります。マージの摩擦は二十人のエンジニアが同じコードベースを変更するときに増加します;長期ブランチとコンフリクトが日常のルーティンになります。デプロイのカップリングはすべてのチームが一緒にデプロイすることを意味します――一つのチームのリスクのある変更が他の全員のリリースを止める可能性があります。
これらの痛みは本物です。しかし、それらは成功の痛みであり、誇大広告が示唆するよりもはるかに後に来ます。ほとんどのチームはそれらに当たるほど幸運であるべきです。
Modular Monolith:ほとんどのチームが飛び越えるスイートスポット
Modular Monolith はまだ一つのデプロイ可能なユニットです――しかし内部的に強制されたバウンダリーを持つモジュールに整理されています。モジュールは明確なインターフェースを通じてコミュニケーションします。一つのモジュールが別のモジュールの内部に直接到達することは許可されていません:兄弟からプライベートクラスをインポートしない・データベーステーブルを共有しない・モジュールの境界線を越えて内部ヘルパー関数を呼ばない。
これは小さな規律のように聞こえますが、ゲームを大きく変えます。
家のフロアプランのようなものだと考えてください。モジュールのないモノリスはスタジオアパートです:一つの大きなオープンルームにすべてがあります。Modular Monolith は壁のある部屋を与えます――まだ一つの家に住んでいて、まだ配管と玄関を共有していますが、キッチンとベッドルームは独自の明確な目的を持っており、ベッドで料理はしません。
モジュールはドメイン駆動設計の境界付けられたコンテキストに自然にマップします:Billing モジュール・Inventory モジュール・Notifications モジュール。それぞれが独自のドメインロジックとデータベーススキーマのスライスを所有します。パブリックインターフェース(サービスファサードまたはパブリック関数のセット)を公開し、他のすべてをプライベートに保ちます。
実践的な利点は大きいです:
- 独立した推論。
Billingに取り組む開発者は、コードベース全体を頭に保つことなくそれを理解・変更できます。 - 安全なリファクタリング。パブリックインターフェースが保たれている限り、モジュールの内部を書き直せます――アプリの残りは気づきません。
- 明確なオーナーシップ。チームがモジュールを請け負います。コードレビューが焦点を保ちます。新しい開発者のオンボーディングは200kラインのコードベースではなく一つのモジュールを渡すことを意味します。
- 移行準備ができたシーム。サービスを抽出する必要が生じても、バウンダリーはすでに引かれています。モジュールがサービスになります。インターフェースがAPIになります。作業は相当ありますが、探索的な手術ではなく既知の作業です。
Modular Monolith は、ほとんどのチームが絡まったモノリスからマイクロサービスへとここで立ち止まらずに飛び越えるアーキテクチャです。それは間違いです。大多数のプロダクトチーム――100人未満のエンジニア・単桁のサービス・一〜二のデプロイパイプライン――にとって、よく管理された Modular Monolith が最も生産的な場所です。
マイクロサービス:実際に手に入れるもの
マイクロサービスはシステムを独立してデプロイ可能なサービスに分解します。各サービスが独自のプロセス・独自のデプロイパイプライン・そして重要なことに独自のデータストアを所有します。サービスはネットワーク経由でお互いに話します:HTTP・gRPC・メッセージキュー・イベントストリーム。
マイクロサービスがうまく機能するとき、スケールで他の何も匹敵できない四つのことを提供します:
- 独立したデプロイ。決済チームは火曜日の午後に、レコメンデーションチーム・検索チーム・通知チームと調整することなく決済サービスへの変更を出荷できます。数百人のエンジニアを持つ企業では、これは変革的です――週に一度のデプロイと一日に百回のデプロイの違いです。
- 独立したスケーリング。
ImageProcessingサービスがピーク時に40のCPUコアを必要とし、UserProfileサービスが2つを必要とするなら、それらを別々にスケールできます。モノリスでは全体をスケールします――必要かどうかにかかわらず、すべての機能のために40コアを購入します。 - チームの自律性。各サービスは小さな自己完結したプロダクトです。五人のエンジニアのチームがサービスを完全に所有できます:言語・データベース・デプロイケイデンスを選びます。これがマイクロサービスの組織的なスーパーパワーです――Conway の法則があなたに反ではなくあなたのために機能する。
- 障害の分離。
RecommendationEngineのメモリリークはRecommendationEngineをダウンさせますが、チェックアウトはダウンさせません。段階的に劣化できます――ストアはまだ機能し、製品にはただパーソナライズされたサジェストがないだけです。モノリスでは、任意のモジュールのキャッチされない例外がプロセス全体をダウンさせる可能性があります。
パターンに注目してください:そのリストのすべては組織的および運用的なスケールについてです。これらはプロダクトマーケットフィットをまだ見つけていない八人のチームには適用されない懸念事項です。
実際に払うもの:分散システムの税金
マイクロサービスは無料ではありません。上記のすべての利点には対応するコストがあり、そのコストは軽くありません。それらを過小評価することが、チームが疲弊して目標未達に終わる原因です。
最悪の結果はマイクロサービスでもモノリスでもありません――それは分散モノリスです:一緒にデプロイしなければならない・同じデータベースを共有する・一緒に失敗するほど密に結合したマイクロサービス。マイクロサービスのすべてのコストを払い、利点は何も受けません。ビジネスドメインではなく技術レイヤーで分割したとき、またはサービスが長いチェーンで同期的に互いを呼び出すときに起きます。ServiceA が ServiceB の後にしかデプロイできないなら、分散モノリスを構築しています。
税金の全貌はこうです:
- ネットワーク呼び出しは失敗します。関数呼び出しはバグを除き失敗できません。ネットワーク呼び出しは失敗・タイムアウト・古いキャッシュ値を返す・最初の試みにすでに副作用があった後でリトライで成功することがあります。どこでも常に部分的な失敗を処理しなければなりません。
- 結果整合性。サービスが独自のデータを所有します。サービスのバウンダリーをまたいでそのデータを一貫して保つには、慎重な設計が必要です――イベント駆動のパターン・サガ・冪等な操作・補償トランザクション。ユーザーが注文を配置できるがすぐに注文履歴に表示されない理由をプロダクトマネージャーに説明するのは厄介です。
- 分散トランザクションは難しい。二フェーズコミットプロトコルは存在しますが、みじめです。ほとんどのチームは代わりにサガパターンを使いますが、複雑さをデータベースからアプリケーションコードにシフトします。どちらも
BEGIN; UPDATE; COMMIT;よりも大幅に複雑です。 - デバッグは別のスキルです。スタックトレースはもはや何が間違ったかを教えてくれません。分散トレーシング(Jaeger・Zipkin・Honeycomb)・すべてのログラインのコリレーションID・サービスメッシュ・サービスにまたがるイベントを相関付けるダッシュボードが必要です。この観測可能性インフラを構築・維持することは本物のエンジニアリング作業です。
- 運用オーバーヘッドが倍増します。一つのサービスに Dockerfile・Kubernetes デプロイメント・イングレスルール・ヘルスチェックエンドポイント・CPUとメモリの制限・ログ集約設定・アラートルールが必要です。三十のサービスで掛け算します。このオーバーヘッドはサービス数に比例してリニアにスケールします;エンジニアの頭数はそうではありません。
- テストは難しくなります。ユニットテストはまだ簡単です。しかし、サービスのバウンダリーをまたぐインテグレーションテストは、複数のサービスを同時に実行し、そのバージョンと設定を管理し、データベースにまたがるテストデータを扱うことが必要です。コントラクトテスト(Pact など)は助けになりますが、学ぶ独自の規律を追加します。
これらのコストはどれも解決不可能ではありません――業界はそのすべてに対して成熟したツールを持っています。重要なのは、それらが毎日払わなければならない本物のコストであり、それらが解放する利点が実際に必要になるまで払う価値がないということです。
シグナル:いつ分割すべきか(そしていつしないか)
では、マイクロサービスがそのコストに見合うしきい値を超えたことをどうやって知るのでしょうか?実際にそれが時期であることを示すシグナルはこれらです:
- 独立したスケーリングのニーズが本物で高価です。システムの残りの10倍のリソースを必要とする特定のコンポーネントを指摘でき、モノリス全体でそのリソースを支払っています。分割することで意味のあるコストの節約または意味ある性能改善が得られます。
- デプロイの競合が慢性的です。チームが他のチームの進行中の変更によって定期的に出荷をブロックされています。共有デプロイの調整オーバーヘッドが測定可能なほど遅くしています――時折ではなく、週次または日次の不満として。
- チーム数がそれを要求しています。異なるデプロイケイデンス・異なる技術の好み・本当に独立したロードマップを持つ別々のビジネスドメインに取り組む五人以上のエンジニアの複数のチームがいます。Conway の法則は最終的なアーキテクチャを予測します――計画した方がよい。
- あるコンポーネントが根本的に異なる信頼性またはセキュリティ要件を持っています。決済・認証・PII ストレージはしばしばパフォーマンス上の理由ではなく、コンプライアンス・影響範囲の縮小・独立した監査の能力のために分離を正当化します。
- モジュールはすでに実質的に分離されています。システムの残りと共有状態がなく、明確に定義されたイベントまたはAPIを通じてのみコミュニケーションし、別のチームが所有しています。組織的なシームはすでに存在します――サービスバウンダリーにすることはすでに真実であることを形式化するだけです。
そして早く分割しすぎているシグナルはこれらです:
- エンジニアリング組織全体が二十人未満です。
- 具体的な制限に当たったからではなく、よりスケーラブルに感じられるから分割しています。
- 計画しているサービスは機能するために一緒にデプロイしなければならないでしょう。
- 各サービスが共有テーブルなしに独自のデータを所有するクリーンなバウンダリーをまだ引けません。
- チームはまだ分散システムをデバッグするための観測可能性インフラ(分散トレーシング・集中ログ・アラート)を持っていません。
- 主な理由がマイクロサービスが求人票や会社のマーケティングコピーに載っているからです。
上手く分割する方法:シーム・ストラングラーフィグ・データオーナーシップ
上記のシグナルが分割を指していれば、方法が非常に重要です。悪い分割は上記で説明した分散モノリスを生み出します。良い分割は本当に独立したサービスを生み出し、それらが自分自身の代金を払います。
技術レイヤーではなく境界付けられたコンテキストで分割する。DatabaseService や ValidationService を作らないでください――それらは技術的な懸念事項であり、ビジネスのものではありません。OrderService・BillingService・InventoryService を作ってください――ドメインに明確で安定した意味を持つビジネス機能の単位。境界付けられたコンテキストは特定のモデルが適用され、言語が一貫しているドメインの部分です。サービスバウンダリーが属するのはそこです。
ストラングラーフィグパターンを使う。宿主となる木を徐々に包む蔓にちなんで名付けられたストラングラーフィグパターンは、一度に全部ではなく段階的にサービスを抽出させてくれます。新しいサービスをモノリスの横に立ち上げ、特定のトラフィックのスライスをそちらにルートし、機能を確認し、モノリスから対応するコードを削除します。モノリスは時間とともに縮小します;決して書き直されません。これはより安全で、より可逆的で、決して終わらない六ヶ月の大爆発マイグレーションになる可能性がはるかに低いです。
一度に一つのサービスを抽出する。すべての抽出が何かを教えてくれます。二回目の抽出は最初よりもうまくいくでしょう。五つのサービスを同時に抽出しようとすることは、学びを分散させてリスクを倍増させます。
データオーナーシップは交渉不可能です。別のサービスとデータベーステーブルを共有するサービスはサービスではありません――余分なネットワークオーバーヘッドを持つモジュールです。各サービスは独自のデータを所有しなければなりません。二つのサービスが同じデータを必要とするなら、一方が権威があり、もう一方がAPI経由でそれを取得するかイベント経由で同期します。この制約を確立することは痛く維持することも痛いですが、それがあなたが求めていた独立したデプロイ可能性と障害の分離を与えるものです。
抽出する前にポートを使ってシームを定義する。Modular Monolith 内で Ports & Adapters アプローチを採用していれば、抽出はほぼ機械的になります:ポートがAPIを定義し、その背後のアダプターがサービスクライアントになります。ドメインロジックは変わりません――デリバリーメカニズムだけが変わります。これが最初からポートで構築することの最も強力な実践的論拠の一つです:抽出準備ができたシームを無料で提供します。
並べて比較
| 次元 | モノリス | Modular Monolith | マイクロサービス |
|---|---|---|---|
| デプロイ単位 | 一つのプロセス・一つのパイプライン | 一つのプロセス・一つのパイプライン | 多くのプロセス・多くの独立パイプライン |
| データオーナーシップ | 共有データベース;すべてのコードがすべてのテーブルにアクセス可能 | 共有データベース;モジュールが慣習によってスキーマ領域を所有 | 各サービスが独自のデータベースを所有;共有テーブルなし |
| 障害モード | 一つのプロセスクラッシュがアプリ全体をダウンさせる | 一つのプロセスクラッシュがアプリ全体をダウンさせる | サービス障害は隔離される;他は段階的に劣化する |
| チームの適合 | 1〜3チームに最適;チーム数とともに摩擦が増大 | 2〜8チームに最適;モジュールがチームオーナーシップに合う | 独立したケイデンスで出荷する5チーム以上に必要 |
| 運用コスト | 低い:一つのデプロイ・一つのログストリーム・一セットのアラート | 低い:プレーンなモノリスと同じ運用フットプリント | 高い:観測可能性・コンテナオーケストレーション・サービスメッシュ・サービスごとの CI/CD |
| トランザクションモデル | 完全な ACID トランザクションが簡単 | 完全な ACID トランザクションが簡単 | サガまたは結果整合性;分散トランザクションは難しい |
| 使う場面 | 新製品・小規模チーム・未知のドメイン | 明確なドメインバウンダリーを持つ成長中プロダクト・10〜100人のエンジニア | 複数の自律的チーム・証明されたドメインモデル・本物の独立したスケーリングニーズ |
会社の規模ごとの正直な見解
アーキテクチャのアドバイスは、マイクロサービスが意味をなすしきい値をすでに超えた企業から来ることが多いです――それらがエンジニアリングブログ・カンファレンストーク・詳細なポストモーテムを書くリソースを持っている企業だからです。これは生存者バイアスを生み出します。より正直な地図を示します:
スタートアップ(1〜15人のエンジニア)。モノリスを構築してください。システムのどの部分がスケールを必要とするか・どの機能が生き残るか・どのドメインバウンダリーが本物かをまだ知りません。早まった分解は、決断を下すのに十分な情報を持つ前にそれを固定します。Shopify・GitHub・Basecamp はすべて Rails モノリスとして始まりました。Twitter もそうで、本物の負荷の下で分解する前に何年もかなりのスケールでそれを動かしていました。
スケールアップ(15〜80人のエンジニア、成長中)。ここが Modular Monolith が真価を発揮する場所です。単一コードベースの制御されない成長が本物の摩擦を引き起こすのに十分なエンジニアがいますが、フルマイクロサービスのオーバーヘッドが速度を殺すほどではありません。モジュールバウンダリー・チームオーナーシップ・明確なインターフェースに投資してください。サービスを抽出するオプションを保ちますが、具体的なスケーリングニーズが強制しない限りまだ行使しません。
エンタープライズ / 大規模組織(80人超のエンジニア、複数の自律チーム)。選択的なマイクロサービスがここで意味をなします――しかし選択的がキーワードです。最も効果的な大規模アーキテクチャは純粋なモノリスでも同じサイズのサービスの海でもありません:それは思慮深い組み合わせです。最も負荷がかかる・ドメインクリティカルな機能のための少数のコアサービス・運用的な中間のための Modular Monolith・独立したスケーリングまたはコンプライアンスが本当に要求する少数の特化したサービス。Amazon はすべてを一度に分解しませんでした;負荷の下でシームを特定し、一つずつ抽出しました。
Netflix と Amazon がマイクロサービスの旅を描くとき、引用される部分は何百ものサービスを持つアーキテクチャ図です。省かれる部分は、彼らがモノリスとして始まり、痛みが否定できなくなるまでそれを動かし、その後移行に何年もの巨大なエンジニアリング努力を投資した――現在マイクロサービスを実行可能にするツールの多くを構築することを含む――という事実です。彼らはどこに到達したかを話しているのであって、どこから始めるべきかを話しているのではありません。
まとめ
- モノリスは正しいデフォルトです。構築が簡単・デバッグが容易・トランザクションが自明。痛みは誇大広告が示唆するよりも後に来て、本物のスケールでのみ。
- Modular Monolith は最も使われていないオプションです。内部モジュール間の強制されたバウンダリーはチームの明確さ・安全なリファクタリング・抽出準備ができたシームを与えます――分散システムの複雑さなしに。
- マイクロサービスは機能ではなく税金です。ネットワーク障害・結果整合性・分散トレーシング・運用オーバーヘッドで払います。見返り――独立したデプロイ・独立したスケーリング・チームの自律性――は本物ですが、それを要求するスケールでのみ。
- 分散モノリスは最悪の結果です。一緒にデプロイしなければならない密に結合したサービスはマイクロサービスのすべてのコストを与え、利点は何も与えません。技術レイヤーではなく境界付けられたコンテキストで分割してください。
- データオーナーシップは難しい制約です。別のサービスとデータベースを共有するサービスはサービスではありません。各サービスは独自のデータを所有しなければなりません;バウンダリーをまたぐ一貫性には明示的な設計が必要です。
- 段階的に分割するためにストラングラーフィグパターンを使う。一度に一つの境界付けられたコンテキストを抽出してください。各抽出は何かを教えてくれて可逆的です。大爆発分解は決してしないでください。
- チームサイズに合わせてアーキテクチャを合わせる、願望ではなく。スタートアップ:モノリス。スケールアップ:Modular Monolith。大規模自律チーム:本当に必要な場所での選択的マイクロサービス。
アーキテクチャの決定は時間を超えて波及し、複利で積み重なります。分割を決めた後に多くのチームが直面する次の質問は、フロントエンドについてどうするかです――単一の統一されたUIを提供するか、それも分解するか。そのラビットホールは Micro-Frontends:いつ価値があり、何が実際にかかるかで探求されています。