🍽️

その先に進むためのモジュラーモノリス再入門

2024/10/09に公開

「モジュラーモノリス」はここ数年で広く普及してきました。実際にモジュラーモノリスを取り入れた開発事例を多く見かけるようになりました。当記事では改めてモジュラーモノリスの起源を遡り、また、さらにその先に進むためにどのような準備をしておくべきかを軽くまとめてみます。

モジュラーモノリスとは

モジュラーモノリスの起源は 2018 年頃

「モジュラーモノリス」という言葉の正確な起源は把握していませんが、Simon Brown が GOTO Conference 2018 で行った講演がその起源の一つかもしれません。この講演は、モジュラーモノリスの概念をわかりやすく説明しており、その後、多くの企業がこれに注目するきっかけとなりました。
https://www.youtube.com/watch?v=5OjqD-ow8GE

次に Shopify が自社の開発事例に基づいてモジュラーモノリスについて説明したことで、この言葉の普及が加速したようです。以下は日本でもよく言及されている記事です。
https://shopify.engineering/deconstructing-monolith-designing-software-maximizes-developer-productivity

更に 「モノリスからマイクロサービスへ」では、モジュラーモノリスについての言及が何度もされており、Shopify についても言及されています。

“多くの組織にとって、モジュラーモノリスは優れた選択肢となる。モジュールの境界が明確に定義されていれば、高度な並列作業が可能になる。その上、より分散されたマイクロサービスアーキテクチャの課題も回避でき、デプロイもよりシンプルになる。Shopifyは、マイクロサービス分解の代替としてこの技術を使用している好例であり、実際とてもうまく機能しているらしい。”
「モノリスからマイクロサービスへ」1.2.1 単一プロセスのモノリス

バランスのよいモジュラーモノリス

モジュラーモノリスは、中程度の規模のシステムにおける最適解として(もしくはマイクロサービス流行の反動として)選ばれています。その理由として、以下のような点が挙げられます。

  • モノリスに対しての利点
    • モジュール化によって密結合度が下がり、開発効率が向上。
    • 予期せぬ破壊的変更のリスクが低減され、全体を理解しなくても開発できる部分が増えるため、並行作業が容易になる。
  • マイクロサービスに対しての利点
    • ネットワーク遅延がなく、トランザクション管理も容易であるため、一貫性を保ちやすい。
    • シンプルなシステム構成により、ローカル開発やデプロイが容易であり、デバッグやモニタリングもシンプル。

縦と横で捉えるモジュラーモノリスの構造

モジュラーモノリスの構造は以下のように捉えられます。伝統的な Java アプリケーションのレイヤー化やオニオンアーキテクチャによる分割は技術的な関心ごとの分離(横の切り口)であり、一方、モジュラーモノリスは業務的な関心ごとの分離(縦の切り口)として整理できます。これは、DDDにおける境界づけられたコンテキストに相当するものであり、業務ドメイン単位でのモジュール化を促進します。

実際、過去に携わっていたシステムでは、縦と横の掛け合わせで100以上のプロジェクト(ビルド単位)が存在し、依存関係はプロジェクトごとに明示的に定義され安定したアーキテクチャが形成されていました。このような構造により、プロダクトは安定しながらも成長を遂げることができました。

しかし、やはり銀の弾丸ではない

モジュラーモノリスの限界

多くの技術がそうであるようにモジュラーモノリスも最終的な終着地ではなさそうです。マーチン・ファウラー氏が述べているように、モノリスから始めることは正しく、Shopify のようにモジュラーモノリスを経由して最適化していくのは理にかなっています。
https://martinfowler.com/bliki/MonolithFirst.html

しかし、システムが成長するにつれ、次のような兆候が見られる場合には、サービス分割を検討することもあるかもしれません。

  • ビルド・デプロイの負担が増大
    • ビルド時間が長くなり、変更のたびにビルドやテストに多大な時間がかかるようになります。これにより、開発サイクルが遅延し、頻繁なリリースが難しくなります。
    • デプロイの際に、影響範囲が大きくなることでリスクが増大し、障害発生時のリカバリも困難になります。
  • スケーラビリティの限界
    • 特定の機能やモジュールが他の部分に影響を与え、全体のパフォーマンスに悪影響を及ぼすことがあります。
    • 特定の機能のみをスケールアウトしたい場合にも、全体が一体化しているため、リソースの無駄遣いが発生します。
  • チームの成長に伴う負荷
    • 複数のチームが同じコードベースを扱うことで依存関係が複雑化し、チーム間の調整が難しくなります。
    • 複数チームによる変更が同じモジュールに集中することで、マージコンフリクトが頻発し、開発効率が低下します。
  • 可用性と信頼性が単一基準
    • 1つのモジュールの障害がシステム全体に影響を与える可能性があります。例えば、特定の機能がダウンすると、他の全機能も影響を受けるリスクがあります。
  • 技術的変化の困難さ
    • コードベースが大規模になり、異なる技術スタックを導入したい場合、全体のシステムに影響を与えずに新しい技術を導入するのが難しくなります。小さく変えることが難しい傾向があります。

これらの多くは、モジュラーモノリスが引き続きモノリスであることから避けきれない限界と言えそうです。モジュラーモノリスは、人間的な側面には最適解を提供しつつ、技術的な側面には課題が残るということかもしれません。

今のShopifyはマイクロサービスと共存

Shopify も 2024 年時点で、Shopify Core と呼ばれる中核機能をモジュラーモノリスで維持しつつ、数百のサービスから構成されるシステムへと進化しています。
https://stackshare.io/shopify/e-commerce-at-scale-inside-shopifys-tech-stack

