Rのつく財団入り口

ITエンジニア界隈で本やイベント、技術系の話などを書いています。

【感想】『マイクロサービスパターン 実践的システムデザインのためのコード解説』:前編

「未来はすでにここにある。まだむらなく流通していないだけだ」←グッとくる

 最初のエモワードがSF作家ウィリアム・ギブスンの引用でイイ! サイバーパンク2077遊んでみた~い……じゃなかった、CloudFoundry.comのファウンダーでありMicroservices.ioの運営者、経験豊富なソフトウェアアーキテクトであるクリス・リチャードソンさんによる『Microservices Patterns』の翻訳本。
 タイトルのようにアーキテクチャパターンデザインパターンのようにマイクロサービスをパターンで体系化し、サンプルストーリーを元にした事例やコード例、OSS紹介を交えつつマイクロサービスを実践する設計方法を探求した本となっています。
Java文化圏で長く活動してきた方とのことでサンプルコードはほぼJavaSpringフレームワーク、ご本人らによるマイクロサービス用のフレームワークEventuate Tramが登場します。前に洋書の表紙を見たことがありますが『POJOs in Action』の作者さんなんですね。
 全13章500ページ超えのがっつりした本で正直なかなか内容も難しいので、少しづつ読んでいきました。以下自分の理解を深めるための読書記録&感想を全3回で残していきます。

microservices.io

本エントリでは時々「マイクロサービスアーキテクチャ」を長いのでMSAと略します。

f:id:iwasiman:20210123161130p:plain
マイクロサービスパターン

Chapter 1 モノリシック地獄からの脱出

1.1 ゆっくりした足取りによるモノリシック地獄への転落

 サンプルストーリーに出てくる架空の会社はFTGO(Food to Go, Inc)社、2005年創業以来アメリカでは有数のオンラインフードデリバリーの会社という設定でCTOのメアリさんが主人公です。コロナ下ではUber Eatsと争っていそうですね。クラウドAmazon SESなども使っているもののサービスの実体は1アプリ、開発言語はJavaでデプロイする実体は標準のWARファイル。設計関連の話で時々出てくるワード Big ball of mud(大きな泥団子)という、どんどん肥大化した巨大な1個のWebアプリケーションとなっています。
 内部のアーキテクチャは後程出てくるヘキサゴナルアーキテクチャ(hexagonal architecture)。六角形の真ん中に機能が居座りこれらがモジュール。左側でRESTやWeb UIを通して顧客がアクセス、DBはMySQLにアダプタを通して繋がり、構成図の右側が他のサービスとなっています。

qiita.com

 こうした従来型のモノリシックアーキテクチャの利点は以下を挙げています。

  • 開発しやすい:元からIDEはひとつのアプリ開発を想定している。
  • 大きな変更を加えやすい:コードやDBを変更しても、デプロイすればよい。
  • テストしやすい:e2eテスト(画面を動かしての全体の結合テスト)もSeleniumなどで行える。
  • デプロイしやすい:サンプルストーリーではWebサーバーがTomcatJava標準の方法でWARファイルをデプロイ。
  • スケールしやすい:ロードバランサーを前段に仕込んで仕込んで複数インスタンスで実行する方法がある。

 しかしこういう成長型のサービスの抱える宿命として、アプリが大きくなるにつれ欠点が目立つようになってきます。

  • 複雑なアプリに開発者が萎縮:拡大するにつれコードが分かりにくく、修正しづらいくなる。
  • 開発ペースが遅い:複雑さは開発速度(ベロシティ)の低下に繋がる。IDEの動作速度も遅くなる。ビルドして実行してテストのループが回しづらくなる。
  • コミットから本番デプロイまでの道が遠い:時間がかかるので月数回レベル。マージに時間がかかったりする。アジャイル開発を導入していても、2週間に1回のリリースできなかったりする。
  • スケーリングが難しい:アプリ内部のモジュールごとにCPUやメモリの要件が違っても、デプロイするのは常に1個のサーバーでしかない。
  • 信頼性の高いモノリスを提供するのが難しい:サイズが大きすぎるために徹底的なテストが難しく、障害の分離もできない。モジュールが同じプロセスで実行されるため、ひとつのモジュールのバグが全体に影響してしまう。
  • テクノロジスタックが陳腐化しても縛り付けられる:最新の言語やフレームワークを採用しづらい。サンプルストーリーではJava開発では定番のSpring Frameworkの古いバージョンを使っていて、アップグレードは一度もできていないという設定。Go言語やNode.jsに興味を持つ先進的エンジニアもいるが試せない。

「複雑さに分割することで対応する」というのはよく設計周りの原則で語られますが、マイクロサービスも根本はこれなんだなというのが分かります。

1.2 マイクロサービスアーキテクチャ(MSA)で状況を打開する

 FTGO社ではメンテ性・拡張性・テスト容易性を犠牲にしてサービスを拡大し続けてきましたが、ここでMSAを検討し始めます。マイクロサービスとは……

  • 3次元のスケーラビリティ:X軸がロードバランサーを使った複数インスタンスで負荷分散、Z軸がリクエストの属性によってルーティングを分散させる手法。Y軸が機能に基づいてアプリケーションをサービスとして分散させる手法。これがMSAである。例えばオーダーサービス、配達サービス、レストランサービス、キッチンサービス、会計サービスなどに分ける。
  • モジュール性の単位としてサービスを使う。サービスは個々にAPIを持って独立、個々にデプロイ、個々にスケーリングする。
  • それぞれのサービスは専用のDBを持つ。DBスキーマを変更しても他のサービスは影響を受けない。

