こんにちは、SREグループの水戸 (@y_310) です。MoTでは多種多様なマイクロサービスが稼働しています。マイクロサービスは一般的に、あるプロダクトを何らかの形で分割した内の一部の役割を担うものです。その分割されたマイクロサービスにおいてどこまでをそのサービスの責務とし、どの役割を含めてどの役割を含めないか、という他のサービスとの適切な境界を見つけることは非常に重要です。SREグループでは多くのマイクロサービスのアーキテクチャ設計に関わりながらマイクロサービスの分割点についての知見を得てきました。この記事では実例をベースにマイクロサービスの分割点の見つけ方について書きたいと思います。
ビジネスロジックとの依存度が低い単機能かつ汎用的な機能は、データストアとの依存性も低く比較的容易に単体のサービスとして切り出すことができます。
移動に関わるサービスでは位置情報を扱うことが頻繁にあり、緯度経度から住所に変換するリバースジオコーディングは様々なサービスで必要とされる機能ですが緯度経度を元に地図データベースを検索し近傍の住所文字列を返す処理のため、依存データが地図データのみとなり独立したAPIとしての実装が容易にできる例となります。ちなみにこちらのサービスは先日開催されたGo Conference 2021 Autumnで詳細が公開されていますのでこちらの資料をご参照ください。
その他の例としては、営業日の判定サービスや、特定の日時・場所のタクシーの需要予測をするサービスなども依存性の少ないサービスとして切り出されています。
あるサービスの中で一部のAPIだけ他のAPIと比べて極端にリクエスト数が多かったり、ピークのタイミングが異なっていたりすると、スケールアウト時にその処理と関係のない処理も含めてリソースを割り当てる必要があるため非効率です。そういった機能は別のサービスに切り出すことで必要最小限のCPUやメモリを割り当てることが可能になるためリソース効率を改善できます。
タクシー車載デバイスからのログ情報を受信するサービスはほぼ毎秒全ての車両からリクエストを受信するため常時大量のリクエストがある状態になります。ただ受信したログはデータストアに書き込むだけのため処理自体はシンプルで負荷の低いものです。そのためCPUやメモリ使用量の非常に小さいコンテナを大量にスケールアウトさせてさばくことでリソースの最適化を行っています。
こういった処理は他のよりリソース使用量の多いサービスと同居させてしまうとスケール時に無駄なリソースを使うことになっていますためマイクロサービス化することが効果的でした。
様々な用途で参照されるデータを持つサービスはクライアントの種類に応じて複数の認証方法を提供することがあります。例えばMoTではユーザ情報管理やタクシー配車機能を提供するコアサービスは一般のタクシー利用者の方向けアプリ、タクシーに搭載する乗務員の方向けアプリ、MaaSアプリなど外部サービスに配車機能を提供するAPI、内部向けの管理画面など様々なクライアントに向けてAPIを提供する必要があり、それぞれ必要とする認証方法も異なります。あるクライアントには利用者が入力するIDやパスワードによる認証、別のクライアントにはAPIキーによる認証、またIoT端末にはデバイスのキッティング時に埋め込む秘密情報をベースとした認証など複数の認証情報を扱う必要があります。 また利用者がアクセス可能なリソースを判断する認可についても、一般利用者向けであれば利用者本人の情報のみ、管理画面であれば複数の利用者を横断した情報へのアクセスなどコンテキストによって認可範囲は変化します。 そのため認証・認可を担うサービスをリソース自体を扱うサービスと分離することで特定の認証方法に依存した部分を局所化し、反対に認証方法に依存しないリソースの参照、変更を共通化することができます。
今年リリースしたGO Dineというタクシーを使ったフードデリバリーサービスでは、ユーザ向けアプリ、レストラン向けアプリ、レストラン向け管理画面という複数のクライアントを当初からサポートする必要がありました。ユーザ向けとレストラン向けでは認証の主体となるユーザも認可の範囲も異なる(ユーザは自分の注文しか見えない、レストランはそのレストランに来た複数のユーザの注文が見える、など)ため、それぞれに対して個別の認証・認可ゲートウェイレイヤとなるサービスを実装しました。これによってコアとなるデータモデルを管理するサービスとクライアント固有のロジックを持つサービスを切り離し拡張性の高いサービス設計ができました。
この詳細についてはMoT Labでも以前ご紹介しています。https://lab.mo-t.com/blog/mot-online-techtalk-6
あるサービスが処理を完遂するために外部にある別のサービスにリクエストしてデータを取得するケースがあります。この場合に依存先のサービスが提供するすべての機能ではなくごく一部の機能を必要としていたり、同じサービスに複数のサービスが依存していてそれぞれで相手先のサービスを使うためのロジックを実装する必要がでてくる課題があります。こういった時に外部サービスへのプロキシとなるマイクロサービスを導入することで外部サービスを使用する複雑性をこのサービスに閉じ込め、利用者側はシンプルなインターフェイスで機能を使用することができるようになります。
MoTのタクシー配車サービスは複数の決済手段に対応しており、決済手段に応じて別々の決済代行サービスを使用しています。決済は決済代行サービスを使った上でもデータの整合性の担保や適切なエラーハンドリングなど考慮事項は多く、また決済手段ごとのインターフェイスの差異もあり決済を使いたいサービスごとにこれらを適切に実装することは難易度が高く工数も余計にかかってしまいます。
そのため複数の決済代行サービスの間を取り持つサービスを開発し、それに前述の様々な固有のロジックを実装する形を取りました。これによって決済利用側は比較的シンプルな実装で高品質な決済処理を実装可能になりました。当初はタクシーアプリGOのバックエンドでのみ使用していましたが、今では前項で紹介したGO Dineでも同じサービスを経由して決済を行っています。
最後に分割が難しいと考えているケースについてご紹介します。 サービスのコアとなるデータを保持するデータベースは肥大化しがちなためマイクロサービスに分割したくなるケースが多いです。多くのモノリスからマイクロサービスへの分割プロジェクトもこういったケースが発端になるかと思います。しかし、コアなデータ、例えば顧客情報、商品情報、注文情報などは一見境界線を作れそうですが、ユースケースを精査していくとテーブルのJOINやトランザクションを使用した更新が必要になることが多く、単純に分割してしまうと検索性能やデータ整合性に問題を抱えることになります。
それを解決する方法として検索についてはCQRSのように参照専用のデータストアを別途構築したり、データ整合性に関してはSagaパターンのような手法があります。ただいずれの方法も一般的なAPI実装に加えて追加で開発が必要になる項目が多くアーキテクチャ全体としても複雑化するためそれらについて十分な開発リソースが取れない限りは分割しない方が安全だと考えています。
マイクロサービスの分割について実際の事例をベースに分割点の見つけ方を紹介しました。誤った分割をしてしまうと依存関係を局所化して開発を効率するはずが、反対に強く依存した複数のサービスを生み出してしまい分割前より開発効率が悪化してしまうことも考えられます。この記事がより良い選択への助けに少しでもなれば幸いです。
興味のある方は 採用ページ も見ていただけると嬉しいです。
Twitter @mot_techtalk のフォローもよろしくお願いします!