
マイクロサービスのハネムーンは終わりました。Uberは数千ものマイクロサービスをより扱いやすい構成へとリファクタリング中であり[1]、Kelsey Hightowerは「モノリスこそ未来」と語り[2]、あのSam Newmanまでもが「マイクロサービスはデフォルトの選択肢ではなく最後の手段だ」と明言しています[3]。
いったい何が起きているのでしょうか。シンプルさと柔軟性をうたったはずのマイクロサービスを採用しながら、なぜこれほど多くのプロジェクトが手に負えなくなってしまったのか。あるいは、結局のところモノリスのほうが優れているのでしょうか。
本記事では、こうした問いに向き合います。マイクロサービスを「分散版ビッグ・ボール・オブ・マッド」へと変えてしまう典型的な設計上の落とし穴と、それを回避するための考え方を整理していきます。
その前にまず、「モノリスとは何か」を改めてはっきりさせておきましょう。
モノリス
マイクロサービスは常に、モノリシックなコードベースに対する処方箋として位置づけられてきました。しかし、モノリスは本当にそれほど問題なのでしょうか。Wikipediaの定義[4]によれば、モノリシックアプリケーションとは「自己完結型で、他のコンピューティングアプリケーションから独立しているもの」です。他のアプリケーションからの独立 ― それこそ、マイクロサービスを設計するときに、しばしば実現できないまま追い求めているものではないでしょうか。David Heinemeier Hansson[5]は、こうしたモノリス批判に早々に異を唱えました。彼は分散システムに付きまとう負債と難しさを指摘し、Basecampを例に、数百万人の顧客を抱える大規模システムでもモノリシックなコードベースで実装・保守しうることを示しました。
つまり、マイクロサービスはモノリスを「直す」ものではありません。マイクロサービスが本来解決すべき課題は、ビジネス目標を達成できないという状況そのものです。チームがビジネス目標を果たせない原因の多くは、変更コストが指数関数的に膨らむ ― さらに悪い場合には予測不能になる ― ことにあります。要するに、システムがビジネスのスピードに追いつけなくなっているのです。そして、この制御不能な変更コストはモノリスに固有の性質ではなく、むしろビッグ・ボール・オブ・マッド[6]の特徴です。
ビッグ・ボール・オブ・マッドとは、無秩序に組み上がり、肥大化し、雑然とした、ガムテープと針金で繋ぎ止めたスパゲッティコードのジャングルである。これらのシステムには、無計画な成長と場当たり的な修繕が繰り返されてきた紛れもない痕跡が見える。情報はシステムの遠く離れた要素間で無分別に共有され、しばしば重要な情報のほぼすべてがグローバル化するか、重複して持たれる状態にまで至る。
ビッグ・ボール・オブ・マッドを変更・進化させる難しさは、複数チームの作業調整、相反する非機能要件、込み入ったビジネスドメインなど、さまざまな要因から生じます。いずれにせよ、私たちはこの複雑さに立ち向かうため、扱いにくい既存システムをマイクロサービスへと分解しようとするわけです。
マイクロ ― 何が「マイクロ」なのか?
「マイクロサービス」という言葉は、サービスの何かを計測でき、その値を最小化すべきだ、というニュアンスを含んでいます。では、マイクロサービスとは具体的に何を指すのでしょうか。よく使われる切り口をいくつか見ていきましょう。
マイクロチーム
1つ目は、サービスを担当するチームの規模です。そして、その指標はピザで測られるべきだといいます。冗談ではなく、本当にそう言われているのです。サービスを担当するチームが2枚のピザでお腹を満たせるなら、それはマイクロサービスである、と。私はこの基準を眉唾なものだと思っています。なにしろ、ピザ1枚で十分まかなえる人数のチームでプロジェクトを作ったこともありますが、その成果物を「マイクロサービス」と呼べる勇者がいるなら、ぜひお目にかかりたいものです。
マイクロコードベース
もう1つの広く知られたアプローチは、コードベースのサイズを基準にマイクロサービスを設計することです。これを極端に推し進め、サービスのサイズを特定の行数に収めようとする人もいます。とはいえ、マイクロサービスを成立させる「正しい行数」はいまだに見つかっていません。ソフトウェアアーキテクチャのこの聖杯が見つかった暁には、次の問いに進むことになるでしょう ― マイクロサービス開発に推奨されるエディタ幅は何文字か?
もう少し真面目な話をしましょう。極端でない形では、このアプローチが現に主流になっています。コードベースの大きさは、それがマイクロサービスかどうかを判断するヒューリスティックとしてしばしば使われます。
ある意味で、これは理にかなっています。コードベースが小さいほど、扱うビジネスドメインの範囲も狭くなり、理解、実装、進化が容易になるからです。さらに、コードベースが小さければビッグ・ボール・オブ・マッド化する確率も下がりますし、仮にそうなってもリファクタリングしやすくなります。
しかし残念ながら、その「シンプルさ」はあくまで幻想です。サービスをそれ自体だけで評価するとき、私たちはシステム設計の核心を見落としています。そのサービスを構成要素として含むシステムそのものの存在を、忘れてしまっているのです。
「サービスの境界を決めるうえで役立つ、示唆に富むヒューリスティックは数多くある。サイズはその中で最も役に立たない部類の一つだ。」 ~ Nick Tune
私たちが作っているのは「システム」だ!
私たちが作っているのはシステムであって、サービスの寄せ集めではありません。マイクロサービスベースのアーキテクチャを採用するのは、システム全体の設計を最適化するためであり、個々のサービスの設計を最適化するためではないのです。誰が何と言おうと、マイクロサービスが完全に疎結合になることも、完全に独立することもありません。独立した部品からシステムを組み立てることは、そもそも不可能です。それは「システム」という言葉の定義そのものに反します[7]。
1. つながり合い、連動して動くものや装置の集合
2. 特定の目的のために組み合わせて使われるコンピュータ機器およびプログラムの集合
サービスは、システムを成り立たせるためにどうしても互いにやり取りせざるを得ません。サービス単位で最適化を進め、サービス間の相互作用を無視したまま設計を続ければ、行き着く先はおそらくこうなります。