The core Shopify app has remained a Rails monolith, but we also have hundreds of other Rails apps across the organization. These are not microservices, but domain-specific apps: Shipping (talks with various shipping providers), Identity (single sign on across all Shopify stores), and App Store to name a few.(コアの Shopify アプリは Rails モノリスのままですが、組織全体では他にも数百の Rails アプリがあります。これらはマイクロサービスではなく、ドメイン固有のアプリです。たとえば、Shipping (さまざまな配送業者とのやり取り)、Identity (すべての Shopify ストアでのシングル サインオン)、App Store などがあります)

サービス分割可能な状態に近づけていく

翻ってモジュラーモノリスにおいて将来のサービス分割に備えるということはどういうことでしょうか。インフラや組織なども重要な観点ですが、特に前もって準備を重ねていく必要があるのは次の2点だと考えられます。

  • アプリケーションが分割可能な状態にあること
  • データベースが分割可能な状態にあること

データベース分割をどう準備していくか

アプリケーションを分割する際にいずれAPIになるであろう箇所のI/F設計、またはそこでの抽象化可能な技術については既にいくつかの記事で言及されています。一方、データベース分割についてはモジュラーモノリスの文脈ではさほど言及されていないようですので、ここから少し整理します(※ ここではRDB前提にします)。

データベースアクセスをコンテキスト単位に分離する

データベースアクセスレイヤーをコンテキスト単位に分離することは、データベース分割の前提と捉えています。それぞれのコンテキスト特有のテーブルは、そのコンテキストからのみアクセス可能にするべきです。

しかし、ものごとはそれほど単純ではなく、複数のコンテキストでアクセスが必要な多くの共通テーブルが存在します。Users のようなテーブルかもしれないですし、Tenants のようなテーブルかもしれません。

テーブルを更新するコンテキストを単一に限定する

テーブルを更新する責任を持つコンテキストを一意に定めることで、データの一貫性と整合性を保つことが可能になります。

更新が複数のコンテキストから発生する場合には、テーブル自体を分割し、それぞれのコンテキストに対応した部分に分けることが必要です。

共通テーブルの参照方法に制約を付与する

  • 共通モジュールから提供するAPIを経由してのみ参照する
    • 直接テーブルを参照することを避け、API経由でのアクセスを推奨することで、モジュール間の独立性を保ちます。
    • APIに柔軟性がなければ、N+1問題も発生する可能性があります。一方で、様々な呼び出し方法が定義する場合、API が巨大化する可能性があります。
  • テーブルを1:1で分割する
    • 特定のコンテキストに固有の属性がある場合は、1:1でテーブルを分割し、その属性を専用のテーブルに移すことが有効です。
    • 更新を単一コンテキストにすることとも関連しますが、共通テーブルを所有するコンテキストが他のコンテキストの知識を持たないようにするための分割です。
  • ビューで制限する
    • 分割後にテーブルのコピーを持つことが想定される場合、またそれが不明でもJOIN相当の処理をアプリケーション内に移行するのが煩雑になる場合はビューでの参照が現実解になりえます。
    • ビューの複雑さ等に応じて、マテリアライズドビュー、トリガー等も検討できますが、追加で更新タイミングの考慮は必要といえど、ビューでの制限の延長線上と捉えています。

データの整合性担保とのトレードオフ

ここまでの"分割可能な状態"段階においては、まだ手を付ていない最も大きな課題はデータの整合性担保です。外部参照制約によって守られていたデータの整合性は、システムの分割によって失われる可能性があります。システムの要件によっては、結果整合性を許容して一定の遅延があってもよいのか、それともレスポンス時間を犠牲にしてでもリアルタイムな参照を維持する必要があるのか、さらには2フェーズコミットのような複雑な整合性管理を行う必要があるのか、実際にサービスを分割するにあたって最も慎重に検討が必要な箇所となります。

一方、必ずしもサービス分割に至らならなくとも、データベース分割に労力を割くことはより"モジュラー"になっていくことであり、その労力単体でも見合う効果はあると個人的には確信しています。

まとめ

モジュラーモノリスについて再整理し、その利点や課題について記述してきました。モジュラーモノリスは、システムと組織のスケールを目指すための重要な手段の一つですが、さらなる成長に伴ってサービス分割も視野に入れたくなるかもしれません。特にデータベースの分割については、前もって準備を進めておくことで、将来のスケーラビリティに対応しやすくなりそうですし、モジュラーモノリスとしてもより完成形に近づいていけるでしょう。

この記事の執筆のための調査を通じて、多くの方々がシステムや組織のスケールのためにモジュラーモノリスを採用し、様々な知見を整理していくださっていることを知りました。貴重な知見を本当にありがとうございました。

参考資料

最後に文中で紹介しきれなかったリンクを幾つか紹介します。

Shopify関連:
https://qiita.com/tkyowa/items/ae9fa550237cb6f48318
https://www.infoq.com/jp/news/2019/10/shopify-modular-monolith/
https://newsletter.techworld-with-milan.com/p/inside-shopifys-modular-monolith
事例関連:
https://speakerdeck.com/ogugu9/modular-monolith-that-support-deep-domains-and-integrated-management-platform
https://speakerdeck.com/disc99/monolith-and-microservices-to-modular-monolith
https://zenn.dev/smartround_dev/articles/9-commentary-of-win-pitch-4
https://speakerdeck.com/mosa_siru/luo-mihe-usaashurotakutofalsemaikurosahisuakitekutiya-layerx
https://speakerdeck.com/4geru/addressing-shared-database-challenges-as-cross-team-peach-garden-oath-architecture
フレームワーク関連:
https://github.com/Shopify/packwerk
https://serviceweaver.dev/

株式会社ログラス テックブログ

Discussion