マイクロサービス化は本当に難しい
はじめに
この記事は、AEON Advent Calendar 2023の21日目です🎉
イオンスマートテクノロジー株式会社(通称AST)のCTO室TechLeadチームの@t0doroki_takaです。弊社ではSREチームの発信に勢いがありますが、アプリケーションレイヤーよりの話題も積極的に発信していければと思います。
自分の敗戦の振り返り
以前、大規模ECシステムのリプレース案件に関わった時(そして敗戦したとき)の振り返りです。
今回取り上げるケーススタディは、システム全体(連係するシステム含む)としては段階的移行ではありましたが、主ターゲットとなるシステムは、全EC機能を包括する大規模なシステムで、それをフルスクラッチでリプレースするものでした。
巨大なモノリス構造であったため、マイクロサービスアーキテクチャに移行することで、サービス提供のアジリティを確保することが目的の一つでした。
- 段階的移行(ストラングラーフィグパターン)の予定が、いつの間にか同時移行(ビッグバーン)になってしまった。
- サービス分割単位の見直しとサービス間連係の方法の見直しが、スケジュール都合などで出来なかった。
- サービス分割方法の失敗とサービス間の連係方法に課題を持ったまま進んでしまった。
- マイクロサービスアーキテクチャは手段であるのに目的になってしまった。
1と2に関しては、プロジェクト全体の計画・課題であって、開発チーム単独では解決できない領域ではありますが、プロジェクト全体としては、そのようなリスクも初めから見込んで採用するアーキテクチャの検討、プロジェクトの計画、戦略が変化した場合の対応策もある程度練っておく必要はあると思います。今回の記事では、そういった観点はスルーして、主に3,4の内容にフォーカスしたいと思います。
ストラングラーフィグパターン
端的に言えば、モノリスのシステムを部分的に切り出していって段階的にマイクロサービス化を進めていく方法です。
Martin Fowler が10年くらい前に書いた記事が発端です。大規模なWebアプリケーション内のソースコードをリファクタリングしてリリースする際の処理方法として定義されたパターンです。マイクロサービスなんて言葉すら無い時代の概念が、ここに来て再び脚光を浴びているのは面白いです。
サービス分割単位
エリック・エバンスのドメイン駆動設計とマイクロサービスは相性が良いと言われています。確かに、業務ドメインごとに分担を割り振って、要件定義と設計・実装を複数チーム・メンバーでスケールアウトする体制の計画・構築には都合が良かったです。
しかし、設計・実装の段階になって色々と厄介な課題が多く発生しました。下記の調整で収拾が付かなくなりました。
- サービス間の依存が多く、サービス間のI/Fの増加
- DBであれば別テーブル参照やJOINするだけなのに、APIを呼び出す必要がある。
- そのため、サービスが提供しないとならないAPIが増加。
- APIの仕様調整の収拾が付かなくなる。
- サービス間を跨ぐトランザクション制御の必要性
- 一般的には、サーガパターンが定型パターンだけど、適切に設計・実装するのは高難度。それを複数のチームで同時に設計・実装するには学習コストや検証・試験を含めてコストが掛かりすぎる。
- 特に失敗した場合のロールバック。補償トランザクションだのなんだの手間が大きい。
特に REST API 仕様のすり合わせがとても難航しました。機能に基づいたサービス分割は、要求分析、要件定義の段階では、作業分担も明確になりスケールアウトして作業が出来たのですが、その後に、どうサービス分割するのか検討するフェーズを取る事が出来なくて、最終的にサービス間でのI/F・責務の調整で余計にコストが掛かってしまいました。
結果的に、「なんのためにサービス分割するのか?」そのWhyに沿っているのか検証・見直しに立ち戻ることが出来ず、もはや業務ドメインごとにサービス分割することが目的となってしまったと言っても良いと思います。
機能別分割 vs. 変動性に基づく分割
上記の本の受け売りです。
- 要件定義は機能別(ドメイン別)に実施
- システム化の際は、機能別から変動性に着目して分割を検討
- アーキテクチャの検証と見直しフェーズを最初から織り込む (必要に応じて2に戻る)
この3段構えがベタープラクティスではないかと感じました。2に関して、具体的な例として、
もしくは、最初から無理にサービス分割しなくても、下記が現実的かもしれません。
- モジュラーモノリスから始める。
- 必要に応じて、ストラングラーフィグパターンでマイクロサービス化を段階的に進めていく。
内部のサービス間の通信をREST APIにするのは悪手?
ある意味、技術的な問題ではなく開発管理的な課題で苦労しました。
各サービスを同時並行で開発していく場合、各サービスの提供するAPIの仕様変更に対して、それを利用する各サービスが追従していく必要があります。その調整・管理が難しいです。
ウォーターフォール的に「REST API仕様確定 ⇒ 実装 ⇒ 試験」という工程開発が出来れば、まだ良かったのかも知れませんが、プロジェクトの特性上、どうしても事前に把握出来ない要件・仕様が多く、設計・実装しながらAPI仕様、DB設計を変更していく必要がありました。
例えば、REST APIのレスポンスの内容が変わった場合、サービス提供側は、仕様と実装の整合をどうとるのか? それを使っているサービス側はどう仕様変更に追従(実装、試験)するのか? などです。開発サーバ上で開発サーバ上に、Swagger UI、Swaggerを使ったモックサーバ、サービス実装をデプロイして開発していましたが、開発中で変更が激しい分、サービスイン後よりも管理が複雑で大変だったと思います。
特にREST APIは、スキーマがないため呼び出し側は呼び出してみないと結果が分かりません。具体的には、Swaggerに書かれている仕様と実際のレスポンス内容が異なるケースです。実装時のJSONプロパティのtypoが原因だったり、そもそもSwaggerに書かれている仕様と実装のバージョンの不整合など、もう常に混乱状態に陥ります。呼び出し元がフロントエンドだけならまだしも、別のサービスからも呼び出しれるとすると更にカオスです。
サービス間の通信
- 非同期: キュー
- 同期: gRPCなどのスキーマ定義ありのプロトコルの採用
フロントエンドとの通信
- REST API
トランザクション制御
- まずはデータベースの分割はしない
- 但し、後々データベースの分割を想定してテーブル設計をしておく
- API間でのトランザクション境界が必要となるサービス分割をしない
といのがベタープラクティスではないか感じています。
下記の内容は、上記の本の受け売りですが、内部と外部でAPIのプロトコルを変えるのは、下記の書籍の人体システムとの対比が面白かったです。
フロントエンドとの通信(外部通信)は、REST APIを選択することが多い。だからと言って、マイクロサービス間の通信(内部通信)に、REST APIを選択することはベターとは言えない。
人体の身体と同じ。プロトコルは内部と外部で異なる。
- 社会的なコミュニケーション (自然言語など)
- 内蔵の代謝 (免疫システム)
内蔵の代謝 (免疫システム)は、生死に直結するが、社会的なコミュニケーション (自然言語など)は、そうではない。特性が違うのでプロトコルが異なるのは当然。
リプレースは新規開発より難しい
システムのリプレース案件は本当に難しいです。特に自分達が元システムの開発に関わっていない場合は、極めて難度が高く時間を必要とします。新規開発よりも大変なので、正直逃げたいと思う事が多いです。極めて難度が高く時間がかかることを、ステークホルダーに理解してもらうことも難度の高い課題の一つだと思います。
特にAsIs把握は困難を極めます。私はAsIsの把握を遺跡発掘と名付けています。既存の資料だけでシステムをゼロから構築・再現出来ることはありません。新規開発では、要件の追加・変更がずっと続いて終わらないことが問題視されますが、リプレースでは、そもそもの要件・仕様(AsIs)を事前に100%完璧に把握する事は不可能です。結局、要件・仕様というものは、開発中に変化するものだと思います。
そのような状況下で、マイクロサービスアーキテクチャとして複数のサービスを同時に開発すると、サービス間の仕様調整がボトルネックになる事は確実です。その辺りの課題にどう向き合うのか、事前にある程度の方法論を構築しておく必要があると感じます。
まとめ
- リプレース、新規開発に関わらず、対象となるシステムが複数の業務ドメインを内包していて、更に各業務ドメイン間の相互関係性が高いシステムを最初から完全なマイクロサービスアーキテクチャ化することは極めて難しい。
- サービス境界の見極め、サービス間のコミュニケーションのありかたの決定など。
- それに伴う開発チームのスケールアウト。
- マイクロサービスアーキテクチャの良い面だけでなく悪い面も見極める必要あり。
- フロントエンド、および、サービス間の通信の方法や仕様管理、トランザクション管理など、本来の目的に対してコストが超過しないか?
- 書籍やWebで当たり前に語られているモダンな技術・手法は、やってみたレベルでは容易であっても自分達のチームでモノにするには時間が掛かる。それを大人数でシステム開発にスケールアウトするのは想像以上に大変。
- 挑戦も大切だけど、まずは自分達が確実にやれることからやる。
- 最初は、モジューラモノリスからはじめるのがベターではないか?
- アーキテクチャ検証のフェーズを予めプロジェクト計画に織り込む。
- リプレースは新規開発より難しい。そのことをステークホルダーに理解してもらうことも難度の高い課題の一つ。
絶賛採用中です!
イオンスマートテクノロジーではエンジニアをはじめとした様々な職種を積極的に採用中です!
これからとてもおもしろいフェーズへ突入していくと思いますので興味のある方は是非カジュアル面談などで話を聞いてください!
Discussion
よくあるマイクロサービス化の方法としては、
Gherkinで既存サービスの振舞いを漏れ無く記述→E2Eを実装(既にあれば尚良し)→既存サービスでとりあえず100%目指してE2Eを拡充→うらでマイクロサービスの設計をしつつ徐々に実装→ある程度出来上がったら、本番のリクエストをコピーしてマイクロサービスが機能してるかテスト→本番の前日にマイグレ(マイグレ以降の本番のリクエストをキューに入れておく。そういえばMQ入れておくと便利)→マイクロサービスを稼働させる。
kubernetesも必要ですし、DBの設計、キャッシュ化、Gitの管理の仕方、ほかいろいろ考えることが山程あって技術者にとってはやりがいがありますね。