個々の「マイクロサービス」はシンプルかもしれませんが、システム全体は複雑性の地獄です。
では、サービス単体だけでなく、システム全体の複雑性にも対処できるマイクロサービスを、どう設計すればよいのでしょうか。
難問ですが、幸いなことに、その答えはずっと以前から知られています。
システム全体から見た「複雑性」
40年前にはクラウドコンピューティングも、グローバル規模の要件も、11.7秒ごとにデプロイする必要もありませんでした。それでもエンジニアたちは、システムの複雑性を飼いならさなければなりませんでした。当時の道具立ては今と異なりますが、課題と ― さらに重要なことに、その解決策は ― 今日でも色あせず、マイクロサービスベースのシステム設計にも応用できます。
Glenford J. Myersは著書『Composite/Structured Design』[8]で、複雑性を抑えるために手続き型コードをどう構造化すべきかを論じています。本書の冒頭ページで、彼はこう書いています。
複雑性という主題には、プログラム各部のローカルな複雑性を最小化することよりも、はるかに重要な観点がある。それがグローバルな複雑性、すなわちプログラムやシステム全体の構造の複雑性(つまり、プログラムの主要な部分どうしの結びつきや相互依存の度合い)である。
本記事の文脈に置き換えると、ローカルな複雑性は個々のマイクロサービスの複雑性であり、グローバルな複雑性はシステム全体の複雑性を指します。ローカルな複雑性はサービスの実装に左右され、グローバルな複雑性はサービス間の相互作用と依存関係によって決まります。
では、ローカルとグローバル、どちらの複雑性がより重要なのでしょうか。片方だけに目を向けたとき、何が起きるのかを見ていきましょう。
グローバルな複雑性を最小化するのは、驚くほど簡単です。システムの構成要素間のやり取りをすべて取り除けばよい ― つまり、すべての機能を1つのモノリシックなサービスに実装するのです。先ほど触れたように、この戦略がうまくはまる場面もあります。一方で、別の状況では、悪名高いビッグ・ボール・オブ・マッド ― ローカルな複雑性のほぼ最高地点へとまっしぐらに突き進むことになります。
反対に、ローカルな複雑性ばかりを最適化し、グローバルな複雑性をないがしろにすればどうなるか。答えは分かりきっています。さらに恐ろしい、分散版ビッグ・ボール・オブ・マッドの誕生です。