DBは個々に持つといってもOracleのライセンスをそのぶん買ってラリー・エリクソンが大儲けするわけではないよ!と名前入りで話題にしているのが笑えます。自分はOracleデータベースもさんざん使ってきたし今も時々使いますが、JavaのWARファイルや通信形式のSOAP同様、モノリス時代の象徴っぽくもあるなあと思います。

www.oracle.com

 2000年代半ばに考え方が提唱され、クラウド時代にさらに注目されると思いきやその後結局あまり流行らなかった「サービス指向アーキテクチャ(SOA: Service Oriented Architecture)とマイクロサービスとの違いは、本書では以下と説明しています。

  • サービス間の通信について。SOAは古い決まりのSOAPやWS標準など重量級のプロトコルを使った「スマートパイプ」(=高機能な通信経路)を使う。 マイクロサービスはRESTgRPCのような軽量プロトコルの「ダムパイプ」(=シンプルな通信経路)を使う。
  • DBについて。SOAは全体で1つのDBを共有。マイクロサービスはサービスごとにデータモデルがあり、DBがある。
  • SOAの典型的なサービスは大きなモノリシックアプリケーションで中身は少数の大きなサービスから構成。マイクロサービスは比較的小さなサービスが沢山ある構成。

www.redhat.com

 MSA(マイクロサービスアーキテクチャ)の利点については以下を挙げています。

  • 大規模で複雑なアプリのCI/CDが可能:サービスごとにコードのリポジトリは別、CI/CDの仕組みも別、アプリの実体も別。テスト容易性とデプロイ容易性を備え、開発チーム間の関係も疎になることで開発のベロシティが向上。世の中にローンチして出すまでの時間がより短くできる。障害復旧よりも価値ある機能の提供に時間を使えるので、開発者の満足度も上がる。
  • 個々のサービスが小さくてメンテが簡単:作る対象の規模が小さければ理解もしやすく、IDEも遅くならず、ビルドやデプロイも早い。開発者の生産性が上がる。
  • サービスを個別にスケール:サービスごとにリソース要件が違ってもそれぞれ別のハードウェアにデプロイし、それぞれ別のスケーリングの工夫ができる。
  • 障害分離性が高い:1つのサービスが止まっても他のサービスは正常に動き続けられる。
  • 新しいテクノロジーを簡単に実験、採用できる:テクノロジスタックに長期に縛られることがない。小さいサービスで新しい言語や技術を試せるし、失敗したら簡単に捨てられる。

 互いに疎結合にして障害分離性が高いというのは、まさにクラウドコンピューティングの原則と同じです。また最後の新しいテクノロジーを使いやすいという利点は本書でも「決して侮れない」とあり、デベロッパー・エクスペリエンスとも関連するのだなあと思います。
 一方、良い事ばかりに見えるマイクロサービスの欠点については以下を挙げています。

  • サービスの適切な分割方法を見つけるのが難しい:明確なアルゴリズムはなく職人技になる。分割を間違えると、本当は分けずに一緒のサービスにするべきだった機能を無理やり分割したアンチパターン「分散モノリス」ができてしまう。
  • 分散システムは複雑になる:メソッドコールではなくプロセス間通信 (IPC: Inter-Process Communication) になる。複数サービスにまたがるクエリ実行やトランザクション制御も難しく、データ整合性に技術の助けがいる。
     複数サービスにまたがった自動テストも複雑。運用、デプロイにも自動化のテクノロジがいる。Spinnakerのような自動デプロイツール、PaaSのOpenShiftオーケストレーションプラットフォームでDocker SwarmKubernetes
  • 複数サービスにまたがる機能のデプロイには調整がいる:依存関係など。モノリシックだとこれが簡単。
  • いつMSAを採用するかの判断:成長する前のアプリケーションだと採用してもメリットがなく、大変になって開発速度が減るだけの場合もある。急拡大して複雑さへの対処が問題になった時が良いタイミング。

 これから一旗揚げる予定のスタートアップ企業などは創業時からは採用しない方がいいと、はっきり書いてあったりします。ただ使えばよいという単純な話でないのは、他の技術要素と同じなのですね。

1.3 MSAのパターン言語

 さまざまな問題に対処するための唯一の答えはない...ということでマイクロサービスについてもパターンを用意している話。

  • MSAは万能薬ではない:ソフトウェアの世界ですべてのものがいいか悪いかの2項対立で語られがちな話で、マイクロサービスも万能薬と思われがちだが状況次第。人間は感情によって動くので、パターンを当てはめて客観的に論じるのがよい。
  • 「特定の状況で起きる問題に繰り返し使える解決方法」をパターン、パターン言語と呼ぶ。それぞれのパターンには以下の3つの価値がある。
  • 1:: フォース:問題を解くために解決しなければならない目的と制約。フォース同士が矛盾することもある。
  • 2:: パターン適用後の状況:利点、欠点、イシュー(新たな問題)を持つ。
  • 3:: 関連するパターン:パターン同士は5種類の関係を持つ。先行パターン、後続パターン、代替パターン、汎化パターン、特化パターン。最後の汎化<-特化はオブジェクト指向の抽象と実装、継承の話と同じ。

 MSAが解決する問題領域を示すパターン言語を図示しているのですが、これが明快に図になっていて分かりやすいです。Microservices.ioに掲載してあるPDF版が以下。

