こんにちは、食べログシステム本部長の京和です。
本エントリでは Shopify の Engineering Blog から、Kirsten Westeinde による「Deconstructing the Monolith: Designing Software that Maximizes Developer Productivity」を翻訳して掲載します。
食べログではユーザーや飲食店に価値を届けるスピードを最大化するべく、マイクロサービス化などをはじめとしたこれまでの組織やアーキテクチャを刷新するための取り組みを始めています。しかし、マイクロサービスはアプリケーションアーキテクチャとインフラアーキテクチャが複雑に絡み合ったシステムで技術的難易度が非常に高く、適切に構築できなければ「分散されたモノリス」と呼ばれるアンチパターンに陥ります。1
Shopifyではマイクロサービスではなく、「モジュラモノリス」と呼ばれるアプローチでマイクロサービスが抱える課題を回避しつつ、モノリシックなアーキテクチャが抱える課題を解決しました。
モジュラモノリスに関する情報は日本ではまだまだ少ないのが現状です。今回翻訳したブログはモジュラモノリスの定義からその具体的な移行方法まで非常に詳細に書かれており、私達にとって、また肥大化したモノリスに課題を感じている多くの他の組織にとっても参考になると考えました。
Shopify社には掲載をご快諾いただき、さらに日本語訳のレビューもしていただけました。ご協力に深く感謝します!
それでは、以下より訳文となります。
※一部、翻訳者による強調表示を加えています。
モノリスを解体する: 開発者の生産性を最大化するソフトウェアの設計
Shopifyは現存する最大規模のRuby on Railsコードベースの一つです。10年以上にわたり、1000人以上の開発者によって開発されてきました。Shopifyには、ストアオーナーへの請求、サードパーティ開発者によるアプリの管理、商品の更新、配送の処理など、多種多様な機能がカプセル化されています。Shopifyは当初はモノリスとして構築されていました。つまり、これらの異なる機能はすべて、それらの間に境界を設けずに同じコードベース上に構築されていました。長年にわたり私たちにとってこのアーキテクチャは機能していましたが、最終的には、モノリスのデメリットがメリットを上回るようになりました。私たちは、この課題にどのように対処するかを決めなければなりませんでした。
マイクロサービスは近年人気が急上昇し、モノリスに起因するすべての問題に対する万能の解決策として売り込まれました。しかし、私たちはこれまでの経験から、万能な解決策というものは存在せず、マイクロサービスにも独自の課題は存在するだろうと考えました。最終的に、私たちは Shopify をモジュラモノリスアーキテクチャに移行することを選択しました。それは、すべてのコードを1つのコードベースに保持しつつ、異なるコンポーネント間の境界が定義され、尊重されるようにすることを意味します。
それぞれのソフトウェアアーキテクチャには長所と短所があるため、アプリケーションの成長段階に応じて異なるソリューションを選択するのが理にかなっています。モノリスからモジュラモノリスへの移行は、私たちにとって合理的な次のステップでした。
モノリシックアーキテクチャ
ウィキペディアによると、モノリシックアーキテクチャとは、機能的に区別できるシステムのさまざまな要素がアーキテクチャとして別々のコンポーネントに分離されているのではなく、すべてが1つに組み合わされたものとなっているアーキテクチャ、とされています。これがShopifyにとって何を意味するかというと、配送料金を計算するコードと支払い手続きを処理するコードが同じコードベース上に存在していて、それら相互の呼び出しを禁止するのはほぼ不可能だということです。時間の経過とともに、これらの異なるビジネスプロセスを処理するコードは極めて密結合な状態になっていきました。
モノリシックシステムの長所
モノリシックアーキテクチャは実装が最も簡単なアーキテクチャです。アーキテクチャが適用されていない場合、結果としてモノリスになる可能性が高いでしょう。特にRuby on Railsではアプリケーションレベルですべてのコードがグローバルに利用可能なため、モノリシックアーキテクチャを構築するのに適しています。モノリシックアーキテクチャは構築が容易なため、初期段階からチームが迅速に動き、いち早く買い物客に商品を紹介できるようになります。
コードベース全体を一つのレポジトリで管理し、アプリケーションを一箇所にデプロイすることには多くの利点があります。リポジトリは1つだけで、そのレポジトリ内を検索すればすべての機能を見つけることができ、テストとデプロイのパイプラインも1つしかないので(アプリケーションの複雑さにもよりますが)、多くのオーバーヘッドを回避することができます。これらのパイプラインの作成、変更、保守にはコストがかかりますが、これは、すべてのパイプラインにおける一貫性確保に対する協調的取り組みが必要なためです。すべてのコードが1つのアプリケーションにデプロイされており、データは単一の共有データベースに保存されているので、ちょっとしたデータが必要なときはそのデータベースにシンプルなクエリを投げるだけで済みます。
モノリシックアプリケーションは一箇所にデプロイされるため、管理する必要があるインフラストラクチャのセットは1つだけです。 ほとんどのRubyアプリケーションには、データベース、Webサーバー、バックグラウンドジョブ、あるいはRedis、Kafka、Elasticsearchなどの他のインフラストラクチャコンポーネントが付属しています。 インフラストラクチャのセットが追加されるたびに、開発に割く時間ではなく、DevOpsに割かなければならない時間の方が増えていくでしょう。また、インフラストラクチャの追加は障害点を増やすことにつながり、アプリケーションのレジリエンシーとセキュリティを低下させます。
複数に分割されたサービスではなくモノリシックアーキテクチャを選択する最も魅力的な利点の1つは、Web APIを介した通信を必要とせずに他のコンポーネントを直接呼び出すことができるということです。これは、APIのバージョン管理や下位互換性、遅延の可能性のある呼び出しについて心配する必要がないことを意味します。
モノリシックシステムの短所
しかし、アプリケーションやチームが一定規模以上になると、モノリシックアーキテクチャの許容範囲を超えてしまうでしょう。Shopifyでは2016年にこの問題が発生し、新機能の構築とテスト時の課題が絶えず増加していくことによって明らかになりました。つまり、発生した問題のいくつかが、私たちにとってトリップワイヤー(地雷の仕掛け線)のように重大な警告として発せられたのです。
私たちのアプリケーションは、新しいコードが予想外の影響を及ぼすほど非常に脆弱なものでした。**一見するとなんの変哲もないように見える変更によって、無関係のテストが連鎖的に失敗する可能性がありました。**例えば、配送料を計算するコードが税率を計算するコードに呼び出された場合、税率の計算方法を変更することで配送料の計算結果に影響を与える可能性がありますが、それがどのような理由かはよくわかりません。これは密結合で境界が欠如していたことに起因しており、記述が難しく、CIの実行にも非常に時間がかかるテストの原因にもなっていました。
Shopifyで開発するには、一見単純な変更を加えるために多くのコンテキストを必要としていました。新しいShopifyスタッフがオンボーディングでコードベースを理解する際、彼らが実戦で役立つ存在になる前に取り入れる必要のある情報量は膨大なものでした。例えば、出荷チームに加わった新しい開発者が開発を始められるようになるためには、出荷に関するビジネスロジックの実装さえ理解していればよいはずです。しかし、実際には、注文がどのように作成されるのか、どのように支払いを処理するのかなど、すべてが絡み合っているため、より多くのことを理解する必要がありました。彼らが最初の機能だけをリリースするために頭に入れなければならない知識量は多すぎました。 複雑なモノリシックアプリケーションは、急勾配な学習曲線を描くことがわかります。
**すべての問題は、私たちのコードには異なる機能間の境界線が欠如していたことから発生していました。**ドメイン間の結合を減らす必要があることは明らかでしたが、問題はその方法でした。
マイクロサービスアーキテクチャ
業界で大きなトレンドとなっているソリューションの1つにマイクロサービスがあります。 マイクロサービスアーキテクチャはアプリケーション開発へのアプローチで、大きなアプリケーションが小さなサービスの集合体として構築され、それぞれ独立してデプロイされます。マイクロサービスは、私たちが経験した問題を解決する一方で別のさまざまな問題を引き起こします。
複数の異なるテストとデプロイのパイプラインをメンテナンスする必要があり、サービスごとにインフラストラクチャのオーバーヘッドが増えてしまうにもかかわらず、必要なときに必要なデータに常にアクセスできるとは限りません。各サービスは独立してデプロイされるため、サービス間の通信はネットワークを介することになります。よって、呼び出しが行われるたびに遅延は増加し、信頼性は低下します。 さらに、複数のサービスにわたる大規模なリファクタリングは面倒で、依存するすべてのサービスにおける変更やデプロイの調整も必要になります。
モジュラモノリス
私たちにはデプロイのユニット数を増やすことなくモジュール性を高め、モノリスとマイクロサービスの両方のメリットを、それほど多くのデメリットなしに得ることができる解決策が必要でした。
モノリス vs マイクロサービス by Simon Brown
モジュラモノリスは、すべてのコードが単一のアプリケーションを動かすシステムであり、異なるドメイン間に厳密に強制された境界があるシステムです。
Shopifyにおけるモジュラモノリスの実装: コンポーネント化
モノリシックなアプリケーションとして許容範囲を超えるサイズになったことが明らかになり、それが開発者の生産性と満足度に影響を与えていることを受けて、主な問題点を特定するためにコアシステムで働くすべての開発者にアンケート調査を実施しました。問題があることは分かっていましたが、裏付けのない単独の事例からだけでなく、より確実な解決策を打ち出すために、データに基づいた解決策を提案したいと考えたからです。
調査結果を受けて、私たちはコードベースを分割することを決定しました。2017年初頭、この課題に取り組むために、小さいながらも強力なチームが結成されました。このプロジェクトは当初 “Break-Core-Up-Into-Multiple-Pieces” と名付けられ、最終的には “Componentization” へと発展することになりました。
ディレクトリ構造の再編成
彼らが最初に取り組むことを選んだ課題はアプリケーションのディレクトリ構造の再編成でした。当時、私たちのディレクトリ構造はMVCモデルに基づいた典型的なRailsアプリケーションとして構成されていました。再編成の目標は、コードを実際の概念(注文、配送、在庫、請求書など)別に再編成することで、コードを見つけやすくし、コードの理解者を見つけやすくし、独立したものとして理解できるようにすることでした。**各コンポーネントはそれぞれ独自のミニRailsアプリとして構造化され、最終的には、Rubyのモジュールを名前空間として使用することを目標としました。**この新しいディレクトリ構成によって、不必要なコンポーネント間の結合が浮き彫りにされることを期待しました。
最初のコンポーネントの一覧を作成するには、社内の各分野のステークホルダによる多くの調査と意見が必要とされました。**私たちはすべてのRubyクラス(合計約6,000個)を巨大なスプレッドシートにリストアップし、それがどのコンポーネントに属するかを手動でラベリングしました。**ディレクトリの再構成では、コード自体に変更がなかったとはいえ、コードベース全体に影響を及ぼし、間違った方法で行うと非常に危険な可能性がありました。私たちはこの変更を自動スクリプトで作成されたPRで一度に達成しました。変更点は単にファイルの移動だけなので、エラーが起こるとしたらオブジェクトの定義を参照できないケースで、その場合は実行時エラーになります。
私たちは開発環境とCIでテストが通ることを確認し、開発環境とステージング環境で可能な限り多くの機能の実行など綿密なテストを行い、見落としがないことを確認しました。そして、開発者の混乱を可能な限り少なくするために、1つのPRですべてを行うことを選択しました。この変更で唯一の残念な点は、ファイルの移動がリネームではなく削除と作成として誤って判断されてしまい、GitHubのGitの履歴の多くを失ってしまったことです。git -follow
オプションを使えば、ファイルの移動の履歴を追跡することができますが、GitHub はこの機能に未対応です。
依存関係の分離
次のステップは、ビジネスドメインを互いに切り離して依存関係を分離することでした。各コンポーネントはパブリックAPIを通した専用のクリーンなインターフェイスによって定義され、関連するデータの排他的な所有者となります。コンポーネントの定義には各ビジネスドメインの専門家を必要とするため、プロジェクトチームはShopifyのコードベース全体を改修することはできませんでしたが、彼らはパターンを定義し、タスクを完了させるためのツールを提供しました。
依存関係の分離というゴールに向けて、**各コンポーネントの進行状況を追跡するために、私達はWedgeという社内ツールを開発しました。Wedgeは、ドメイン境界の違反(他のコンポーネントがその公開定義されたAPI以外のものを介してアクセスされた場合)や、ドメイン境界をまたいだデータの結合を検出します。**これを実現するために、CI中にRubyトレースポイントにフックして完全なコールグラフを取得するツールを作成しました。 そして、コンポーネントごとに呼び出し元と呼び出し先をソートし、コンポーネントの境界を越えた呼び出しのみを選択してWedgeに送信しています。また、同時にActiveRecordの関連や継承など、コードの解析結果から追加データを送信します。Wedgeはコンポーネントの境界をまたぐもの(呼び出し、関連、継承)のうちどれがOKでどれが違反しているかを判断します。
例:
- コンポーネントをまたぐ関連は常に違反とする
- 明示的に公開されているAPIのみ呼び出しを許可する
- 継承も同様の扱いとする(が、まだ完全には実装されていない)
そして、Wedgeはコンポーネントごとに違反するものをリストするだけでなく、総合的なスコアも計算します。
ShopifyのWedge - 各コンポーネントのゴールへの進捗状況を追跡する
コンポーネントの境界を強制する
私たちは長期的にはこれをさらに一歩進めて、コンポーネントの境界を仕組みによって強制したいと考えています。Dan Manges氏によるブログでは、あるアプリチームがどのようにしてドメイン境界の強制を達成したかを詳細な事例として紹介しています。私たちのアプローチはまだ検討中ですが、大まかな計画では、各コンポーネントは明示的に依存している他のコンポーネントのみをロードするようにする予定です。これによって、依存関係を宣言していないコンポーネントのコードにアクセスしようとすると、ランタイムエラーが発生します。また、コンポーネントが公開API以外のものを介してアクセスされた場合に、ランタイムエラーやテストの失敗を引き起こすようにすることもできます。
さらに、意図せず生じた偶発的な依存関係や循環的な依存関係を削除することで、ドメインの依存関係グラフを解きほぐしていきたいと考えています。コンポーネントの完全な分離はShopifyの開発者全員がコミットしている課題で、進行途中ではあるもの、期待された成果はすでに出始めています。例えば、私たちはレガシーな税務計算エンジンを使用していましたが、ストアのニーズ応えられるものではありませんでした。この記事で紹介した取り組みを行う前は、古いシステムを新しいシステムに置き換えることはほぼ不可能な作業だったでしょう。しかし、依存関係の分離に大きく力を注いだ結果、私たちは新しい税務計算システムに置き換えることが可能になりました。
結論として、システムの初期段階において、最初のアーキテクチャがベストなアーキテクチャであることはめったにありません。これは、優れたソフトウェアプラクティスを実戦してはいけないと言っているわけではありませんが、まだ熟知していない複雑なシステムの設計に数週間から数か月もの時間を費やすべきではありません。Martin Fowler氏のDesign Stamina Hypothesisは、ほとんどのアプリケーションの初期段階では、最小限の設計でとても速く動くことができることを解説して、この考えを的確に説明しています。設計の品質より市場に公開するまでの時間を優先することは現実的です。機能や機能を追加するスピードが遅くなり始めた時こそ、良い設計に投資するべき時なのです。
リファクタリングやリアーキテクトを行うベストなタイミングは、できる限り遅く、です。なぜなら、あなたは開発をするとともに、システムとビジネスドメインについて常に学んでいるからです。ドメインの専門知識がないうちにマイクロサービスの複雑なシステムを設計するのは、多くのソフトウェアプロジェクトが陥りやすいリスクの高い行動です。Martin Fowler氏によると、「私が聞いたことのあるほとんどすべてのケースでは、ゼロからマイクロサービスシステムとして構築されたシステムは深刻なトラブルに見舞われています…たとえアプリケーションが十分に価値のあるものになると確信していても、マイクロサービスで新しいプロジェクトを始めるべきではありません」。
良いソフトウェアアーキテクチャとは常に進化するものであり、アプリケーションに適したアーキテクチャは、そのアプリケーションがどのような規模で運用されているかに強く依存します。モノリス、モジュラモノリス、またサービス指向アーキテクチャは、アプリケーションの複雑性の増加に伴う進化的アプローチとして分類できます。それぞれのアーキテクチャは、それぞれ異なる大きさのチーム/アプリケーションに適しており、また苦難を引き起こす時期も異なります。本記事で取り上げたさまざまな問題を感じ始めたとき、それは今のアプリケーションやチームの規模にアーキテクチャが追いついていないことを示すものであり、次を検討すべき時期が来たということを意味します。
Monolith vs Microservices画像の使用許可についてSimon Brownに感謝します。Modular Monolithの詳細については、GOTO18でのSimonの講演をご覧ください。
——
当社では常に人材を募集しており、あなたからのご応募をお待ちしています。エンジニアのキャリアページで募集中のポジションをご覧ください。
-
さらに、「マイクロサービスは組織論」 とも言われるように、多くのケースで組織構造とアーキテクチャは不可分であるため、両面的なアプローチが必要になります。 ↩