つまり、片方の複雑性だけに集中するなら、どちらを選んでも結果は変わりません。それなりに複雑な分散システムでは、もう一方の複雑性が一気に跳ね上がるからです。だからこそ、片方だけを最適化することはできず、ローカルとグローバルの両方を釣り合わせる必要があるのです。
興味深いことに、『Composite/Structured Design』が示す複雑性のバランスの取り方は、分散システムだけでなくマイクロサービスの設計にも示唆を与えてくれます。
マイクロサービス
まずは、ここで言う「サービス」と「マイクロサービス」が何を指すのかを、改めて定義しておきましょう。
サービスとは何か?
OASIS Standard[9]によれば、サービスとは次のように定義されます。
1つまたは複数の機能(capability)へのアクセスを可能にする仕組みであり、そのアクセスは規定されたインターフェースを通して提供される。
「規定されたインターフェース」という部分が肝です。サービスのインターフェースは、外部に対して公開する機能を定義します。Randy Shoup[10]は、サービスの公開インターフェースとは「サービスにデータを出し入れするためのあらゆる仕組みのこと」だと述べています。同期型(典型的なリクエスト/レスポンス)でも、イベントを発行・購読する非同期型でもかまいません。同期か非同期かを問わず、公開インターフェースとはサービスに対してデータを出し入れする手段にほかならないのです。Randyはまた、サービスの公開インターフェースをそのサービスの正面玄関になぞらえています。
サービスはその公開インターフェースによって定義される ― この一点さえ押さえておけば、何が「マイクロ」サービスを成立させるのかも自ずと見えてきます。
マイクロサービスとは何か?
サービスがその公開インターフェースで定義されるのなら ―
マイクロサービスとは、マイクロな公開インターフェース ― マイクロな正面玄関を持つサービスである。
このシンプルな指針は手続き型プログラミングの時代から踏襲されてきたものですが、分散システムの世界においても十分以上に通用します。公開する範囲が小さいほど実装はシンプルになり、ローカルな複雑性も下がります。グローバルな複雑性の観点でも、公開インターフェースが小さければサービス間の依存や接続は減ります。
マイクロインターフェースという発想は、「マイクロサービスは自分のデータベースを外部に公開しない」という広く知られた慣行も説明してくれます。あるマイクロサービスが別のマイクロサービスのデータベースに直接触れることはなく、必ずその公開インターフェースを通すのが原則です。なぜなら、_データベースをそのまま開放してしまえば、それ自体が巨大な公開インターフェースになってしまう_からです。リレーショナルデータベース上で実行できる操作の多さを考えれば、納得できるでしょう。
つまり、分散システムにおいては、サービスの公開インターフェースを最小化することでローカルとグローバルの複雑性を釣り合わせ、サービスを「マイクロ」なサービスとして成立させているのです。
注意点
この指針は、一見すると拍子抜けするほどシンプルです。マイクロサービスがマイクロな公開インターフェースを持つサービスにすぎないのなら、いっそ公開インターフェースをメソッド1つに絞ればよい、と。正面玄関がそれ以上小さくなりようがない以上、それこそ理想のマイクロサービスのはず ― そう思えるかもしれません。_しかし、そうはいきません。_その理由を示すために、本テーマに関する以前の記事[11]から例を引きます。
たとえば、次のようなバックログ管理サービスがあるとしましょう。

これを8つのサービスに分け、それぞれが公開メソッドを1つだけ持つようにすれば、ローカルな複雑性は完璧と言えるサービス群が出来上がります。

では、これらをつなげてバックログを実際に管理するシステムを組み立てられるでしょうか。_答えはノーです。_システムとして機能させるには、サービス同士がやり取りし、各サービスの状態変化を共有しなければなりません。_しかし、それができないのです。_サービスの公開インターフェースが、それを支えていないからです。
そこで、サービス間の_連携を可能にする_公開メソッドを_正面玄関_に追加していくことになります。