https://microservices.io/i/MicroservicePatternLanguage.pdf

  • まず最初の選択として <Monolithic architecture>パターン<Microservice architecture>パターンのどちらにするかの2択がある。ここでマイクロサービスを選ぶと以下3層に分類。
  • 1:: インフラストラクチャパターン: アプリに関係しないインフラ層の話。デプロイや外部APIなど。
  • 2:: アプリケーションインフラストラクチャパターン:両方に関係する話。セキュリティや通信パターンなど。
  • 3:: アプリケーションパターン:アプリの話で分解やDB構造、データ整合性の話。

この3層、あるいはまたがる形で幾つか目的と問題があり、それらを解決するためのパターンが幾つか存在。それぞれ先行と後続だったりどちらかを選ぶ代替パターンであったり、一般的に使える汎化パターンだったり、特化パターンだったりするよ……と分類しています。
 本書のタイトル通りこのパターン群がこの本の主題でもありますが、よく作ったと思います。

1.4 プロセスと組織

 アーキテクチャ/プロセス/組織 の3角形で、これらは互いに関係しあっているよという話。
例えば巨大なアプリケーションを何十人ものメンバーで開発していたら「チームオブチームズ」、8〜12人のチームに分割してそれぞれのチームがひとつのサービスだけを受け持つ、「逆コンウェイの法則」に乗っ取るのがよいと論じています。そしてひとつのチーム=サービスが大きくなりすぎたら、またチームとサービスを分割していくと。

またマイクロサービスを受け入れる人については他の本の引用で、どのような反応を示すかを「トランジション」という概念で示しています。

  • 終わり、喪失、別れ:安全なチームから引き離されて横断型の組織に入れられて嘆いたり。データモデルやDB全体を見ているチームだったら、分割されることで脅威を感じたり。最初は抵抗がある。
  • ニュートラルゾーン:混乱しながら新しいやり方を身に着けようとする。
  • 新しい方法の始まり:新しい方法を支持し利点を感じ始める。

 感情を無視すればマイクロサービスの導入は茨の道になるでしょうと述べており、人間の感情の話も入ってくるのは面白いですね。

 なおサンプルストーリーのFTGO(Food to GO)サービスは本書全体を通して今後もずっと出てくるのですが、超どうでもいいですが見るたびに FGO(Fate Grand Order)と空目しそうになります(笑)

www.fate-go.jp

Chapter 2 サービスへの分割

FTGO社のメアリさんもあちこちから聞いたりして、サービスの分割を始めます……

2.1 MSAとは正確なところなんなのか

 まずは前段としてソフトウェアアーキテクチャとは何なのかを掘り下げています。

  • 「システムを作り上げる上で必要となる一連の構造のこと。ソフトウェア要素とそれらの間の関係、両者の性質から構成される。」部品への分割とそれらの関連のこと。
  • アプリケーションには機能要件とサービス品質要件がある。このサービス品質を満たすための鍵がアーキテクチャにあり重要である。品質属性(quality attribute)、イリティ(-ility)とも呼ばれる。スケーラビリティ、信頼性、メンテナンス性、テスト容易性、デプロイ容易性など。
  • 次にアーキテクチャスタイルとは、「組織構造のパターンの観点から、システム全体像を定義するもの。そのスタイルのインスタンスで使えるコンポーネントとコネクタの種類と、それらの組み合わせ方に関する制約を定義する。」

 物理的な建築のアーキテクチャにビクトリア様式とかアールデコ様式があるように、「様式(スタイル)」という言葉を本書のこの章では使っています。
これは「アーキテクチャパターン」と言い換えてもだいたい同じような意味、さらにマイクロサービス特有の組織論や開発様式諸々も一式含めた意味合いなのかなと思います。古めの本ですが設計の文脈でよく名が挙がる『エンタープライズアプリケーションアーキテクチャパターン(PofEAA: Patterns of Enterprise Application Architecture)』という本もありますね。

  • 階層化アーキテクチャ(layered architecture):古典的なプレゼンテーション層、ビジネスロジック層、永続記憶層の3層からなるスタイル。欠点として複数システムから呼び出されるのに対応していない、DBがひとつを想定してビジネスロジックが永続化層に依存するという2点がある。
  • ヘキサゴナルアーキテクチャ(hexagonal architecture):この欠点を改善。六角形の中央にビジネスロジックが鎮座、周囲にアダプタがあって外側とやりとり。外側にブラウザがあったり、データベースがあったり、メッセージをやりとりするところがあったり。

nrslib.com

 ネットでも話題になったボブおぢさんの『The Clean Architecture』は、このヘキサゴナルの発展形のようなイメージですね。

