この記事はGLOBIS Advent Calendar 2020の9日目の記事です。
こんにちは、@yohira_devです。
数年前からグロービスの開発案件に参画させていただいています。「智将は務めて敵に食む」という言葉がありますが、今回はグロービスのAdventCalanderで記事を書かせていただきます。
今回のAdventCalanderでは、「モノリスからマイクロサービスへの設計の切り替え方」について、書いていきたいと思います。
この記事を読むべき人
- 「マイクロサービスってなんか良さそう!うちもとりあえず導入してみようかな...」と思っている人
- モノリスでは起こらなかったデータ不整合に消耗している人
モノリシックアプリケーションとマイクロサービスの違い
モノリシックなWebアプリケーションを書いている感じで「手なりで」マイクロサービスを実装すると思わぬ落とし穴に嵌ることがあります。
単一のデータベースではトランザクションを適切に利用することで、
例えば以下のケースを考えます。
- G社は、Web上で教材コンテンツを販売するWebサービスを運営している。
- ユーザーはコンテンツを期限付きで購入することができる
- ユーザーがコンテンツを購入すると、購入の会計データが作成される
モノリシックWebアプリケーションだとこういう感じ(※Railsを模した擬似コードです。引数の扱いとかが間違ってたらすみません)
ActiveRecord::Base.transaction do
product = Product.find(params[:product_id])
payment = Payment.create!(user: current_user, product: product)
product_period = ProductPeriod.create!(product: product, type: "1week")
Accounting::Sales.create!(payment)
end
ActiveRecord::Base.transactionで囲われているので、この4つのメソッド呼び出しでどこかが失敗すると全部のレコードがロールバックされ、このメソッドが呼び出される前の状態に戻ります。
ユーザーに再操作を要求こそしますが、データの不整合は起きていません。
これが、マイクロサービス化されている場合も考えましょう。
- G社は、Web上で教材コンテンツを販売するWebサービスを運営している。
- ユーザーはコンテンツを期限付きで購入することができる
- 決済は決済用のマイクロサービスにHTTP通信で作成する
- 会計データは会計データを扱うSaaSにHTTP通信で作成する
(※Railsを模した擬似コードです。引数の扱いとかが間違ってたらすみません)
ActiveRecord::Base.transaction do
product = Product.find(params[:product_id])
payment = Faraday.post("https://sugoi-kessai.com/payment",{user: current_user, product: product})
product_period = ProductPeriod.create!(product: product, type: "1week")
Faraday.post("https://shanai-kaikei.com/sales/",{payment: payment})
end
paymentの作成とAccounting::Salesの作成が外部通信になっています。
さて、この処理で Faraday.post("https://shanai-kaikei.com/sales/",{payment: payment})
が何らかの原因で失敗したらどうなるでしょうか。
トランザクション内で例外が発生したのでproduct, product_periodはロールバックされて「なかった」ことになりますが、問題はpaymentです。
外部通信でCREATEを正常に通してしまっていると、いくらこのメソッドがRollbackされようとも元に戻りません。
購入失敗したのに決済が走っている、データ不整合状態が発生しています。
サービスをまたぐとトランザクションでは一貫性を保証できなくなることに注意しましょう。
トランザクションの仕組みに乗っかりたい局面ではマイクロサービスを用いるべきではありません。
だいたいの場合はトランザクションの仕組みに乗っかったデータ整合を期待して実装するほうがラクだったりもします。マイクロサービスのご利用は計画的に。
実装のポイント
サービス間のインタフェースで冪等性を保つ(いつでもリトライできるようにする)
各サービス間のAPIインタフェースは、「同じリクエストを投げると、同じ結果が返ってくる」ように実装する必要があります。
これを怠ると、↑で起きたデータ不整合に対する対応が難しくなります。
受け取り手側は、イベントが複数飛んできても対応できるように実装する
CRUDでは特にcreateに注意。手なりで書くと失敗しやすい
RESTful APIのなかでは、CREATEメソッドの実装に特に注意しましょう。
「最初の通信が失敗したのでリトライしたら二個レコードが出来てしまった...」というのはよくある話です。
トランザクションの再発明をすることになるので、必要ないところでいたずらにサービス分割をすべきではない
そもそもの話になりますが、モノリスにしてトランザクションの仕組みに乗っかっていれば発生しなかったエラー処理や例外的な実装を書くことになります。
なので、サービス分割は「細ければ細かいほどいい」というものではなく、システムアーキテクトと呼ばれる人はシステムの切れ目を適切に見抜く魔眼、今風に言うと、隙の糸を見極められる能力が必要になってきます。やっていきましょう。
チーム開発としての設計のポイント
マイクロサービスを検討するときは、どのようにサービスメッシュを切っておくのかをチームとして合意を取るべきです。
どの切り方でもメリット・デメリットがあり、慎重に議論を重ねて設計をするべきです。銀の弾丸はありません。
個人的には3つの方法があると思っています。
Before
ところで、グロービス麹町オフィス5Fは壁面一面がホワイトボードになっていて、実装イメージを書いて共有しながらチームメンバーと設計作業をすることができます。うれしいですね。
認証、決済などで切る(横に切るやり方)
システム全体のうち、処理が同じで別サービスに対しての横展開ができそうなモジュール単位でサービスを分割する方針です。
例えば「認証」「決済」など。
このやり方では、
- 直感的でわかりやすく、DRYに書きやすい
- エンジニアが決済などの専門領域に注力しやすい
というメリットがある一方で、
- モジュール間のトランザクションを保証できない
- 業務設計を失敗し、「サービス固有の事情を共通モジュールが取り込んでしまう」と、サービスと密結合なモジュールができてしまう**(よくある)**
- その実装が、モジュールを利用しているサービスに影響することもある
というデメリットが想定されます。
サービス一括で切る
縦に切るやり方。
一見モノリスを2つにしただけに見えますが、結局マイクロサービスでやりたいことは「デプロイの独立性を保って高速にCI/CDを保ちつつ、ビジネス的な仮説検証を回したい」ということなので、こういった設計も有効に機能する場合があります。
例えば、
- 一見共通のサービスのように見えるが、個人向けのプロダクトと法人向けのプロダクトではビジネスロジックが異なっている。個人向けではあいまいさを許容した投機的な機能設計をしたいが、法人向けでは厳格な機能設計をしたい
- 個人向けではクレジットカード以外の支払い方法を許容しないが、法人向けでは請求書払いや口座振替、出精値引きを許容したい
などといったケースが考えられそうです。
このやり方では、
- サービスドメイン領域でのトランザクションが保証できる(サービス間の通信におけるリトライ安全性は重要ですが、RDBの機能で担保できるならそれに越したことはない)
- サービスに最適化したロジック(認証、決済など)を気兼ねなく実装できる。(別サービスは別サービスで認証・決済のコードを持っているため)
というメリットがある一方で、
- コードの重複が生まれやすい
- 決済や認証がサービスに最適化した横展開しにくい作りになる可能性がある(むしろサービスに最適化するために縦に切っていくのでこれはデメリットではないかも)
というデメリットが想定されます。
組織構造に一致するように切る
はい。
「システムの構造と組織構造は一致する」というコンウェイの法則があります。
上記の2つの方法は組織構造を無視してプログラムの論理構造のみを考えてきましたが、実際、きれいに横に切ろうが縦に切ろうが切った部分と実際の部署が分かれていると非常につらいです。(コミュニケーション、前提のズレ、落ちたボールを誰も拾わない問題...)
本来は組織を最適化するのが王道アプローチですがまぁ組織ってトップの覚悟がないとそう簡単に変えられないのでこういう場合は既存の組織に合わせてサービスを作っていくしかなさそうです。
おすすめ書籍
我々のチームでは定期的に読書会を開催して、設計やコーディングに関するノウハウの共有、速度と品質を両立できるチーム作りを目指しています。
以下に、今まで読書してきた本を3冊ピックアップしましたので、アプリケーションの設計レベルを高めたい方はぜひ参考にしてみてください。
Microservices architecture よろず本 その一&その二
【PR】【PR】【PR】グロービスでは一緒に働くエンジニアを募集しています。
グロービスでは一緒に働く仲間を募集しています。
私自身、課金や決済の最適化をミッションとしたチームにおり技術的には大変成長できる環境だと思っていますが、いかんせん人が足りないです。。。(´;ω;`)
新規事業 教育の新たなプラットフォームを作るサーバーサイドエンジニア募集! - 株式会社グロービスのWebエンジニアの求人 - Wantedly
私自身のnote,ブログもよろしくお願いいたします。