最近「マイクロサービスアーキテクチャ」を読んだので、気になったところや学びになったところのメモと、自分なりの解釈とか感想を書いていきます。
あくまで気になったところや学びになったところなので、全部の項目に触れているわけではありません。
元から割と知ってた部分とか、「まあそうだよね」って思った部分とかで全く触れていない章もあるので、マイクロサービスについて調べたい時のちょっとした参考までにご覧ください。
マイクロサービスとは?
マイクロサービスとは、強調して動作する小規模で自律的なサービスです。
とあります。
イメージ的には、プロダクトの各機能単位のシステムがそれぞれサービスとして作られているようなものです。
通常の構成(モノリシック)とマイクロサービスアーキテクチャの構成
通常のいわゆるモノリシック(一枚岩)なサービスは、下記のようになります。
全ての機能が一つのサービスの中に入り、同一のサーバー上で動作しています。
対してマイクロサービスとして分離した構成は下記。
前述したように、各機能単位のシステムがサービスとして分離されています。
ここでいうサービスは、「プログラム、サーバーが独立して起動、管理されている単位」といった単位です。
モノリシックな構成では一つのサービスにアクセスし、その中で機能ごとのロジックを呼び出すという流れでしたが、マイクロサービスアーキテクチャでは「呼び出したい機能に応じて必要なサービスにアクセスする」という流れになります。
マイクロサービスにすることのメリット
サービスの分割のやり方
どのくらいの規模で分けるべきか?
「十分に小さく、ちょうどいい大きさである」
オーストラリアのRealEstate.com.auのJon Eavesは、マイクロサービスを「2週間で書き直せるもの」と特徴付けています
といったことが書かれています。
恐らくプロジェクトの規模や状況に応じて、明確な定義はなさそう(というより難しそう)です。
個人的なイメージとしては、モノリシックなサービスでビジネスロジックのクラス(SpringでいうServiceクラスなど)をパッケージ分けしてる単位がイメージとして近いのかなと思いました。
ビジネス概念に沿ったモデル化
ビジネスで境界づけられたコンテキストに基づいて構築されたインターフェースの方が、技術的概念に基づいたインターフェースよりも安定性があります。
と書かれています。
書籍内で紹介されているMusicCropという架空のオンライン販売サイトの例でも、「倉庫」「経理」などビジネス上のドメインの単位でサービスを分割していました。
前述したSpringのServiceクラスのパッケージ分けなどは、これに近い形でやることが多いので、イメージ的にはやはり近いのかなという印象です。
最初から分割を考えてはいけない
まず前提として、最初から分割を考えることはNGだということが書かれています。
どこを分離すべきなのか?分離しても良いのか?は、システム全体が出来上がり、各機能の仕様や構成を把握できないと適切に判断できないからです。
新規開発も困難です。これは、ドメインに対する知識が不足しているだけではありません。存在しないものを分割するよりも、存在するものを分割する方がはるかに簡単です。
とあり、新しいサービスを作る際もまずはモノリシックで作り、必要に応じて分割を考えていくことを薦めています。
また、「時期尚早な分解」として、初期段階から分割して開発したものの、それぞれのサービスのユースケースを微妙に違い変更の収集がつかなくなり、一度モノリシックなシステムにマージした事例も紹介されていました。
ライブラリや基盤システムの開発でもよくありますが、最初から共通化や汎用化を考えても、結局要件が合わず上手く行かなかったパターンの一つですね。
似たような手法との違い(ライブラリ、モジュール等)
サービスの分離の方法として、モノリシックな構成の中でもできる似たような方法として、
- 共有ライブラリ
- モジュール
との比較が紹介されていました。
共有ライブラリ
コードをライブラリ化して共通化することは標準的なテクニックとして存在します。
会社単位でライブラリ化して他のプロジェクトで再利用したり、ということもよくあると思います。
ただ、ここでは欠点として
- 同じ言語であるか、少なくとも同じプラットフォームで動作しなければならないため、技術的多様性が失われる
- システムの各部分を独立してスケールさせることができない
- DDLを使っていない限り、ライブラリを更新する時にシステム全体をデプロイしないとならない
っといったことが挙げられていました。
マイクロサービスのメリットとして、各サービス単位でデプロイできたり、自由に技術選定ができる点が挙げられるので、この欠点は対になっていますね。
確かに中央的に管理されているライブラリが更新された時、そのためのデプロイをしなければならないのは運用中のプロダクトなどでは面倒だったりします。
(なにか別のデプロイのついでに入れることも多いですが)
モジュール
簡単なライブラリ以上の独自のモジュール式分解テクニックを提供する言語もありますそのような言語ではモジュールのライフサイクル管理が可能です。例えば、実行中のプロセスにモジュールをデプロイでき、プロセス全体を停止せずに変更が可能です。
とあります。
マイクロサービスと同様に、機能単位で適切にモジュールで分離できていれば、それぞれでデプロイすることも可能そうに思えます。
が、こうもあります。
モジュールはすぐに残りのコードと密結合になり、主な利点の一つを放棄します
結局は一つのサービス、システムの中でモジュール分けしているだけだと、他のモジュールと密に依存するような設計になりがち(そういう手段が取れてしまう)ので、適切に分離した状態を保つのが難しいということですね。
その方が作るのが楽でやってしまうこともあれば、分離したつもりで作っていてもよく見たら依存していた、ということもあると思います。
マイクロサービスでは、他のサービスど強引に依存させようとしてもそのサービスの開発も巻き込まなければならず、自分の手だけではできなくなるので、こういうことが起こりづらそうではあります。
適切な統合
「分割」の話の後に「統合」がでてきてややこしいですが、主に複数のマイクロサービス間の呼び出し(経理サービスから倉庫サービスへアクセスしてデータを取得したり)など、分割されたマイクロサービス群をどういった技術で統合して動かすかというような話です。
マイクロサービスとして分離しつつも、それはシステム上の話で全体として一つのサービスとして存在しているため、統合は不可欠です。
ここを間違えると結局サービス間の結合が密になってしまったりして意味がなくなるので、大事な部分ですね。
RPCとREST
マイクロサービス間の対話の方法として、RPCとRESTが紹介されています。
RPC
RPCはRemote Procedure Callの略で、リモート上にある処理(サーバープログラムだと思ってください)を、呼び出しの複雑な部分を隠蔽して、ローカルの関数などを呼び出すしているかのように使うことのできる仕組みです。
最近ではgRPCがよく挙がります。
RPCの詳細は省きますが、ここでは欠点として
- リモート呼び出しであることを隠蔽しすぎて、コストを意識せず使ってしまう
- インターフェースの構造がサーバー(呼び出される側のサービス)、クライアント(呼び出す側のサービス)で密に連携してしまう
ということが書かれています。
RPCフレームワークでは前述の通りローカルの関数などを呼ぶようにリモートの処理を呼べるような仕組みが用意されていますが、実際には通信もしているし、ローカル呼び出しよりもコストがかかります。
それを意識せずに頻繁に実行するような形になっていると、かなり危険です。
また、RPCはProtocol Buffersなどを使うことで、サーバーとクライアントのインターフェース部分を共通化することができます。
しかし、共通化されている分なにも考えずに使うと密に連携されてしまい、サーバー側のインターフェースを変えた時(パラメータの追加、削除など)、それを使っているクライアントにも反映しないと動かなくなってしまうリスクがあります。
これでクライアントが常に同時にリリースしなければならないとなると、本末転倒です。
ネットワークが完全に隠される状態までリモート呼び出しを抽象化せず、クライアントの同時アップグレードを求めることなくサーバインタフェースを進化させるようにしてください。
とあります。
なので
- 単純な関数呼び出しのようにするのではなく、パラメータにRequestのオブジェクトを渡してResponseを受け取るようにしてあくまでリモート接続であることを意識させる
- パラメータが削除された場合、クライアント側はデフォルト値を入れて無視するように扱う
などの対応が必要なのかなと思います。
REST
HTTPでのREST通信はサーバー/クライアント通信でお馴染みの方法ですが、マイクロサービス間の呼び出しで使うこともできます。
RESTはJSONなどの「リソース」を間に挟み、サーバー、クライアントで自由に実装できるため、できるため、RPCよりも疎結合にする意味で良いというようなことが書いてあります。 まずはRESTを出発点として考えた方が良いと、推してもいます。
が、結局のところJSONを挟んでもサーバー、クライアントでそれぞれのパーサーや受け取り後の処理次第では結局依存してしまうため、この面では大きく変わらないのでは・・・と思いよく分かりませんでした。
DRY原則のリスク
DRY原則はプログラム設計の基本として普段意識している部分かと思いますが、マイクロサービスアーキテクチャにおいては危険をはらんでいます。
例えば複数のマイクロサービス間で共通で利用できるコード(ライブラリなど)を用意してしまうと、それを起因にサービス間が密に結合されてしまう可能性があるからです。
なので
- 1つのサービス内でのDRY原則は守る
- サービスをまたいでのDRY原則の違反は寛大に対処する
といったことが大事と書かれています。
これを見ると、本当に各サービスがそれぞれで自律して、いかに他のサービスを意識しないようにするのが大事なのかなと思います。
バージョニング
サービスのバージョン管理の話です。
セマンティックバージョニング(よくある1.0.0みたいなMAJOR.MINER.PATCHで表すバージョン形式)を使ってクライアントがサーバー変更の影響の情報を把握しやすいようにしましょうということでした。
- MAJORが上がればアプリの変更が必要
- MINERが上がっても基本的にはクライアント側の振る舞いには影響しない
- PATCHはバグFIXなど
といったように、バージョンを変えることでどれだけ影響があるのかを、クライアントが情報を得やすくし、サービス間での情報交換(他サービスへ影響の確認など)のプロセスを簡素化できることが良いと書かれています。
フロントエンド向けのバックエンド(BFF)
BFFは、デバイスの種類(モバイルアプリ、Webアプリ等)ごとにAPIゲートウェイ的なバックエンドの受け口を作るパターンです。
デバイスの種類が変わると、インターフェースや、取得する必要のある情報も変わったりするため、1つのAPIゲートウェイで処理しようとすると、分離が失われてしまうためです。
例えばモバイルアプリ版だけリリースしたいのに、インターフェースの変更がWeb版にも影響して一緒にリリースしなければならなかったり。
イメージは下記です。
サードパーティソフトウェアとの統合、カスタマイズ
サードパーティのソフトウェアを使うか、自分で構築するかの選択肢の話です。
「特殊な処理をするなら構築してください。それを戦略的資産とみなせます。使い方が特殊でなければ購入してください」
とあったのが印象的でした。
サードパーティ製品は基本的に汎用化されたものであり、ある意味その仕様に縛られた中で開発をしなければならず、特殊な処理をしようとした時にコストがかかるからです。
ここで載っていた例だと、「給与システム」は戦略的資産とみなさない、つまりサードパーティ製品を使う対象としています。
給与の支払いは世界中で同じで、組織ごとでこだわった使い方はあまりないから。
一方で商用のCMSは、プロダクトの内容によっては構築した方が良いとありました。
ここでの例は「ガーディアン」というイギリスの新聞のWebサイトの話でしたが、新聞事業ではWebサイトが中核であるからとのことでした。
たしかに核となる重要なWebサイトを、CMSの仕様に縛られた形でないと作成できないのは辛い、だから自分で作るというのは納得ですね。
カスタマイズは注意が必要
ちなみにサードパーティ製品をカスタマイズするというのは基本的に注意が必要とあります。
既に完成されたものに対してカスタマイズするということ自体が、ゼロから作るよりコストのかかる可能性があるからです。
本当に一部分だけ・・・とかであれば良さそうですが、大体一箇所カスタマイズし始めると「あれもこれも」となることは多いですね。
個人的にもサードパーティ製品を使う場合は、その仕様をよく理解してそれに則った形で実装する(仕様の範囲内でできる形で実装する)のが基本的には正だと思っています。
ストラングラー(締め殺し)パターン
ストラングラーパターンは、レガシーなシステムを新しいシステムへリプレースしていく時などに使うパターンです。
システム全体を一気に置き換えるのではなく、一部の機能をマイクロサービスとして切り出して作り、サービスの手前でリクエストをインターセプトし、機能によって古いシステムへルーティングするか、新しいシステムを切り替えるようなイメージです。
レガシーシステムを大幅に作り変える(例えばフレームワーク変更レベルの改修だったり)をするのはかなり重いので、コスト対効果を考えても現実味が薄くなかなかできないと思いますが、このパターンを使えばだいぶリスクは下げられるし、やれる可能性がでてきますね。
サービス間でのデータベースのトランザクション制御
モノリシックなサービスでは、データベースのトランザクションは一つで完結するため、特に意識しなくてもできるようになっています。
しかし複数のマイクロサービスサービスを使用し、それぞれでデータベースの更新処理があった場合、そのトランザクション制御をどうするかは、考えなければなりません。
後でリトライ
エラーが発生した時、キューやログファイルなどにキューイングしておいて、その情報を元に後でリトライする方法です。
リアルタイムでの整合性は保証されませんが、結果整合性という形で最終的には整合性が取れるようになります。
整合性を厳密に求めず、一時的にズレている状態が許容されるようなシステムであれば有効な手段ですね。
バッチ処理などでもものによってこのリトライ方法は使うことがあります(失敗したレコードだけログに吐いて後でリトライするなど)。
操作全体の中止
簡単に言うと、複数のサービスを呼び出しいずれかの処理で失敗した時に、一連の処理を全てロールバックしたような状態に戻すことです。
例えばサービスAでデータの挿入処理があり、その後サービスBでの挿入処理で失敗した場合、サービスAで挿入したデータを削除するといった対応です。
ただし、サービスAでの削除処理も補正トランザクションとして、新たなトランザクションを発行する形になります。
もしこの処理で失敗した場合はどうなるか?また、サービスが4つ、5つと増えてきた場合にどうなるか?と考慮することが多くなり複雑になります。
分散トランザクション
分散トランザクションは、複数のサービスのトランザクションを中央のトランザクションマネージャと呼ばれるプロセスで制御し、全体で一貫性が取れるように制御する仕組みです。
例えば
- サービスAのトランザクション開始
- サービスAの更新処理
- サービスBのトランザクション開始
- サービスBの更新処理
- サービスA、サービスBをコミット
のようなイメージですね(多分)。
一連の処理で扱う各サービスの処理の終了時ではコミットせず、全てのサービスの処理が終わったタイミングで全てコミットするということです。
さらにこのパターンでは2フェーズコミットを使うことが多いです。
コミットの際、まず投票フェーズとしてこの一連のトランザクションの参加者(コホートと呼ばれる)に対して「コミットしていいか?」を問い合わせ、全ての参加者からコミットがOKが返って来たら全てをコミット、1つでもNGがあれば全てをロールバックするという仕組みです。
前述の例で使うと、5のコミットのタイミングで
- サービスAに「コミットしていいか?」を問い合わせる
- サービスBに「コミットしていいか?」を問い合わせる
- サービスAから「OK」が返ってくる
- サービスBから「OK」が返ってくる
- サービスAをコミット
- サービスBをコミット
みたいなイメージです(分かりやすくするために全て直列で書いています)。
もしサービスAが「コミットできない」となった場合は
- サービスAに「コミットしていいか?」を問い合わせる
- サービスBに「コミットしていいか?」を問い合わせる
- サービスAから「NG」が返ってくる
- サービスAをロールバック
- サービスBをロールバック
となります。 ただ、この方法はコストが高い割に不確実です。
まず全てのサービスの処理が終わるまでそれぞれのトランザクションがコミット、ロールバックされないため、更新している全てレコードないしテーブルがロックされ続けてしまいます。
また、投票後のコミットで失敗する可能性もあり、それをどうするかも考える必要があります。
(あと、こちらも絡むサービスが4つ、5つと多くなった場合はよりコストが大きくなります)
実装としてはJava Transaction API(JTA)などライブラリは存在するようです。
が、上記の理由からあまりおすすめはできなさそうです。
結局なにを選ぶべきなのか?
ここで紹介されている方法は、結局のところどれも複雑で「絶対これ」というものはありませんでした。
そして
現在単一トランザクション内で起こっているビジネス操作があったら、本当に必要かどうかを自問してください。
本当に一貫性を維持したい状態に遭遇したら、まずは分割を避けるためにあらゆる手を尽くします。
とあります。
つまり同一トランザクション内でやりたいようなものは、極力分割するなということですね。
サービスが4つ、5つ絡むと複雑になるということを前述しましたが、そもそも4つ、5つでまたいでトランザクションを張る必要がある場合、分割が適切でないようには個人的にも思います。
モノリシックなサービスでも、複数のドメインのビジネスロジックで更新処理を呼び出すのはせいぜい2〜3くらいなので。
そしてそこが多くならないのであれば、前述の方法でもカバーできるのでは、とも思います。
(それでも分散トランザクションは高コストなので微妙ですが・・・)
コンウェイの法則とシステム設計
コンウェイの法則とは?
プログラマの世界で有名な法則の一つで、
システムを設計するあらゆる組織は、必ずその組織のコミュニケーション構造に習った構造を持つ設計を生み出す。
というものです。
ここでは「疎結合組織と密結合組織」として、組織の結合度との関連を話しています。
密結合組織は
通常はしっかりと足並みを揃えたビジョンと目標で結びつけられた商用製品の会社と考えてください。
疎結合組織は
代表例は分散ソースコミュニティです。
というイメージで書かれています。
そして疎結合な組織ほどモジュール性が高く結合度の低いシステムを作り、密結合な組織ほどモジュール性が低いという研究結果が書かれています。
サービスの分離の話で書かれていることを考えると合致しますが、例えば機能ごとにチームが存在し、それぞれである程度独立して開発されている組織であれば、自然と結合度が低くなると思います。
チームが違えば設計思想や開発のやり方も変わり、かつそのチームで開発している単位で成り立たなくてはならないため、自然とモジュール性も高くなる。
逆に全体が一体となっている組織では、全体で決まった設計思想や規則に則った開発をするため、自然と統一された構造になります。
さらに全体の単位で共通化なども考えるため、システム全体から依存するコードも増えてきます。
(前述のDRY原則の話と同じ)
フィーチャーチーム
フィーチャーチーム(フィーチャーベースのチーム)とは、小規模なチームが一連の機能開発を推進し、たとえコンポーネント(サービス)境界を越えても、必要なすべての機能を実装するという考え方です。
とあります。
これまでマイクロサービスのチームについて出てきた話そのものだと感じます。
フィーチャーチームとコンポーネントチーム
フィーチャーチームについて調べてみると、よく「フィーチャーチーム」と「コンポーネントチーム」の比較が出てきます。
「コンポーネントチーム」は、例えば「UIチーム」「インフラチーム」「フロントエンジニアチーム」「サーバーエンジニアチーム」「企画チーム」みたいな担当作業の領域に応じて組まれたチームです。職種毎のチームという言い方もできそうですね。
よくある構造です。
これに対してフィーチャーチームは、「販売機能チーム」「在庫管理機能チーム」みたいにプロダクトのフィーチャーごとにチームを組み、それぞれにUIデザイナーもエンジニアも企画も人員がいるイメージですね。
フィーチャーチームがなぜ良いのか?
コンポーネントチームはチームとしての責任範囲が各作業単位になるため、開発の一連の作業全体へ意識が持ちづらくなります。
例えば自分の職種の仕事を良くするための方法は考えるけど、そのプロダクト、あるいは機能に対してどう良くするかは考えにくくなったり。
(自分の担当領域以外との関わりが薄くなるゆえ)
それに対してフィーチャーチームは、例えば企画からデザイン、プログラムの実装、テストまで開発の一連の流れを一つのチームで担当するため、全体が見えやすなり、プロダクトや機能、要は自分が開発している「モノ」自体へのコミットメントも高くなりやすいです。
また、マイクロサービスはビジネスドメインに沿って分離される形が望ましいため、その思想ともマッチします。
実際に過去経験したプロジェクトでも、大きい開発の機能ごとにチームを分けてそれぞれに各職種の担当者を置きそれぞれで責任を持って進めることで、開発が上手く進むようになったことがありました。
現在も大規模なプロジェクトではこの構成にしているプロジェクトはよく見ます。
その時はこの言葉を意識していたわけではないですが、やっぱり上手くいく理由のある方法だったんだなと思いました。
大規模なマイクロサービス
障害はどこにでもある
マイクロサービスは分離されていて障害が置きた起きた時の影響範囲は抑えられますが、数が増える分発生確率は高まります。
特に大規模なサービスになれば、その数はどんどん多くなっていきます。
どんなにエンジニアが対策していても、ハードウェアが故障することもあればネットワークが落ちることもあります。
そういったことに対して、「絶対に落とさないようにする」ではなく「障害は必ずどこかで起きる」と考えることで、「落ちた時に素早く復旧できる方法を考える」ことができるようになり、問題に対する備え方が変わってくるという話です。
(もちろん普通は落ちないための努力は全力でやった上でですが)
サーキットブレーカー
サーキットブレーカーはマイクロサービスのくだりでよく出てくる言葉です。
これはマイクロサービス間での呼び出しの際、下流のサービス(呼び出し先)のサービスで障害が発生し正常なレスポンスが返せない場合、上流のサービス(呼び出し元)が止まらないようにするため、下流のサービスへの接続を遮断する仕組みです。
落ちているサービスへずっと接続しつづけると、その度に待ちが発生しタイムアウトで落ちる、という状態になってしまいます。
サーキットブレーカーを使い、下流のサービスが落ちていると判断した場合はすぐにエラーになるようにしておくことで、無駄な待ちを発生させる状態を防げます。
感想
マイクロサービスアーキテクチャについて大枠は知っていましたが、思想や考え方についてしっかり知れ、具体的な実現方法なども分かり良かったです。
原著が出たのが2015年のため少し前の本ですが、根本的な知識を得るためにも、マイクロサービスを学ぶ上では読んでおいた方が良い本だと思いました。
また、モノリシックなシステムで開発していたとしても、サービスの分離の考え方などは設計を考える時に参考になる知識だったので、マイクロサービスを使う予定がなくても読んでおいて損はないなとも感じました。
あとは
- もともと4〜5人のチームで開発、管理しているサービスを分離する意味はあるか?
- 分離していった時、その他多数の細々した機能たちはどこに置くべきか?
など疑問点もあるので、これはもっと実例なんかも調べながら探って行きたいと思います。