f:id:iwasiman:20200827210602j:plain
The Clean Architectureのアーキテクチャ構成図。Webより借用しました。

 そしてモノリシックアーキテクチャ、マイクロサービスアーキテクチャもこのアーキテクチャスタイルのひとつだよと立ち位置を分類して、話を進めます。次に「サービス」の定義の話。

  • 何かの役に立つ機能を実装する、個別にデプロイできるソフトウェアコンポーネントのこと。サービスはAPIを持っており、問い合わせの「クエリー」、オーダ登録のような「コマンド」、クライアント側に予約などやりたいことができたことを返す「イベント」を持つ。
  • 常にAPI経由でしかアクセスできないので、モジュール性を強制する構造になっている。
  • サービスは疎結合であり、理解、変更、テストが用意。DBスキーマを変更しても外には影響がない。
  • 複数サービスからよく使われ、変更されそうにない機能はライブラリとして共用してよい
  • 「マイクロ」サービスという名前から誤解されがちだが、サイズは重要でない。サイズを小さくするのが目的ではなく、結果として小さくなることがほとんどである。設計のしっかりしたサービスを作ることの方が重要。

 序文でも作者さんがこれまでよく聞かれた質問No.1がサービスのサイズの話だったとあり、このへん誤解されやすいのだなあと思います。プログラミングの原則、名前重要……!

2.2 マイクロサービスアーキテクチャの定義の方法

 一般的なシステム設計技法も説明しながら定義の方法を解説する節。機械的に行うのではなく反復しながらやることになると言っています。Java世代の方らしくUMLの話が出てきたりして懐かしいです。
1)システム操作の見つけ方、2)分割のガイドラインと障害と対処方法、3)APIの定義方法 と進みます。

  • まずシナリオなどから言葉を洗い出して、「高水準ドメインモデル」をつくる。ここからOrderクラスやRestrauntクラスなど、大まかなクラス図が作られる。1個1個のクラスがサービスになるイメージ。
  • 次に「システム操作」を考える。ここからそれぞれのクラスが持つコマンド≒クラスのメソッドや関数、createOrder()などが導き出せる。
  • 技術の問題ではなくビジネスの問題によって構成する。

サービスを定義して分割していくには以下ふたつの方法を述べています。

  • 業務による分割 <Decompose by business capability>:業務を洗い出して、「レストランサービス」「オーダーサービス」のようにサービスを定義していく。サブ業務ごとにサービスになる場合も、結合度を考慮して複数業務がひとつのサービスになることもある。
  • サブドメインによる分割 <Decompose by sub-domain>:エリック・エヴァンスのドメイン駆動設計(DDD)を活用する方法。DDDではドメインの一部をサブドメインとして分割していく。
    サンプルではFTGOがFTGOドメイン、その中が受注サブドメインや配達サブドメインに分割されていく。この1サブドメイン=MSAの1サービスで、ぴたりとマッピングできる。

 分割のガイドラインとして、出ましたUncle Bob、ロバート・C・マーティンさんのCleanシリーズなど書籍群で説明されている原則のうち2つが特に役に立つと述べています。

  • 単一責任の原則(SRP): クラスを変更する理由は1つでなければならない。結果的に、クラスの責務はひとつになる。これがそのままサービスにも当てはまる。
  • 閉鎖性共通の原則(CCP): 同じ種類の変更に影響を受けるものは、1つのパッケージの中にまとめられるべき。同じ理由で、書き換えられるコンポーネントを一つのサービスにまとめるとうまく分割できる。

 このサービス分割の障害になるものは以下を挙げています。

  • ネットワークレイテンシ:サービス間通信はネットワークを介するので基本遅くなる。バッチを使ったり、複数サービスを統合してその中でメソッド/関数呼び出しにする手もある。
  • 同期通信による可用性の低下:同期的なREST呼び出しで、通信先サービスが落ちていると注文が作れなかったりする。非同期通信/メッセージングが役に立つ事が多い。
  • サービス間でのデータ整合性の維持:サービスそれぞれが別にDBを持っていて両方更新する時に、MSAではトランザクションが使えない。本書では結果整合性を保証する「サーガ」という仕組みを推奨。原子性を保証する場合は単一のサービス内に閉じ込める必要があり、分割の障害になる。
  • 整合性のとれたデータビュー:複数サービスのそれぞれのDBを参照して問い合わせ結果を返すときに整合性が取れない。単一のサービスに閉じ込める手法もある。
  • 分割を妨害するゴッドクラス:設計のまずいシステムで巨大な神クラスがあると分割の障害になる。DDDのサブドメインの考え方に基づいてクラスを分割したりしていく。

APIの定義方法は以下。

  • まずリクエストがどのサービスに飛んでくるのかを考えて、各サービスにcreateOrder()などの操作を当てはめていく。
  • 単一の操作で完結せず、他のサービスと連携しなければならない操作を判断していく。createOrder()の前に顧客やレストランをverify()するなど。

 ここでドメイン駆動設計やSRP原則の話も出てくるんですねえ。2章はMSAだけに限らす、アーキテクチャや設計全般の勉強になる話です。

