最近分散システムチックなコードに触れる機会が増えたので、参考になるかと思い読んでみています。
この記事はその読書ログです。まずは第 I 部です。
I部 基礎
1章 マイクロサービスとは
「ビジネスモデルを中心にモデル化された、独立してリリース可能なサービス」
特徴として次がある。
- 独立デプロイ可能性
- マイクロサービスはこれを目的とする
- 技術よりも、ビジネスモデルに基づきサービスを分割する
- 技術で分割すると職務専門チームができる
上記の特徴を持つことで、コンウェイの法則に基づいたストリームアラインドチームを組みやすくなる。
独立デプロイが可能で、他のサービス影響を与えないので、チームは独自の判断で高速にデプロイができる。
ビジネスモデルに基づきサービスが分割されることで、職能でなくサービスごとにチーム(ストリームアラインドチーム)を組むことができる。
マイクロサービスの対になるものはモノリスだが、間にグラデーションがある。
- 単一プロセスモノリス
- コードも一つ、DB も一つ
- 小さいチームでは非常に有効
- モジュラーモノリス
- コードレベルでは分割されているが、DB は一つ
- 分散モノリス
- 複数サービスから成り立つが、デプロイはまとめる必要がある
- 分散システムの欠点(複数サービスによる複雑さ)とモノリス(デプロイを同時に行う)を併せ持つので、筆者はおすすめしていない
マイクロサービスには次のような利点もある。
- 技術の異種性 = 各サービスごとに異なる技術を導入できる
- 堅牢性 = (設計にもよるが)1つのサービスが停止しても、他に影響を及ぼさない
- (設計によりこれを実現できることが分散システムの強みだが、ここを設計するのが難しそう)
- スケーリング = 重要なサービスに絞ったスケーリング
- デプロイ容易性
- 合成可能性 = サービスを組み合わせてさらなるサービスを生む
逆に多くの課題もある
- 開発者体験の悪化のおそれ = ローカルで開発が難しくなる事例も
- 技術の過負荷 = マイクロサービス化と同時に多くの技術的刷新をしがちで過負荷になる
- コスト増 = インフラやチームのキャッチアップ
- レポーティング難易度増 = 横断的なデータを用いた分析が大変
- 監視とトラブルシューティングの複雑化 = 分割するほどシステムの状態数が爆発するので、問題の特定が難しい
- テスト = E2E の難易度が跳ね上がる
- 遅延 = あらゆる処理に通信が入る
- データの一貫性 = データベースのトランザクションに頼れない
様々な課題が発生するため、安易に取り入れるべきでない。
少なくともドメインモデルが安定してから、またサービスごとにチームを設けられるような十分な人員が確保できてから。
2章 マイクロサービスのモデル化
ビジネスモデルに基づきサービスを分割するために、情報隠蔽が重要。
「情報隠蔽 = あるモジュールが他のモジュールに対して仮定する事項を減らす」
これにより、モジュールを独立・並列して開発したり、個別に理解できたり、独立して変更を行えるメリットがある。独立デプロイ可能性のためには必要である。
隠蔽の度合いを示す指標がある。
- 凝集 (cohesion)
- 一緒に存在し、一緒に変更されるコード
- 結合 (coupling)
- モジュール間の依存
結合はいくつかに分別できる。
- ドメイン結合
- ドメインロジックを起動するために他のサービスを呼び出す
- これがないとサービスが成り立たない
- パススルー結合
- さらに下流のサービスで必要なデータを送るために、そのデータを送ること
- e.g.) サービス A がサービス C で必要なデータをサービス B 経由で送信する
- データの形式を変更するには A、B、C 全ての修正が必要になる
- できる限り下流サービスと直接通信するようにする
- 共有結合
- 複数のサービスが同じデータを参照・更新すること
- 静的なデータを参照する分には問題ないが、動的に更新される状態を持ったデータを複数サービスから更新する構成は避ける(ロジック分割がうまくできていない)
- 内容結合
- 上流サービスが下流サービスの内部状態を変更する
- 他のサービスがあるサービスの内部状態を知るべきではない(情報隠蔽できていない)
- (普通にやっていればこの結合は発生しなさそう?)
ドメイン駆動設計などを利用し、これらの結合を減らしていく。基本的には一つの集約を複数のサービスに分けないようにする(複数の集約を一つのサービスにいれるのは OK)
3章 モノリスの分割
モノリスをどのように複数サービスへ分割していくかの心得を示す。
- なぜマイクロサービスにしたいのかを明確にする
- 少しづつ移行する
- モノリスを許容する(悪者にしない)
- ドメイン理解が十分出ない間は行わない
(具体的な方法論があるが省略)
マイクロサービスにすることでデータを分割する必要があるが、データ分割には次のような懸念がある
- パフォーマンス
- JOIN が使えないので、データ取得のパフォーマンスが悪化する
- データの完全性
- 複数の DB をまたぐので、DB の制約による完全性・整合性が担保できない
- トランザクション
- 同様にトランザクションによる保証がない
4章 マイクロサービスの通信スタイル
複数サービス間の呼び出しはプロセスの呼び出しとは次のような違いがある
- パフォーマンス
- 通信が必要、データのサイズを気にする必要があることも
- インタフェース
- 互いに影響なく変更するために、互換性の考慮が必要
- エラー処理
- より多くのエラー状態がありうる
- エラーの表現力を高くする必要がある
- 例:”Distributed Systems” で挙げられている障害モード
- クラッシュ障害
- サービスが停止した状態
- 欠落障害
- 送信したのにレスポンスがないなど
- タイミング障害
- 起こるのが早い・遅いというズレに起因するもの
- レスポンス障害
- 期待しないレスポンスを受け取ったケース
- 任意障害
- 参加者間で発生の原因が同定できない
- (どちらで発生したか区別できないケース?)
サービス間の通信方式は「同期ブロッキング」と「非同期・非ブロッキング」に大別できる。
- 同期ブロッキング
- リクエストを送り、レスポンスがあるまで待機する
- サービス間の時間的結合が避けられない
- 次のような課題がある
- レスポンスがサービスに到達しなかったとき
- レスポンスが遅れたときにサービスに負荷がかかる
- 呼び出しが連鎖するとリソースを圧迫する
- 1箇所で失敗すると全体で失敗する
- 非同期・非ブロッキング
- レスポンスを待たず、ブロッキングしないのが特徴
- 実現方法いくつかあり、後述する
- 処理にかかる時間を気にしないので、数日・数時間といった処理にも対応
具体的な通信手法をいくつか挙げる。
- リクエスト・レスポンス
- リクエストを送り、レスポンスを待機する
- 次の処理の前にリクエストの結果が必要なケースで利用
- 何をすべきかを知っているのはリクエスト側
- 同期ブロッキング版
- 単純に実装できる
- 非同期・非ブロッキング版
- キューを使うことで非同期にもできる
- (GCP で言うなら Cloud Tasks になる…?)
- (AWS ならこれ? https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-temporary-queues.html)
- イベント駆動方式
- 非同期・非ブロッキング
- イベントを発行し、それを監視しているサービスがそれに基づき処理を実行する
- 何をすべきかを知っているのは受信者側
- 共通データを介した通信
- 非同期
- あるサービスが配置したデータを他のサービスが参照する
- ポーリングが多い
- 通信と認識されないことも
- 低遅延が求められる場合は不適