結果はご覧の通りです。サービスごとの複雑性だけを最適化していれば、素朴な分割は見事に成立します。ところが、それらをシステムとしてつなごうとした途端、グローバルな複雑性が牙をむきます。出来上がったシステムが絡まり合った混沌になるだけでなく、当初想定していなかった範囲まで公開インターフェースを広げざるを得なくなる ― 連携のためにです。Randy Shoupの言い方を借りれば、小さな正面玄関の傍らに、私たちは_巨大な_「関係者通用口」を作ってしまったのです。ここから、重要なポイントが導かれます。
あるサービスにおいて、ビジネス関連のメソッドより連携(統合)関連のメソッドのほうが多い ― これは、分散版ビッグ・ボール・オブ・マッドが育ちつつあることを示す強力なサインである。
つまり、サービスの公開インターフェースをどこまで小さくできるかという閾値は、サービス単体の事情だけでなく、(むしろ主に)そのサービスが組み込まれるシステム側の事情によって決まります。マイクロサービスへの適切な分解は、システム全体のグローバルな複雑性と、各サービスのローカルな複雑性の両方を_釣り合わせる_ものでなければなりません。
サービス境界の設計
「サービスの境界を見つけるのは、本当に死ぬほど難しい… フローチャートなんて存在しないんだ!」 ― Udi Dahan
このUdi Dahanの言葉は、マイクロサービスベースのシステムにこそ強く当てはまります。マイクロサービスの境界設計は難しく、最初から正解にたどり着くことはまず不可能です。だからこそ、それなりに複雑なマイクロサービスベースのシステム設計は反復的なプロセスにならざるを得ません。
そう考えると、まずは広めの境界 ― おそらく適切な bounded context[12] の単位 ― から始め、システムやビジネスドメインに対する理解が深まるにつれてマイクロサービスへと分解していくほうが安全です。これはとくに、コアなビジネスドメイン[13]を扱うサービスについて言えることです。
分散システム以外の世界に見る「マイクロサービス」
マイクロサービスという呼び名こそ比較的新しいものですが、同じ設計原則の実装は、ほかの分野にもいくらでも見つかります。代表的なものをいくつか取り上げてみましょう。
クロスファンクショナルチーム
クロスファンクショナルチームが最も効果的なチーム形態であることは、よく知られています。同じ課題に取り組む多様な専門家からなる集団であり、効率的なクロスファンクショナルチームは、チーム内のコミュニケーションを最大化し、チーム外とのコミュニケーションを最小化します。
業界がクロスファンクショナルチームに注目し始めたのは比較的最近ですが、タスクフォースという形態は昔から存在していました。その根底にある原則は、マイクロサービスベースのシステムとまったく同じです。すなわち、チーム内の高い凝集度と、チーム間の低い結合度。タスクの遂行に必要なスキルをチーム内に取り込む(つまり実装の詳細を内側に閉じ込める)ことで、チームの「公開インターフェース」を最小化しているわけです。
マイクロプロセッサ
この例は、Vaughn Vernonの素晴らしい同テーマのブログで見つけたものです。彼はその記事の中で、マイクロ_サービス_とマイクロ_プロセッサ_の興味深い類似に光を当てています。とくに、プロセッサとマイクロプロセッサの違いについて、こう述べています。
中央処理装置(CPU)がマイクロプロセッサと呼べるかどうかを判断するうえで、サイズの分類があるというのは興味深い ― それはデータバスの幅である[21]。
マイクロプロセッサのデータバスはまさにその公開インターフェースであり、マイクロプロセッサとほかの構成要素との間でやり取りできるデータ量を決めています。CPUがマイクロプロセッサと見なされるかどうかについて、公開インターフェースの厳密なサイズ分類が存在しているわけです。
Unix哲学
Unix哲学(Unix way)とは、ミニマルでモジュラーなソフトウェア開発に対する文化的規範と哲学的アプローチの集合です[22]。
「Unix哲学は、完全に独立した部品からはシステムを組めないという主張と矛盾しているのではないか」と感じる方もいるでしょう。_Unixのプログラムは互いに独立しているのに、それらを組み合わせて立派なシステムを作っているではないか?_と。実は、話はむしろ逆です。Unix wayは、プログラムがマイクロインターフェースを公開すべきことを、ほとんど文字どおりに定めています。Unix哲学の原則をマイクロサービスの考え方に重ね合わせて見てみましょう。
第一原則は、本来の目的と無関係な機能を抱え込ませるのではなく、プログラムの公開インターフェースが一貫した1つの機能だけを担うことを求めています。
各プログラムには1つのことを上手にやらせよ。新しい仕事をしたければ、新しい「機能」を継ぎ足して古いプログラムを複雑にするのではなく、ゼロから作り直せ。
Unixコマンドは互いに完全に独立していると見なされがちですが、実際にはそうではありません。それでも他のコマンドとやり取りする必要があり、第二原則はその通信インターフェースの設計指針を定めています。
あらゆるプログラムの出力は、まだ知られていない別のプログラムへの入力になりうると想定せよ。余計な情報で出力を散らかすな。厳格な桁揃えやバイナリ入力形式は避けよ。インタラクティブな入力に固執するな。
通信のインターフェースが厳しく絞り込まれている(標準入力、標準出力、標準エラー)だけでなく、コマンド間で受け渡されるデータも厳格に制限されるべきだ、というわけです。すなわち、Unixコマンドはマイクロインターフェースを公開し、互いの実装の詳細に依存してはならない、という原則です。
_ナノ_サービスはどうか?
「ナノサービス」という言葉は、しばしば「小さすぎるサービス」を指す批判的なニュアンスで使われます。先ほどの例にあった、メソッド1つだけの素朴なサービスはナノサービスだ、と評する向きもあるでしょう。しかし、私はその分類に必ずしも賛同しません。
「ナノサービス」は、それを取り巻くシステム全体を視野に入れず、個々のサービス単体だけを見て使われがちな言葉です。先ほどの例でも、システムを式に組み込んだ瞬間、サービスのインターフェースは拡張せざるを得ませんでした。実際、もとの単一サービスの実装と素朴な分解後の状態を比べてみると、サービスをシステムへとつないだ時点で、システム全体の公開メソッド数は8から38へと増えていることがわかります。サービスあたりの平均公開メソッド数も、目指していた1から一気に4.75まで跳ね上がっています。
つまり、コードベースではなくサービス(=公開インターフェース)を最適化対象に据えるなら、ナノ-サービスという呼び名はもはや成立しません。なぜなら、_サービス_はそれが属するシステムのユースケースを支えるために、結局また大きくならざるを得ないからです。
これだけで十分か?
いいえ。サービスの公開インターフェースを最小化することはマイクロサービス設計の優れた指針ですが、それでもあくまで一つのヒューリスティックであり、常識の代わりにはなりません。実のところマイクロインターフェースという考え方は、より根本的でありながら、はるかに複雑な設計原則 ― すなわち結合度と凝集度 ― に対する一種の抽象化にすぎないのです。
たとえば、2つのサービスがそれぞれマイクロな公開インターフェースを持っていたとしても、分散トランザクションで歩調を合わせなければならないのなら、両者の結合度は依然として高いままです。
とはいえ、マイクロインターフェースを目指すことは、機能的・開発的・意味的といったさまざまな種類の結合に対処する強力な指針であることに変わりはありません。ただ、その詳細はまた別の記事のテーマとしましょう。
理論から実践へ
残念ながら、ローカルな複雑性とグローバルな複雑性を客観的に定量化する手段は、まだありません。一方で、分散システムの設計を改善するために役立つ設計上のヒューリスティックなら、いくつか手元にあります。
本記事の主旨はシンプルです。サービスの公開インターフェースを、次のような問いを立てて継続的に評価してください。
- そのサービスのエンドポイントのうち、ビジネス志向のものと連携志向のものの比率はどれくらいか?
- サービスの中に、ビジネス的には互いに無関係なエンドポイントが混在していないか?連携用のエンドポイントを新たに増やさずに、それらを2つ以上のサービスに分割できないか?
- 2つのサービスを1つに統合すれば、もとの2サービスをつなぐために追加されたエンドポイントを取り除けないか?
これらの問いを、サービスの境界とインターフェースを設計する際の指針として活用してみてください。
まとめ
最後に、Eliyahu Goldrattの言葉で締めくくりたいと思います。彼は著書のなかで、こう繰り返し述べていました。
「私をどう測るか言ってくれ。そうすれば、私がどう振る舞うかを言ってみせよう」 ~ Eliyahu Goldratt
マイクロサービスベースのシステムを設計するうえでは、何を測り、何を最適化するのかを正しく選ぶことが決定的に重要です。マイクロな_コードベース_やマイクロな_チーム_の境界を設計するほうが、確かに楽ではあります。しかし、私たちが組み上げたいのは_システム_であり、その視点を欠かすわけにはいきません。マイクロサービスとは、個々のサービスではなくシステムを設計する営みなのです。
そして話は冒頭のタイトル ―「マイクロサービスの複雑性を解きほぐす ― 分散システム設計の勘所」― に戻ります。マイクロサービスの絡まりをほどく唯一の方法は、各サービスのローカルな複雑性と、システム全体のグローバルな複雑性を釣り合わせることに尽きるのです。
参考文献
- Uberに関するGergely Oroszのツイート
- Monoliths are the future
- マイクロサービスの第一人者が開発者に警告 ― 流行のアーキテクチャをすべてのアプリのデフォルトにすべきではなく「最後の手段」とせよ
- Monolithic Application(Wikipedia)
- The Majestic Monolith ― DHH
- Big Ball of Mud(Wikipedia)
- 「system」の定義
- Composite/Structures Design ― Glenford J. Myers著
- Reference Model for Service Oriented Architecture
- Managing Data in Microservices ― Randy Shoupの講演
- Tackling Complexity in Microservices
- Bounded Contexts are NOT Microservices
- Revisiting the Basics of Domain-Driven Design
- Implementing Domain-Driven Design ― Vaughn Vernon著
- Modular Monolith: A Primer ― Kamil Grzybek
- A Design Methodology for Reliable Software Systems ― Barbara Liskov
- Designing Autonomous Teams and Services
- Emergent Boundaries ― Mathias Verraesの講演
- Long Sad Story of Microservices ― Greg Youngの講演
- Principles of Design ― Tim Berners-Lee
- Microservices and [Micro]services ― Vaughn Vernon
- Unix Philosophy
本記事は vladikk.com にて2020年4月9日に公開されたものです。