Chapter 3 マイクロサービスアーキテクチャで使われるプロセス間通信

 本章はプロセス間通信(IPC: Inter Process Communication)の話。流行りはRESTとJSONであることを認めつつ、本書では非同期メッセージング主体を勧めています。

3.1 MSAにおけるプロセス間通信の概要

 クライアント←→サービス間の対話が「インタラクションスタイル」。

1クライアントで相手のサービスが1つしかない場合は…

  • リクエスト/レスポンス:いつもの同期的通信。
  • 非同期リクエスト/レスポンス:サービスがレスポンスを返してこなくても、クライアントは待たない。
  • 一方通行の通知(one-way notification)クライアントからリクエストを送るだけ。

1クライアントから相手のサービスが複数ある場合は非同期のみになります。

  • パブリッシュ/サブスクライブ:クライアントから通知メッセージを発行。その後サービス側が別タイミングでサブスクライブ(購読)して来たことに気付く。
  • パブリッシュ/非同期レスポンス:発行後、特定のサービスからはレスポンスを一定時間待つことがある。

 そしてMSAでは特に重要であるAPI設計については、まずインターフェース定義言語(IDL)を使ってAPIを先に定義する「APIファースト設計」がよいと述べています。
 APIが新しくなっていく中で使うのは「セマンティックバージョニング」。MAJOR.MINOR.PATCHの形式でよく見るやつです。

  • MINOR: 下位互換性のある変更。オプション属性の追加や新しい操作など。
  • MAJOR: 互換性を破る変更。しばらくは新旧両バージョンを並行で使えるようにする。
    URLに/v2/のように書く方法も、HTTPリクエストヘッダーのAcceptヘッダにMIMEタイプを指定する際、最後にversion=1のように加える手もある。

 メッセージ形式は、当然ながら特定のプログラム言語に依存しないものが大事。

  • テキスト:XMLの後にはJSONが流行。XMLスキーマのような定義方法として、JSON Schemaというメカニズムも考えられた。テキスト形式の欠点は冗長になりがちなことで、処理効率が重要な場合はバイナリも検討する。
  • バイナリ:有名なものはプロトコルバッファAvroの2つ。API[のバージョンの増加を考えるとプロトコルバッファの方が扱いやすい。
3.2 同期的なリモートプロシージャ呼び出しパターンを使った通信

 日本語だとあまり使わない気がしますが、Remote Procedure Invocationを略してRPI

<Remote procedure Invocation>パターン
クライアントのビジネスロジック→RPIプロキシアダプタ→(リクエストがネットワークを飛んでいく)→RPIサーバーアダプタ→サービスのビジネスロジック
と進んで返ってくるもの。RPIプロキシの部分が通信プロトコルカプセル化します。

 具体的な通信メカニズムとしては、まずよく使われるREST

ja.wikipedia.org

  • REST API定義としては一番有名なものがSwaggerから発展したOpen API Specificationが存在。
  • 複数のリソースをまとめて取得したい場合は、GETのエンドポイントLの最後にURLパラメーターで ?expand=aaa のようにつけたりする方法もあるが、通信に時間がかかる。こうした場合にはGraph SQLNetflix Falcorなどがある。
  • 操作をHTTPメソッドに対応付ける時、リソースのエンドポイント末尾に /orders/1/cancel とかつけると今いちしっくりこないケースがある。こうしたケースではgRPCが人気。
  • RESTの利点:単純で使いやすい、ファイアウォールに邪魔されない、アーキテクチャが単純になる。
  • RESTの欠点:直接やり取りするのでクライアント-サービス双方が常に動いている必要があり可用性が下がる、サービスのURLを知っている必要がある、複数リソースをフェッチするのに時間がかかるなど。

 続いて言語に中立なクライアントとサーバーを書くためのフレームワークgRPCについて。gRPC自体はシステムだともプロトコルだとも言われています。
主要な各言語に対応していてそれぞれの言語でのコードを生成してくれるので、本書ではフレームワークと呼んでいるのでしょうか。公式サイトでも A high performance, open source universal RPC Frameworkとあります。

knowledge.sakura.ad.jp

  • go言語っぽい書式で.protoファイルにサービスの定義を書く。Javaのインターフェースぽくもある。
  • メッセージ形式にはバイナリのプロトコルバッファを使い、高速。
  • 更新操作が多いAPIを設計しやすい。
  • 大きなメッセージをやり取りする時に効率的。
  • 双方向ストリーミングでRPIとメッセージングの両方のスタイルで通信できる。
  • クライアント側とサービス側で言語が違ってもOK。
  • 一方、古いファイアウォールだとHTTP/2をサポートしていない場合がある
  • RESTと同じで同期通信ではあるので、部分的なエラーの対処が必要。

 以前はGo言語のテクノロジーなのかと誤解していたのですが、gRPCはGo言語やマイクロサービスとセットでよく使われる事が多いという位置づけなんですね。

grpc.io

 そしてマイクロサービス特有の、いくつもある問い合わせ先のサービスが死んでいた場合どうするかの問題。

<Circuit breaker>パターン
レスポンスは無限に待つのでなく、必ずネットワークタイムアウト時間を決める。また何回無反応まで許すかを決め、超えたらリクエストを失敗させるようにする。
JVM言語ではNetflix製のHystrixが有名。.NETではPollyライブラリが有名。

  • クライアントからのリクエストを一旦ひとつの「APIゲートウェイ」で受け止め、ここから複数サービスに順次リクエスト。
  • 死んでいるのがあったら省略したりキャッシュを使ったり、このAPIゲートウェイの中で解決してクライアントにレスポンスを返すようにする。

 そして、例えばOrderServiceクラウド上にあった場合、同じサービスでも自動でスケールしてインスタンスがどんどん増えていき、IPアドレスも動的に変わっていく場合もあります。これに対応するのが「サービスディスカバリ」の仕組み。存在しているサービス一覧を台帳に保存して教えてあげるようなものです。

<Self registration>パターン
インスタンスが生まれたらそのインスタンス自身がサービスディスカバリとAPIを通して通信、自分のサービス名とIPアドレスなどを登録する。

連動するのが

<Client-side discovery>パターン
クライアント側はサービスを呼びたい時にまずAPIを通してサービスディスカバリに問い合わせ、目的のサービスが複数返ってきたら自力でその中の一つを選ぶ。

 より本格的になるのが、本格的なデプロイプラットフォームがあり、その中にサービスレジストリがある構成。

<3rd party registration>パターン
インスタンスは自分を登録する必要はなく、デプロイプラットフォームの一部である「レジストラ」がインスタンス生成時に登録を代行する。

 これと連動するのが

<Server-side discovery>パターン
ライアント側はサービス名を問い合わせて自力で選ぶ必要はなく、DNS名で GET http://hoge-service/ のようにデプロイプラットフォームに向かってリクエストを投げるだけ。するとデプロイプラットフォーム内部のルーターがサービスレジストリに問い合わせて、インスタンスを選んでリクエストを投げるのを自動でやってくれる。

 そのデプロイプラットフォームを使ってすべてのサービスをデプロイしていく必要が出てくるが、基本的にはデプロイプラットフォームに任せる方法を本書では推奨しています。では具体的な例としてどんなプラットフォームがあるのか? というとここで出てくるのがKubernetes。
 あーこういうところで便利になる訳ね、とk8sの存在理由がちょっと分かりました。後半のデプロイメントの章でがっつりと再登場します。

kubernetes.io

また今後の章でもそうなのですが、Netflixの名があちこちで登場し、マイクロサービスの分野ではネトフリは先駆者なのだなあと思います。

Netflix (ネットフリックス) 日本 - 大好きな映画やドラマを楽しもう!

3.3 非同期的メッセージングパターンを使った処理

 同期の次は非同期のパターン。

<Messaging>パターン
非同期メッセージングを使ってクライアント←→サービス間をやりとりする。

  • メッセージのヘッダにはメッセージIDや返すチャネルを表すリターンアドレスなどを格納、ボディにバイナリ形式でデータの本体を格納。
  • データだけを含む「ドキュメント」、操作とパラメーターが入った「コマンド」、送り手側で何か起こったことを知らせる「イベント」がある。

送り手側のビジネスロジック→送信ポートを実装したメッセージセンダー→通信経路のインフラである「チャネル」→メッセージハンドラ→受信ポート→受け手のビジネスロジック
という経路をたどってやり取りしていきます。

  • 「ポイントツーポイント」が1対1の通信。
  • 「パブリッシュ/サブスクライブ」 が1対多で、すべてのコンシューマーに送る。

  • 非同期リクエスト/レスポンスを実装するには、リクエストチャネルを通って進むメッセージにはメッセージIDとリターンアドレスとして返信用のリプライチャネルを指定する。戻りのメッセージには相関ID(correlation Id)として同じIDを指定することで、関連が分かる。

  • 一方通行の通知は簡単で送るだけ。
  • パブリッシュ/サブスクライブ はそれ用のチャネルに送信。受け手の様々なサービスがこのチャネルを見てメッセージを知る。
  • ここで受け手のコンシューマーが相関IDを入れたメッセージをリプライチャネルから送ると、パブリッシュ/非同期レスポンスになる。

 一般的には「メッセージブローカー」という仕組みを間に入れて実現します。

  • サービス同士が直接通信しあうブローカーレスアーキテクチャもある。トラフィックも軽く構成もシンプルになる。しかしサービス同士が相手の位置を知る必要がある、どれかが落ちていると可用性が下がる...などの欠点が多い。
  • 通常はメッセージブローカーを使う。ActiveMQRabbitMXApache KafkaクラウドだとAWS KinesisAWS SQSになる。
  • メッセージブローカーの利点:疎結合になる、メッセージをバッファできる、柔軟性の高い通信が可能など。
  • 欠点:かつてはパフォーマンスのボトルネックになる危険があった、単一障害点になる危険、運用の複雑度の上昇。
  • メッセージの順序の維持のためには、内部でシャーディングというメカニズムを持っていたりする。
  • 重複メッセージを処理する方法としては、受け手のメッセージハンドラ内で飛んできたメッセージIDをその都度記録用テーブルにINSERT、一意制約で失敗したら2回目なので処理しないと判別するような、べき等の処理で対応可能。

en.wikipedia.org

  • サービス側で何かが作られてパブリッシュする必要があるとしたら、DBのテーブルを使う方法がある。

<Transactional outbox>パターン
例えば注文のオーダーが作られたら原子性が保証された一連のトランザクションの中で、オーダーサービス内部に持っている [ORDER] テーブルの他に [OUTBOX] テーブルにもINSERT。ここの中を定期的に見てメッセージブローカー経由でパブリッシュしていく。

<Polling publisher>パターン
上の仕組みでは「メッセージリレー」という仕組みが定期的に [OUTBOX] テーブルの中を全件見て、メッセージブローカー経由でメッセージを発行したらその後 [OUTBOX] テーブルのレコードはDELETEしていく。このやり方のこと。

<Transactional log tailing>パターン
DBにINSERTなどが行われたら、トランザクションログを見て変更を検知する高度な方法。AWSDynamoDB streamsが実はこのパターン。

メッセージング用のライブラリもいろいろあり、高水準のものを使った方がよい。本書で紹介しているのは作者さん謹製のEventuate Tramフレームワーク

 読めば読むほどやはりメッセージブローカーがあった方がよく、ここも高度な仕組みが必要で単一障害点にならないよう堅牢でパフォーマンスが必要……クラウド上ならここでAWS SQSなどの出番となるわけか、という感じです。

aws.amazon.com

3.4 可用性向上のための非同期メッセージング

 例えば同期通信では、クライアント→あるサービスにGETリクエストが飛んできて、そのサービスが他のサービス群と通信→通信→通信→最後にレスポンスを返す……となります。この流れではどこかのサービスが落ちていたら即アウトとなります。他の手段を解説する節。

  • 非同期的なインタラクション:クライアントから発信するところからすべて、チャネルを通した非同期のメッセージングでやりとりする。アーキテクチャの回復力が高くなる。
  • データの複製:例えば顧客やレストランのサービスでデータが増えたらイベントを発行、オーダーサービスが受け取って自分が持っている内部の複製DBの中で、顧客やレストランの複製データもその都度増やしていく。
  • レスポンスを返した後で最終的な処理:クライアントからオーダー作成のリクエストが飛んで来たらオーダーサービスは自分で完結する処理だけ行い、OrderをPENDING状態で作成してDBにINSERTしてレスポンスを返して一旦終了。
     その後、オーダーサービスは他のサービス群と通信してあれこれ処理、無事終わったらOrderをVALIDATED状態にUPDATEする。クライアントは自分からポーリングして終わったか聞きに行くか、オーダーサービス側から通知を出す。

 3章は通信の話でなかなか難しくもあり勉強にもなります。ぱっと見は同期通信でサービス同士でJSONでやりとりすればいいじゃん……と思えますが、読めば読むほど一旦キューに貯めて非同期メッセージングで後から個別にやっていく、サーバーレスなアーキテクチャでもよく見るやり方が必要になるんだなあとだんだんと分かってきました。

f:id:iwasiman:20210123161130p:plain
マイクロサービスパターン

Chapter 4 サーガによるトランザクションの管理

 サービスがそれぞれ独立している中、離れたサービス同士でDBとのコネクションが保持できるわけもないしトランザクションは一体どうするんだ!?というとても大きな疑問に答える章。
 トランザクション処理において必要とされる4つの要素、Atomicity(原子性)、Consistency(整合性/一貫性)、Isolation(独立性/分離性)、Durability(永続性/持続性)を合わせた「ACID特性」のうちの I:独立性/分離性 は諦めた「ACD属性」の3つだけサポートする、「サーガ」という考え方を論じています。
ところで「ACID特性」は酸のACIDで「アシッド特性」と読むのですが、Iが抜けて「ACD特性」になっても読みは「アシッド」のままな気がします…!

ja.wikipedia.org

4.1 マルチサービスアーキテクチャにおけるトランザクション管理

こうした文脈では以前から「分散トランザクション」という考え方がありましたが、メッセージブローカーやNoSQLなど新しい技術は未サポートの場合も多い。すべてのサービスが生きていないとアウトなので可用性も下がり、クラウド時代に適合していない。ということで…

<Saga>パターン
非同期メッセージングで互いに教えあう一連のローカルトランザクションを使い、複数サービスにまたがるデータの整合性を維持するパターン。

例に上がっているのは……

  • オーダーサービスがcreateOrder()OrderをDBにINSERTする。状態はAPPROVAL_PENDINGトランザクションのT1。
  • 非同期メッセージングで知らせを受けた他の関連サービスが順番に確認のverify()をしたり、順に処理。それぞれT2, T3~。
  • 最後にオーダーサービスがapproveOrder()で作成済みのOrderの状態をAPPROVEDにUPDATEして終わり。これがトランザクションのTn。

各ステップが各サービス内で独立したトランザクションでT1~Tnまで進み、途中で失敗した場合は「補償トランザクション」という別の処理でCn-1, Cn-2....C1まで逆順に戻っていく必要がある。

  • 各サービスでレコードをINSERTする処理、createOrder()とかcreateTicket()は補償トランザクションREJECTED状態にUPDATEしていく処理がいる。(レコードのDELETEではない)
  • verify...authorize...など、SELECT文しか発行しない処理にはいらない。
  • 前処理が成功したら必ず成功するapproveOrder()のようなUPDATE処理にもいらない。
4.2 サーガのコーディネート

これを調整していく2種類のやり方を解説しています。

コレオグラフィ (choreography)

  • 各ステップを処理するごとに、非同期メッセージングでメッセージブローカーにイベントを発行する。
  • 受け側の別サービスがこれを検知、イベントを消費して自分のステップを処理、また次のメッセージブローカーにイベントを発行……
  • 途中で失敗したら~Failedのイベントを発行。前のステップのサービスがこれを検知して、順に補償トランザクションを行って戻っていく。
  • 仕組みが単純で、各サービス同士も疎結合になる。
  • しかしサーガの実装部分がサービスのあちこちに分散して分かりにくく、循環依存が発生したり複数のサービスが密結合になりやすい欠点がある。

オーケストレーション (orchestration)

  • 例ではオーダーサービスがOrderをINSERTした後、Create Order Saga用の「オーケストレーター」を作成して後を任せる。
  • このオーケストレーターが、後に続くサービスに次はチケット作っといてね~とコマンドをメッセージブローカー経由で送信。
  • 各サービスは処理が終わったらTicketCreatedメッセージを返す……の繰り返し。
  • 戻ってきたメッセージを検知して次のサービスに順次指令を出していくのはすべてこのオーケストレーターが代行する。
  • 全部終わったら、例ではオーケストレーターがオーダーサービスにApproveOrderコマンドを送って終わり。
  • オーケストレーターが各サービスに依存するだけで依存関係が単純になり、各サービスはオーケストレーターしか知らないのでより疎結合になる。調整のロジックはオーケストレーターに集中するのでソフトウェア設計の「関心の分離」が実現され、より単純になる。
  • オーケストレーターにビジネスロジックが集中しすぎるリスクもあるが、他の事をしないようにすればよい。

 読むと本書で推奨されているように、ほとんどの場合はこのオーケストレーションの方を使うのがよいように思えます。

4.3 分離性の欠如への対処方法

ACID特性のIの分離性(Isolation)は、トランザクション内部の過程が他の操作からは隠蔽され独立し、外部からは変更前と変更後しか観測できないということ。これが実現できないマイクロサービスで分離性をどう確保していくかの話。

  • あるCreate Sagaの実行中にCancel Sagaも実行されたら上書きされてしまうため、更新の消失が起こる。
  • 更新途中のデータが見えてしまうため、ダーティリードも発生する。

ある論文でこうした問題に対応するアイデア「Semantic lockカウンターメジャー」と呼んでおり、この「カウンターメジャー」を対処する方法の名前と本書では論じています。

  • カウンターメジャー:Semantic lock:: レコードにAPPROVAL_PENDING, ACCEPTED, REJECTEDなど状態を持つ列をひとつ用意し、次々と更新していく。
    後続のサービスのトランザクションは自分の処理の前にこの値に注目する。不正だったら失敗させてクライアントに再試行させる手もあり、自力でブロックし続ける手もある。
  • カウンターメジャー:Commutative updates:: 借方、貸方のdebit()credit()のように更新操作が互いに順番を入れ替え可能であれば、更新の消失が起こらない。
  • カウンターメジャー:Pessimistic view:: 途中で失敗した時の「補償トランザクション」をCn-1から逆順でやらず入れ替え、例えばOrderをキャンセル済みに更新するトランザクションを最初にやる。ダーティーリードを防げる。
  • カウンターメジャー:Reread value:: レコードをUPDATEする前に変更されていないことを確認、変更されていたらサーガを中止、最初からやり直したりする。楽観ロック。
  • カウンターメジャー:Version file :: レコードに対する操作の履歴を記録しておく。例えば会計サービスはあるOrderにCancelの操作が飛んで来たらそれを記録しておけば、その後でCreateの操作が飛んできても判別してスキップしたり。
  • カウンターメジャー:By value :: ビジネスリスクで区別。リスクが低い処理はこれらのカウンターメジャーを使ったサーガで処理。最重要のハイリスクな処理は分散トランザクションで行う。

 RDBを操作する前にレコードの更新日時や更新回数を比較、特定の列の値で状態を保持…なんかはモノリシックなシステムでもよくやるので、この辺がイメージしやすいです。

4.4 オーダーサービスとCreate Order Sagaの設計

 言語はJava、作者さんのEventum Tram Sagaフレームワークを使った実装の実例。サンプルのOrderServiceはSpringフレームワークを使っており、@Transactional@Autowired などSpring特有のアノテーション記述も出てきます。Eventum Tram Saga側のクラスも分割されており、個別にテスタブルな設計になっています。

eventuate.io

 4章は図も多くてけっこう難しいのですが、トランザクションが共有できないマイクロサービスでは、やっぱりこうやってメッセージングでサービス間で知らせ合いながら、それぞれのサービス内のトランザクションを実行していき、成功/失敗を伝えあってまた次のサービスが…とやっていくしかないのだなと分かります。
 本書ではこの考え方にSagaという名付けをしてフレームワークの実物も用意し、深掘りしているわけですね。RDBの知識も必要でなかなか難しい章です。

中編に続くよ→

iwasiman.hatenablog.com