マイクロサービスへの上手な分割手法
ドメイン駆動設計(Domain Driven Design -DDD-)はご存知でしょうか。ご存知ない方は以下のサイトなどを参考にドメイン駆動設計について調べてみてはいかがでしょうか。
ドメイン駆動設計はドメインの知識に焦点をあてた設計手法です。〜略〜 ドメインは「領域」の意味をもった言葉です。ソフトウェア開発におけるドメインは、「プログラムを適用する対象となる領域」を指します。重要なのはドメインが何かではなく、ドメインに含まれるものが何かです。
ドメイン駆動設計には戦略的 DDD と戦術的 DDD が存在します。戦術的 DDD の中にはアプリケーションのアーキテクチャで有名なヘキサゴナルアーキテクチャ(やその派生のクリーンアーキテクチャ)、ドメインイベントなどがあります。
ブログなどで DDD が取り上げられる場合多くのケースが後者の戦術的 DDD の方です。今回は戦略的 DDD を中心にマイクロサービスの分割方法について取り上げてみたいと思います。
前提
今回は「本」を扱う EC サイトの開発を例にとってみたいと思います。
DDD を採用していないケース
DDD を採用しないシステム開発をしていた場合、作りたいシステム全体でデータモデルを設計していくことになるのではないかと考えています。
例えば、本を扱う EC サイトの場合、最初は「基本機能」のみで実装することになるのではないかと考えます。
「基本機能」は左のようなデータモデルを扱っています。当初はこれで問題ありませんでしたが、途中で在庫を管理するための機能を追加してくれという要望が出てきました。
「基本機能」に「在庫管理機能」が追加されることになりましたが、「在庫管理機能」は最初から所持しているプロパティ「在庫数」に加えて「在庫位置」も管理するような設計になりそうです。
このように機能追加がなされていく中で徐々にデータモデルは肥大化していきます。
「商品モデル」にはある機能にとっては興味のあるプロパティ、ある機能にとっては興味のないプロパティというものが混在してしまっています。結果として以下の動画のようなことが発生します。
ではどうしたら良いのでしょうか。
DDD パターン 境界づけられたコンテキスト
業務にはその領域におけるエキスパートが存在します。事業領域のエキスパートは(多くの場合)、事業戦略を達成するように働くものです。
- 商品を仕入れる人 : この本は売れるのか?人気の作者か?
- マーケティング : この本を早々に売ってしまいたい。年度末にこれくらい売り上げたい。
- 在庫を管理する人 : この本の在庫位置を知りたい。在庫の引っ越しにも柔軟に対応したい。
彼らは同じ「本」を見ていますが、その「本」に対して興味があるプロパティは異なることがわかりますでしょうか。
- 商品を仕入れる人 : 本そのもの
- マーケティング : クーポンのように見えている?
- 在庫 : 段ボールのように見えている?
「商品を仕入れる人」にとっての「本」や「マーケティング」にとっての「本」、「在庫」にとっての「本」というのは少しずつ見え方が変わるようです。「本」という言葉の意味が変わる境界がありそうです。
言葉の意味が変わる境界のことを「境界づけられたコンテキスト」「Bounded Context」と呼びます。
このように言葉の意味が変わる境界でデータモデルを分割すると、その境界にとって興味のあるプロパティしか出現しません(ISBN は全てのもモデルにとって主キーになるので重複出現)。
「Book」、「Coupon」、「Inventory」、「取り扱い商品を検索」等はDDD では「境界づけられたコンテキスト」内で一意に認識可能な「ユビキタス言語」と言います。
例え、「Book」という言葉を全てのコンテキストで使っていたとしても、コンテキストを超えると意味が変わる点に注意が必要です。
マイクロサービスへの分割
マイクロサービスの分割はこの「境界づけられたコンテキスト」を一つの目安とすると良いと思います。ポイントはデータとビジネスロジックを「境界づけられたコンテキストで分断」してデータやロジックの責務を分割する点にあります。
マーケティングにとっての興味のあるプロパティは、以下になります。
- ISBN
- 割引率
- 割引期間
このプロパティは「商品を仕入れる人」や「在庫」にとってはさっぱり興味のないプロパティです。言い換えると、「商品を仕入れる人」や「在庫」では扱うべきプロパティではないと言うことです。
「マーケティング」(チーム)が責任を持って管理していくべきデータになり、「マーケティング」の人が自身のサービスのためにこれらのプロパティをころころ変えたとしても、他の「境界」には極力影響がないような分断がなされています。
仮に途中で「ポイント機能」(マーケティングの責務)を追加したとしましょう。
このようなケースでも他の「カタログ」サービスや「在庫」サービスには影響を与えることはありません。これは後述するモジュラーモノリスの場合でも同じです。
データモデルとロジックを分割させてしまうケース
RDBMS を単純な CRUD API 化することを推奨しているやり方がありますが、個人的にはお勧めできません。問題点を具体的に見ていきたいと思います。
システム開発をおこなう際にデータモデルから考えるケースが多いと思います。例えば、上記の「本」を考えると左のようなデータモデルになっていることが多いのではないでしょうか。「Book マスター」などという名称が付けられているケースもあります。
このデータモデルをそのまま API として CRUD 操作できるようにしたらどうなるでしょうか。
このモデルにはまだ「マーケティング」が興味のあるプロパティが付与されていませんが、マーケティングが途中でプロパティを付与して欲しいとなった場合を見てみましょう。
「マーケティング」が興味のあるプロパティは「マーケティング」の境界内に閉じ込めないと影響範囲が他のサービスに染み出してしまいます。RDBMS を CRUD API 化しただけだと、データモデルの変更が様々な「ロジック」に染み出してしまい、1箇所の変更に「数ヶ月かかります」というような事態を引き起こします。
これは以下の記事で言及されています。
ヘキサゴナルアーキテクチャを使う
ビジネスロジックがユーザーインターフェース層に出現したり、データベース層に出現したり様々な場所に散らばって密結合になってしまうことを懸念される場合、DDD のヘキサゴナルアーキテクチャを採用する方法があります。
従来の課題
長年にわたるソフトウェアアプリケーションの大きな問題の1つは、ユーザーインターフェイスのコードにビジネスロジックが入り込んでしまうことでした。この問題には3つの側面があります。〜略〜
多くの組織で繰り返されている解決策は、アーキテクチャに新しいレイヤーを作成し、今回は本当に、新しいレイヤーにはビジネスロジックを入れないという約束をすることです。しかし、その約束に違反したことを検知する仕組みがないため、数年後には新しいレイヤーにビジネスロジックが散らばっていて、以前の問題が再発していることに気づくのです。
課題の解決(ヘキサゴナルアーキテクチャ)
ユーザーサイドの問題もサーバーサイドの問題も、実は設計やプログラミングにおける同じミス、つまりビジネスロジックと外部とのインタラクションが絡み合っていることが原因なのです。利用すべき非対称性は、アプリケーションの「左」と「右」の間のものではなく、アプリケーションの「内」と「外」の間のものです。従うべきルールは、「内側」に関連するコードが「外側」に漏れないようにすることです。
実際、「マイクロサービスパターン」という書籍では、早い段階でヘキサゴナルアーキテクチャが紹介され、以降のマイクロサービスの構成はすべてヘキサゴナルアーキテクチャで記載されています。
# 2021/9/12 追記 こちらに関しては、別記事で解説しています。
モジュラーモノリス
個人的な考え方ですが、「データ」と「ロジック」が境界でしっかりと分割されており、同一のデータベースを使うとしても「スキーマ」によってしっかりと責務が分割されていればモノリスでも問題ないと思っています。モジュラーモノリスは運用も複雑化しないためイミュータブルインフラストラクチャの出番は少ないかもしれません。
ただし、この場合は「データベース」「ビルド」「テスト」「デプロイ」などが全機能で共有される点に注意が必要です。詳細は以下のサイトの「チームを分割しても、アプリケーションの複雑さは残る」をご覧ください。個別の機能としてリリースする事はできず、ある機能の遅れによって、全体のリリースタイミングも遅れることになります。この辺は現在抱えているシステムの課題をみながら決めていくことかと思います。
また、 デプロイの単位が一つになるため、個々の機能の単位でカナリアデプロイ、スケールアウトすることもできません。
コンテキストマップ
そうは言っても、あるサービスの影響を自分のサービスが受けることも考えられます。DDD ではドメインを「境界づけれれたコンテキスト」に分割したあと、そのコンテキスト間の関係を図示する「コンテキストマップ」を用意しています。
U は Upsream(上流)、D は Downstream(下流)を意味します。つまりデータは U から D に流れることになります。
- OHS (公開されたホストサービス)
多くの場合、REST API / gRPCを意味しますが、非同期メッセージングの口になることもあります。 - ACL (腐敗防止層)
上流のデータモデルを下流のデータモデルに変換します。上流でデータモデルの変更が行われたとしても、この層があることで変更の影響を直撃することを防ぎます(例えば、ユーザーモデルは全コンテキストで利用されるがそれぞれのコンテキストに合わせて変換する)。 - PL (公表された言語)
XML や JSON などデータの表現方式を表します。
上記の図に現れているもの以外にも様々な関係があります。
- パートナーシップ
コンテキスト同士が相互依存していて、協力してやりとりの方法を考える必要がある場合の関係です。 - 共有カーネル
共有データベースのようなものです(モデルの共有のみだけではなく、ライブラリの共有なども意味します)。RDBMS を単純に CRUD API 化した際の問題点(データモデルの共有)と同様、大抵の場合アンチパターンになります。 - 顧客 / 供給者の開発
コンテキスト間の関係に上下関係(U/D)があるパターンです。上流のチームが成功するには下流の結果に左右されます。例えば、上記の認証アクセスコンテキストはそれ単体としてお金を生み出さないけれども下流のお金を生み出すアジャイルプロジェクト管理コンテキストに使ってもらって初めて成功するといった関係です。
この場合、上流は下流の要望に応える必要が出てきます。 - 順応者
コンテキスト間の関係に上下関係(U/D)があるパターンです。上流のチームが成功するのに下流の結果に左右されません。
したがって、上流は下流の要望に応える必要はありません。 - 巨大な泥団子
コンテキストが複数混ざったシステムを分割するのを諦めて扱うケースです。
サービス間のインテグレーション
このようにうまくサービス同士を分割でき、コンテキストマップをかけたとしても問題が発生することがあります。サービス間のインテグレーションの知識が乏しく、すべてのサービスを REST API / gRPC で実現してしまうと思わぬ落とし穴にハマってしまいます。
たとえば、「商品を仕入れる人」が自身のサービスに「取り扱い商品が増えた」ことを操作可能な API を実装したとします。
「マーケティング」や「在庫」の人は「取り扱い商品が追加された」ことは知りたいと思っています。なぜなら、それぞれ、新しい商品に対して「割引情報を設定したい」、「在庫情報を追加したい」と考えているからです。
これを REST API や gRPC で実装するとどうなるでしょうか。
在庫が何かの原因で止まっていた場合を考えてみましょう。①や③で反映させた DB への変更はどのように元に戻しますか?マイクロサービスではプロセス間を超えた 2フェーズコミットは使うことができません。
では別の問題を見てみましょう。「取り扱い商品が増えた」ことを「カタログ」や「マーケ」、「在庫」に関係する人にメールで通知するサービスを追加しようとしています。REST API で実装するとこんな問題点も出てきます。
「カタログ」は「取り扱い商品を追加した」ことを知りたいサービスが増えるたびにその通知先を加えるための改修をおこなう必要があります。
ドメインイベント
これを解決するのが DDD における「ドメインイベント」になります(こちらは戦術的 DDD に相当するので割愛します)。また、「チェンジイベント」もあります。
本問題の解決方法は弊社が提供する Cloud Native Integration Day でご紹介していますので、興味がある方はぜひ弊社営業へお声がけください。
「境界づけられたコンテキスト」を発見し、そのコンテキスト間の連携方法はコンテキストマップを用いて早いタイミングで検討しておくことをお勧めします。複数コンテキストに関わることなのでステークホルダーが多くなり、後になればなるほど変更が困難になるためです。コンテキスト分割のタイミングで、連携手法は検討しておいた方が良いでしょう。
そのためのワークショップも Red Hat ではご用意しています。
最後に
世の中ではマイクロサービスという言葉が賑わっています。
しかし、ちゃんとした境界でサービスを分割しないと、
- マイクロサービスを適用したのに何も変わらなかった
- マイクロサービスを適用したのに、変更に数ヶ月かかった
- 運用だけ大変になった
などの結果をもたらしかねません。
本記事では触れていませんが、マイクロサービスを適用するにはチームの体制も変化させていく必要があると考えています。詳しくは 2pizza rule、コンウェイの法則、逆コンウェイの法則をお調べいただくとわかるかと思います。
適切な方法で分割していくためのテクニックとして DDD を学習しても良いかもしれません。Netflix のシステムをマイクロサービス化して AWS に持っていったエイドリアン・コッククロフトはマイクロサービスを以下のように定義しています。