- バックエンドはたいてい モノリス で始まるが、複数の機能チームが同じコードベースを触り始めると結合度が高まり、変更速度が遅くなることがある
- マイクロサービスは、ビジネス機能ごとにサービスを分割して 独立デプロイ と小規模チームの自律性を得る方式である
- サービスが小さすぎると利点よりも 運用負荷 が大きくなり、
micro という名前がかえって判断を鈍らせることがある
- 全面的な導入には、標準化、リソースプロビジョニング、リモート呼び出しへの防御、CI/CD、運用、結果整合性 に至るまで幅広いコストが伴う
- 通常はモノリスで境界を十分に学んだうえで、成長痛が大きくなってきたときに1つずつ切り出すほうが安全であり、マイクロサービス優先のアプローチは経験とプラットフォームの準備がある場合に適している
モノリスが大きくなったときに表れる限界
- 現代的なシングルページ JavaScript アプリケーションのバックエンドは、通常 RESTful HTTP API を提供し、リレーショナルデータベースを使う 単一のステートレスな Web サービス として始まる
- このサービスは、異なるビジネス機能を実装するコンポーネントやライブラリで構成される
- 同じコードベースに参加する機能チームが増えるほど、コンポーネント間の結合度が高まり、チーム間で作業領域が重なりやすくなる
- コードベースが十分に複雑になると、誰も全体を完全には理解できなくなり、新機能の実装やバグ修正に時間がかかる
- バックエンドがチーム別ライブラリに分かれていても、ライブラリの変更は結局サービスの再デプロイにつながる
- メモリリークのようなバグが入ると、サービス全体が影響を受ける可能性がある
- 誤ったビルドをロールバックすると、バグを作ったチームだけでなく、すべてのチームの速度に影響する
サービス境界がもたらす自律性
- モノリシックなバックエンドの成長痛を減らす1つの方法は、バックエンドを 独立してデプロイ可能なサービス の集合に分割することだ
- サービス同士は API で通信し、API は同一プロセス内のコンポーネント境界よりも侵しにくい境界を作る
- このアーキテクチャはマイクロサービスアーキテクチャと呼ばれるが、
micro という表現は誤解を招くことがある
- サービスは必ずしも小さくある必要はない
- 機能が少なすぎるサービスは、利点よりも運用負荷のほうを大きくしがちである
- より適切な名前は service-oriented architecture かもしれないが、この名称にも古いイメージがつきまとう
小さなチームと独立サービスが生むスピード
- バックエンドをビジネス機能ごとのサービスに分けると、各サービスを 小規模な単一チーム が開発・運用できる
- 小さなチームは、コミュニケーションコストがチーム規模に応じて 二乗で増加 するため、より効果的である
- 各チームが独自のリリース日程を決め、コードベースを管理すれば、チーム間の調整が減って意思決定が速くなる
- サービスのコードベースが小さいと、開発者にとって理解しやすく、新規入社者の立ち上がり時間も短くなる
- 小さなコードベースは IDE の速度低下を抑え、開発者の生産性向上にも役立つ
- 強いサービス境界のおかげで、開発者はバックエンドの一部を変えるとき、全体ではなく小さな部分だけ理解すればよい
- 各サービスは独立してスケールでき、自身の必要に合った技術スタックを選べる
- API の利用者は、機能が内部でどう実装されているかを気にしない
- システムの他領域に影響を与えずに、新しい技術を試し評価しやすい
- 各マイクロサービスは独自のデータモデルとデータストアを持てる
- 開発者は他サービスに影響を与えずにスキーマを変更できる
マイクロサービスが追加する現実のコスト
- マイクロサービスはシステム全体に可動部を増やし、そのコストは決して無料ではない
- マイクロサービスを全面的に受け入れるコストは、数十の開発チームにわたって 分散償却 できる場合にのみ支払う価値がある
-
開発体験
- サービスごとに異なる言語、ライブラリ、データストアを使うことは可能だが、アプリケーション保守を難しくすることがある
- ソフトウェアスタックが完全に異なると、開発者があるチームから別のチームへ移るのが難しくなる
- ロギングのように全サービスで必要な共通機能を提供するには、採用した各言語向けにライブラリをサポートしなければならない
- 一定レベルの 標準化 が必要である
- 推奨言語や技術ポートフォリオに従うチームに優れた開発体験を提供することで、特定技術の利用を緩やかに促せる
-
リソースプロビジョニング
- 多数の独立サービスを支えるには、サーバー、データストア、その他の汎用リソースを簡単に作成できなければならない
- 各チームがバラバラの方法でリソース作成を行うようにしてはならない
- プロビジョニングされたリソースは再設定も必要であり、これを扱うには相当な 自動化 が必要になる
-
通信
- リモート呼び出しはコストが大きく、システムの壊れ方に新たなパターンを持ち込む
- 障害に備えるには、タイムアウト、リトライ、サーキットブレーカーのような防御機構が必要である
- ネットワーク通信による性能低下を減らすには、非同期処理やバッチ処理も活用しなければならない
- こうした要素はシステムの複雑さを高める
- モノリスでも、リモートクライアントアクセスやサードパーティ API の利用により似た問題は起こりうるが、規模はより小さい
-
継続的インテグレーション、デリバリー、デプロイ
- 継続的インテグレーションでは、自動ビルドとテストの実行後にコード変更がメインブランチへマージされる
- コード変更がマージされたら、本番に近い環境へ自動で公開・デプロイされるべきである
- その環境では統合テストとエンドツーエンドテストを実行し、マイクロサービスが依存先の他サービスを壊していないかを確認する
- 個々のマイクロサービスのテストはモノリスのテストより特別に難しいわけではないが、すべてのマイクロサービスの 統合テスト ははるかに難しい
- 個別サービス同士が相互作用すると、非常に微妙で予想外の挙動が現れることがある
-
運用
- モノリスと違い、各サービス担当チームごとに別個の運用チームを置くのははるかに高コストである
- サービスを開発するチームが、通常そのサービスのオンコールも担当する
- チームは各スプリントで開発作業と運用負荷のどちらを優先するか決めなければならず、その間に摩擦が生じる
- システム障害のデバッグもさらに難しくなる
- アプリケーション全体をローカルマシン上で立ち上げ、デバッガでステップ実行することはできない
- 可動部が多いため、障害の起き方も増える
- あらゆるレベルで優れたロギングとモニタリングが重要になる
-
結果整合性
- アプリケーションを複数のサービスに分けると、データモデルはもはや単一のデータストアだけに存在するわけではない
- 異なるデータストアにあるレコードを原子的に更新し、強い整合性を保証するのは遅く、高コストで、実装も難しい
- このようなアーキテクチャでは通常 結果整合性 を受け入れる必要がある
いつ分割すべきか
- アプリケーションをサービスに分けると、システム全体の複雑さは大きく増す
- 一般的にはモノリスで始め、分割する十分な理由があるときにだけ切り出すのがよい
- サービス間の境界を正しく定めるのは難しいため、モノリスの中で境界を動かしながら適切な地点を探るほうが容易である
- モノリスが十分に成熟し、成長痛が大きくなり始めたら、マイクロサービスを1つずつ切り出せる
- マイクロサービス優先のアプローチは、すでに経験があり、そのためのプラットフォームを構築済みであるか、プラットフォーム構築にかかる時間を織り込んでいる場合にのみ始めるのが適切である
1件のコメント
Hacker News のコメント
記事にはおおむね同意するが、逆に見ると、コストを払う価値がある場合もある。古いサードパーティ依存関係の互換性問題を別サービスとして切り出すことで、苦痛を伴うリファクタリングや自前での再実装を避けられるし、高可用性が求められる中核システムのデプロイ制約が他のシステムのデプロイを妨げないようにでき、大口顧客が変えられない設計上の制約を隔離することもできる。
無料という意味ではなく、単純なモノリスをゴミの山にしたり、危険なデプロイで不安定にしたり、より悪い技術スタックを持つビジネスパートナーに縛られたりしないために、支払う価値のあるコストだという意味。
マイクロサービスとは、内部コンポーネントを基本的にライブラリやクラスではなくサービスとして作るアプローチでは?
機械学習の推論サービスは Python、サーバーサイドレンダリング UI は Rails、入出力と並行性の多いサービスは Go で書く、という具合なら、この3つを別サービスに分ける追加負担は正当化され得る。
マイクロサービスは好きなほうだが、時期尚早な最適化や時期尚早なスケーリングは、後になってまったく別の方向に最適化しなければならなくなったときに生じる技術的負債と同じくらい悪くなり得る。
何かを別サービスとして抽出すべき時点は、そうしなければ許容可能な形で解けない問題が発生したときである。
境界を強制するためにアーキテクチャの複雑さを増やすことは、組織規律の不足に対する解決策にはなり得ないのに、中規模のテック企業はそれを絶えずそのように扱っている。ドメイン境界が弱くて問題が起きているなら、サービスを抽出してもうまく解決しない可能性が高い。
引用: https://www.oreilly.com/library/view/software-architects-han...
テストが通るまで PR をブロックするCI/CDも、組織規律の問題を解決するためのコストの大きい技術的解決策だ。
ドメイン名解決を外部サービスが行うのは妥当だと誰もが認めており、再帰DNS リゾルバとキャッシュをモノリスに統合する人はほとんどいない。ところが、このように昔からある責務分離は、例としてあまり認められない。
問題は、「優秀な」従業員ほど価値を示したい意欲が強すぎて過剰設計につながり、結局地獄になることにあるようだ。
マイクロサービス
grug は、大きな脳を持つ者たちが、システムを正しく分解するという最も難しい問題に取り組み、そこにさらにネットワーク呼び出しまで入れる理由が気になる。
grug には非常に混乱して見える。
https://grugbrain.dev/#grug-on-microservices
「SCALE_REASONS のために FOO_TABLE へのアクセス方式を近代化しようとして、MySQL から DynamoDB に移したいが、59チーム中32チームが FOO_TABLE に直接アクセスしているか、私たちのクラスの private メソッドを直接呼んでいる。優先順位が違うため、それらのチームは FOO_SERVICE を使うように移行したり、シャーディングされたテーブルを使うようにクエリ方式を変えたりできない。これで FOO_TABLE の拡張は複数四半期にまたがる作業になり、各チームがゆっくり更新できるようにしてやらなければならない。1〜2年後には、今燃えている古いやり方を廃止できそうだ。その間はオンコールを楽しめばいい」
マイクロサービスなら、チームはテーブルがスケールしないことに気づくが、データは API で提供されている。次のスプリントでマイグレーションを計画して実行し、API 利用者はずっと速くなったと感じるようになる。
最新の Ruby の REPL を開けば、自動補完や構文チェックの面で、静的型付け言語の IDE に期待するもののかなりの部分を得られる。
隔離は軽く見るべきものではなく、ソフトウェアに入れられる最も強力な機能の一つだ。性能を高め、障害境界を作り、セキュリティ境界を提供できる。
これはアクターモデルの基礎概念と同じだ。2つのコンポーネントが互いのメモリを共有して変更する代わりに、アクターやマイクロサービスのような2つの隔離されたシステムが、定義されたプロトコルだけで通信するようになる。
人々はモジュール性を誤解していると思う。モジュール性は重要だが、もう一つ重要なアーキテクチャ原則として 単一渦原則 があると考えるようになった
ソフトウェアシステムにおける渦とは、データフローのループのこと。データをどこかへ送り、処理された結果を受け取る、あるいは何らかの形でそのデータの影響を受けるなら、そこには渦がある。変更可能な変数は、ごく小さな渦の例である
単一渦原則とは、理想的にはソフトウェアシステムに渦は1つだけであるべき、言い換えれば、すべての構成要素が自分の渦がどちら向きに回っているかを知っているべき、という意味である
2つの渦を成すモジュールを1つに結合するとき、渦の向きが合っていれば合成は簡単で、逆向きなら新しい渦の向きをどうするか決めなければならず厄介になる。だから、すべてのモジュールの渦が同じ向きを持ち、1つの渦を成すのが最もよい
この原則は Flux パターン、CQRS、イベントソーシング、不変性といったアイデアを一般化したものだ
あるいはAで予期しないことが起きてCが詰まることもある。理想的には親は子だけを呼び、親に依存しないべきで、これに失敗すればアーキテクチャも失敗している
アーキテクチャ図を見るとき、戦いの半分は、何が非同期/同期なのか、プッシュ/プルなのか、フローの方向と順序を把握することだ
エンタープライズサービスバス とメッセージキューは、両方向が合わない場所で渦を作るための解決策のように見える
「採用した言語ごとに1つずつ、すべてのサービスに必要なロギングのような共通機能を提供するためにサポートしなければならないライブラリ数」が問題だというくだりがある
これが私たちがマイクロサービスをやめた 最大の理由 だ。2023年のツール、特に純粋なクラウドネイティブや無限スケールのFaaS環境では、こうした心配に精神的帯域を使うのは完全な無駄である。銀行・金融機関のような顧客層は、サードパーティ依存関係の一つ一つを厳しく精査するのでなおさらだ
今はgzipで約250MBの単一のモノリシックな .NET バイナリ配布物で運用しており、ほころびの兆しはまったくない。10〜100MBのSaaS配布物について「自分の exe がもう L2 に収まらない」などと細かな心配をしているなら、安心していい。モノリシックソフトウェアの旅はまだ始まってすらいない
こうしたゴミ山の一つを書き直さなければならない状況になったら、すべてのコミットがグローバルに一貫した一か所にあるほうが、はるかに楽ではないだろうか?
マイクロサービスだからといって無法地帯で、すべてのチームが組織全体を考慮せずに行動してよい、という意味ではない。チーム間の一貫性をある程度保つためのルールはあり得るし、あるべきだ
最高のコードとは、捨てて書き直しやすいコードである。現実的には古いコードほど多くの人の手を経て劣化し、何より誰も保守したがらなくなる
マイクロサービスを好む側は、サービスがどう動作すべきかについて 明確な契約 があり、そのサービスのコードベースがずっと小さいので、変更や書き直しがしやすいと言う
モノリス側は、すべてが大きな糸玉なので、1本を変えるにはその糸が他の糸と絡む箇所だけ分かればよく、変更や書き直しがしやすいと言う
どちらが正しいのかは分からない。何年もコードベースを顕微鏡でのぞき込んできた長期在籍者にはモノリスが、それ以外の人にはマイクロサービスが向いている可能性が高い
リリース管理の観点から見ると、モノリスからマイクロサービスへ移行することは、間違った理由で行われる場合が多い
実際の変更の唯一妥当な理由は、性能ボトルネックによる スケーラビリティ 問題のように見える。それ以外は、ソフトウェア開発の複雑さをシステム保守へ移しているだけだ
もちろん開発者は、他チームと足並みをそろえる大きな負担が減るので喜ぶだろう。だが、機能がいつどのように実装され、マイクロサービス全体で適切にテストされ、本番で有効化され、集中的に管理されるのかを明確にすることは、開発チーム間のコミュニケーションが十分に成熟していなければ、はるかに難しくなる。そして、その未成熟さこそが、そもそもモノリスを分割する実際の理由である場合が多い
逆に開発者が少なく、文脈がよく定義されている状態で複数のマイクロサービスを扱うのは苦痛だ
「それ以外はソフトウェア開発の複雑さをシステム保守へ移しているだけ」という言い方は、ソフトウェアが活発に開発中ならもっともらしい。開発は高くつく。分散システムの維持費が、大規模なモノリスを開発するコストより低いチームもあり得るし、結局は状況次第だ
「モノリスのコスト」という記事はどこにあるのか?ここでは見当たらない。みんなが無知なままマイクロサービスを実装しているから、その問題だけが見えているのだ
みんなが無知なままモノリスを実装していたなら、「モノリスは悪い」という記事がものすごくたくさん見られたはずだ
人々は、2つのシステムが結局同じ量の問題を生み出すという点を理解していない。1頭の象に仕事をさせるのと、1000匹のネズミに仕事をさせることの違いだ。どちらも問題を抱える。ただ問題の種類が違うだけだ
どちらか一方を選んだからといって問題が消えるわけではない。どちらでも問題は起きるし、片方を選んで解決策を用意しなければならない。「どのアーキテクチャか」が最大の障害になっている会社なら、そこで働きたいくらいだ
モノリスがうまく分解されているなら、同じ場所に配置されたマイクロサービスと何が違うのか?おそらくインターフェースだけだろう。関数呼び出しがRPCになり、構成要素を個別に扱える利点、たとえばパッチ適用のために多少のオーバーヘッドを受け入れるということだ
分散マイクロサービスと、同じ場所にあるマイクロサービスは何が違うのか?デプロイはより複雑になるが、プロセスをより適したハードウェアに賢く配置できる。障害モードは増えるが、より高い耐障害性を得られる
ここに万能の答えはない。利点が必要ならコストを払えばよい。こうした マイクロサービス対モノリス 論争は、あるパターンを誤って適用したか、生活を楽にしてくれるツールが足りなかったか、あるいは多くの場合、10年前のソフトウェアをリファクタリングし続けたり再設計したりしなかったために、初期アーキテクチャとは関係なく働くのがつらい状態になったことから来ているのだと思う
「関数呼び出しがRPCになる」という言い方は簡単に見えるが、そうなった瞬間にクライアントアプリは、DNSサーバーにアクセスできない、ホスト名解決の失敗、ホストに到達できない、接続拒否、認証拒否、信頼できないTLS証明書、レート制限超過、301恒久的移動、結果準備の遅延、タイムアウトといった実行経路をすべて処理しなければならない
うまく分解されたアプリであっても、これらを堅牢に処理するには、非同期キュー、リクエストのレート制限、結果キャッシュ、ログ付きバックオフ再試行、設定可能なクライアント資格情報・信頼ストア・リソースURL、失敗ログを可能にするよう再設計する必要がある可能性が高い
ローカル関数呼び出しではコンテキスト内にあったデータを提供するためのRPC関数とパラメータも追加で必要になる。また、モノリスUIは以前なら起こり得なかったネットワーク遅延や失敗をユーザーに伝えなければならない場合もある
最もよく作られたモノリスであっても、マイクロサービスへリファクタリングするには相当な労力が必要だ
このRPC呼び出しは安全に再試行できるのか?指数バックオフは?APIロードバランサーは正しく動いているのか。ならば監視も必要だ。マイクロサービス間のトレーシングはどうするのか。今度はOpenTelemetryのようなものが必要になる
マイクロサービスアーキテクチャでブレークポイントによるデバッグはどれほど難しくなるのか?2つのマイクロサービス間のデータベーストランザクションをどうロールバックするのか?単純な関数呼び出しが突然はるかに複雑になる
この概念を擁護するたびに、サービスが非常に小さくなければならないという要件が生む断片化を計算から抜かしている。より具体的に言えば、モノリスとマイクロサービスが1つあるなら、それはマイクロサービス群ではない。マイクロサービスという言葉は、すべてが小さなサービス群に分割され、モノリスがないことを含意している
マイクロサービス賛成論の大半は、必ず「マイクロ」でなければならないと気づいた瞬間に崩れる。そしてその要件を捨てると、新しくも深くもある提案は何も残らない
運用の観点では、「デプロイがより複雑」という言い方はものすごい控えめ表現だ。各パッケージは固有の特性と問題を持つ、個別に管理すべき対象になる
フロントエンド開発者がReact/Vue/Svelteのようなフロントエンドフレームワークやライブラリなしには「現代的なWebアプリ」を作れないと考えるのと似て、最近は「モノリス」が恐ろしい巨大なタールの塊を単一インスタンスで動かすことと同じで、「スケールしない」という考えが広まっているようだが、これは筋が通らない
もう1つ見える点は、コード総量がはるかに大きくなり、サービスの大半が 20%のビジネス/ドメインコード と 80%のネットワークメッセージ送受信処理 で構成されるということだ。どれだけ隠しても結局そこにあり、何らかの形でネットワークを扱わなければならない
フロントエンドの狂気と同じく、このマイクロサービス・カルトは、経済が崩れてこうした破滅のバベルの塔を支える金がなくなったときにようやく終わるだろう
付け加えると、マイクロサービスにも使いどころはある。コストを上回る価値を得られる一部の会社の中では正しい
サービスごとの独立したスケーリングのような技術的利点もあるが、最大の利点は明確な所有権の境界と、不要な過剰結合を防ぐことだ
小さなチームが単一の製品を作っているなら、マイクロサービスが解く組織上の問題はまだ存在しない可能性が高いので、必要ないかもしれない。その場合、マイクロサービスは早すぎる最適化であり、まだ存在しない問題を解くためにコストを払っていることになる
さらに言えば、フロントエンドの複雑性はすべて頭の中にある: https://bower.sh/front-end-complexity
この記事は、開発組織が完全に壊れていて制御不能であることを前提にしているように見える。スプリント中に何をするか決められず、こっそりサードパーティライブラリや未知の言語を持ち込み、互換性のない変更を静かに本番環境へデプロイする、といった具合だ
開発者が愚かでないなら、マイクロサービスももっと簡単なはずだと思う
賢く有能な開発者で構成されたチームで働いているなら、マイクロサービスはもっと簡単になると確信している。もちろん、そうであれば何でも簡単になるだろうが