☎️

[MicroService] 俺流 サービス分割アンチパターン

に公開

はじめに

マイクロサービス開発において、サービスをどのように分割するか。
これは永遠のテーマだろうと思っている。

殆どの開発者はこの問いに対して、ドメインごとに分割するだの頭の中に入れられる量が一つのマイクロサービスのサイズだのというふうに言われているが、はっきり言ってビジネスロジックだけ見てサービスを分割すれば、ほぼほぼ間違いなくかったるいことになる。 断言する。
というわけで、今回はそれについて書いていこうと思う。

実際の経験

私は現在phoxという独自のCRMを開発している。
これはどちらかというと商品としてというわけではなく、社内システムを商品にもできるようにという意図があって、今までノーコードなどで適当に作っていた社内システムを今一度再開発しようと言った意図が強い。
せっかく手元にK8sクラスタがあるので、Istioで超イカしたマイクロサービスメッシュを作ってやろうと思ったのだが、色々壁にぶち当たったので、それの備忘録も含めて、ここでそれに関する情報を残しておこうと思う。

トランザクションがダルくなりそうだったら諦めろ!

マイクロサービスとしてサービスを分割したとしても、データベースも綺麗に分割できるわけではない。
というか、その分割の仕方が課題になることが殆どだろう。
そして、どう足掻いても複数のサービスに跨ってデータを操作することも必然として発生する。

こう言った時に、最もわかりやすくて実装しやすいのは、
マイクロサービスAで管理しているデータと、マイクロサービスBで管理しているデータが1to1の関係だとして考えると、マイクロサービスAに特定のAPIが叩かれた時に、AからBにデータを作成するAPIが叩かれる。といった内容が考えられると思うが、これはアンチパターンである。
なぜならこれは垂直結合の関係になってしまい、可用率が低下するというのと、マイクロサービス間で依存関係ができてしまうと、これはデススターの始まりだ。

依存関係が後々複雑になればなるほど、問題を解決するまでの手順が長く複雑になってしまい、保守性を上げるためのはずのマイクロサービスの意味がなくなってしまう。
マイクロサービスは、あくまで水平分散を徹底しなければならないのだ。

RabbitMQのようなメッセンジャーや独自のゲートウェイを設けて、非同期で処理した方が美しいだろう。
では、もしこれらのデータの整合性がかなり重要なものであった場合、トランザクションが必要だ。
結論、これを解決する方法はSagaパターンのようなアプローチがベストプラクティスとされている。

Sagaパターンはどう考えてもダルすぎる

実際に企業が導入している例としてメルカリがある。
メルペイのポイントをメルコインに変えるために、サービス間トランザクションが必要というものだ。
https://engineering.mercari.com/blog/entry/20230614-distributed-transaction/

だが、はっきり言ってこのような自体になること自体避けるべきだと思う。
これが通っているのは、メルカリが腐るほど金を持っていて、かつ動かしているプロジェクトがとてつもない大規模なプロジェクトだからこそ、各サービスをマージすることができず、このような状態になっていると言ってもいい。

普通に考えて補償トランザクションなんて絶対書きたくない。
結局のところworkflowを管理するツールがトランザクションに関する処理をやっているだけだし、どっちにしろ仕様は複雑化するので保守も大変だ。

ということがあるので、そもそもサービス間でトランザクションが起こるということ自体避けるべきだと思う。

認可がダルくなりそうだったら諦めろ!

まずマイクロサービス開発で、トークンベース認証を採用していない方が珍しいと思うため、auth0やfirebaseでjwtを発行してそれで認証をしていることを前提に話を進めさせてもらおう。

例えば、私が開発しているphoxでは、顧客サービスと架電サービスというものが存在する。
顧客サービスで管理している顧客に対して架電を行うというわけだ。シンプルだろう。
要はCRMとCTIという分け方だ。

架電サービスと顧客サービスに分けたのは、架電サービスはzoom phoneなどと連携する処理を追加していくことを考えたら、分けた方が後々拡張しやすいと考えたからだ。

だが、要件として、顧客レコードは、顧客リストテーブルと紐づいており、
顧客リストテーブルにて、顧客の情報にアクセスできるユーザーの一覧を格納している。

当然、架電情報は顧客情報と紐づいているわけなので、認可されていないユーザーが架電を行ってはいけないのは自明だろう。電話番号がわかればシステム上は可能だとしても、少なくとも架電履歴として紐づいたらそれはおかしい話である。

しかしどうしたことか、認可に関するロジックは顧客サービスにある。
これを回避するには、ペイロードにアクセス可能な顧客IDを格納することが考えられるが、これも現実的ではない。普通に考えてRBACといったレベルではない。
ペイロードが長くなればなるほど署名の検証にかかる処理も長くなるし、何より各認証プラットフォームがペイロードをどれぐらい柔軟に設定できるかにだいぶ依存してしまう。

ゲートウェイやサイドカーパターンなら対応可能かもしれない。
例えばOpen policy agentという認可に関するロジックを記述できる技術もある。
httpにも対応しているので、envoyのようなプロキシにjwtを検証させてから、OPAで顧客サービスにHTTPリクエストを送信して認可を行う。 といったこともできるだろう。

ちなみに、OPAを使わなくてもEnvoyにはExternal Authorizationというものがあり、
顧客サービスであらかじめ定義されたスキーマで待ち受け、架電サービス側のEnvoyから顧客サービスのExternal Authを叩くという方法も取れるだろう。

だが、お察しの通りこれもSagaパターンとおんなじ目に合っているのがわかるだろう。
こんなに認可のロジックが難しくなっているとテストも難しくなるし、ビジネスロジックがインフラにいくらなんでも依存しすぎている。

というわけで、認可がそれなりに複雑な場合もダメだ。
roleなどで、そもそもそのサービスにアクセスできるかどうかが決まるといった単純な内容ならサイドカーパターンで全然大丈夫だとは思う。

Discussion