メルカリShopsはマイクロサービスとどう向き合っているか

こんにちは。ソウゾウのSoftware Engineerの@napoliです。連載:「メルカリShops」プレオープンまでの開発の裏側の3日目を担当させていただきます。

メルカリShopsではマイクロサービスアーキテクチャによる開発を採用しています。ここではメルカリShopsではどのようにマイクロサービスと向き合っているかを紹介させていただきます。

メルカリShopsのマイクロサービス群

メルカリShopsはざっくりと、図のような形でマイクロサービス群が構成されています。

Frontendがひとつ、Backend For Frontend(BFF)がひとつ、そして(執筆時点で)約40ほどのBackendサービスが、それぞれが独立した実行環境で稼働しています。

BackendはShop(ショップ)Product(商品)Order(注文)Payment(決済)といったドメインごとに独立したサービスが構築されており、ひとつのサービスはおおよそ10から20種程度のAPIを提供しています。

各サービス間の通信は基本的にgRPC / Protocol Buffersによって行われています。

マイクロサービスのメリット

システムを構築する際、マイクロサービスをアーキテクチャとして採用するメリットは何でしょうか。細かく考えると多岐にわたりますが、大きくは以下のような点が挙げられると思います。

  • 変更による影響を局所化できる
  • システムリソース(機能)を効率的に再利用できる
  • サービスごとに自由な技術選定ができる

ソウゾウは会社のValueとして「Move Fast」を掲げており、開発に置いても「スピード感」をとても重視しています。もちろんスピードだけでなく、誰もが知っているような大きなプロダクトに育てていきたいという想いもあります。

大規模なプロダクトにおいて、短期的にも長期的にも開発のスピード感を維持し続けていくという目標を掲げた時、マイクロサービスアーキテクチャの採用は必然と言えるものでした。

そしてこの「開発のスピード感を維持し続けていく」ために「変更による影響を局所化する」という点が非常に重要な要素となってきます。

日々巨大化していくシステムの中で、「いちエンジニアがシステム全体を詳細に把握してから開発をスタートさせる」ことは困難です。困難というより無理と言っても過言ではありません。「ある箇所を触ったら思いもよらぬところで不具合が出た」なんてことが日常茶飯事で起こっていたとしたら、とてもじゃないですが「スピード感のある開発」は行なえません。

かといってスピードを重視するあまり、テスト不十分のまま不具合だらけの機能を提供するわけにもいきません。開発メンバーは変更による影響を注意深く探し続け、特定できた影響範囲に対して入念なテストを実施する必要があります。テストはどのような場合でも非常に重要なステップではありますが、その量が増えれば増えるほど、機能追加や改善に掛けられる時間はどんどんと少なくなってゆきます。

また、そのような複雑なサービスはあとから見た時や新しいメンバーが開発に参加した際に全体像の把握が難しくなります。どんなに優秀なエンジニアでも人間が一度に把握できる情報量には限界があります。安心してのびのび開発するためにも、影響範囲を局所化していくことは非常に重要なのです。

「サービスごとに自由な技術選定ができる」という点に関してはメリットではありますが、デメリットにもなりえます。プロダクトを構成するマイクロサービス群のなかで、各マイクロサービスごとにあまりにもバラバラな技術スタックを採用していると、いざ開発メンバーでメンテナンスを行おうとした際に、それぞれの技術による知識を習得することから入る必要があり、とても大変です。

メルカリShopsではサーバサイドはGolang、フロントエンドとBFFはTypescript、データベースはCloudSQL(PostgreSQL)などといったように、使用する言語やサービスを限定的にして開発を行っています。

マイクロサービスのデメリット

一方で、マイクロサービスアーキテクチャにはデメリットも多くあります。

  • 設計の難易度が上がる
  • データの一貫性を担保するのが難しくなる
  • サービス間通信によるレイテンシが大きくなりやすい
  • 一定規模以上のプロダクトでない限り、恩恵を感じることが少ない
  • コードや設定が冗長になりやすく、初速が出にくくなる

個人的には、「設計の難易度が上がる」ことが一番のデメリットであるかなと感じます。正直なところ、マイクロサービスアーキテクチャは「エンジニアとしてこれから頑張っていくぞ!」というフェーズの方にはおすすめできないアプローチかもしれません。ゲームで言えば初見からHARDEST Modeでプレイするようなものです。一気に設計の難易度が上がります。

幸いにしてソウゾウには歴戦のツワモノエンジニア達が揃っているので、この点についての不安はそこまではありませんでした。(ただ、それでもやっぱり開発しながら難しいな…と感じることは多々ありますが…)

しかし設計の難しさをクリアできるとしても、「コードや設定が冗長になりやすく、初速が出にくくなる」という問題は無視できるものではありませんでした。ひとつのマイクロサービスを作るためには、実行環境の設定、権限の設定、依存サービスの設定、Bootstrapコードの作成、などなど、色々と作業があります。特にインフラ系の設定周りなどは普段アプリケーションよりの開発を行っているとハマりやすく時間が吸い取られていくポイントでもあります。

サービスを起動するための初期コードの作成も地味に大変です。例えばサーバサイドであればサーバを起動するための基本となるコードです。マイクロサービスアーキテクチャではなく、既に稼働しているサーバに機能追加するのであれば基本的には必要とされない作業です。生産的な作業でない割には時間が取られますし、「似たようなコードだから」といってテストしないわけにもいきません。

Code Generation

このマイクロサービス初期構築時の負担の軽減のため、メルカリShopsでは初期コードを自動生成してくれるCode Generationの仕組みがあります。

具体的にはyamlファイルに記載された設定をもとに、goのプログラムがサーバ起動のための最低限のコードを自動生成します。

これにより開発メンバーはサーバの「機能実装」に集中することが出来るとともに、マイクロサービス構築までの時間を大きく短縮させることが出来ます。

もちろんひとつの自動生成プログラムだけで十分な自動生成ができるわけではありません。メルカリShopsでは他にもコード生成の負担を減らすためのプログラム(ツール)が用意されています。

コードが冗長になることを受け入れ、ツールによってその負担を軽減していく、というのがメルカリShops開発での基本のスタンスとなっています。

Circular dependencyの問題

マイクロサービス群が成長していくと、マイクロサービス同士がCircular Dependency(循環参照)を起こしていないかを気をつける必要があります。

Circular Dependencyが発生すると

  • サービス単体で再利用がしづらくなる
  • あるサービスで不具合が発生した際に、関係のない箇所にまで影響が及んでしまう
  • 依存されているサービスからデプロイができなくなり、一時的にダウンタイムが発生してしまう

などといった問題が出てきます。

例えば以下のような構成のシステムがあるとしましょう。

ここでService EService Bを参照し、循環参照が発生したとします。

その上で、Service Cに障害が起こったとします。

基本的に、あるサービスが障害等によりダウンすると「そのサービスを利用しているサービスにも影響がある」ので、Service BService Aにはもちろん影響があります。

そして、循環参照によりService EService Bを利用しているので、Service Eも影響を受けてしまいます。

これは循環参照により「もともと関係性のなかったサービス同士が影響を受けることになってしまった」状態と言えます。

多くの場合、Service EService Bを参照するのは「Service Bの一部の機能を使いたい」という動機からでしょう。しかしその副作用としてService Cとも繋がってしまうため、意図しない密結合を生んでしまうことになります。

また、仮にService FService Eに依存していたとします。それ自体は何も問題はありません。しかし循環参照により、Service Cが死ぬことで、運が悪いとシステム全体が死んでしまう構成になっている」ことになります。

また、循環参照があるとデプロイもうまくいかなくなることがあります。

通常、サービス全体でダウンタイムなしにデプロイするには、「依存されているサービス」からデプロイしていく必要があります。

しかし、もしService CService Bを参照する循環参照が起きていたとすると、「BとCはどちらが依存されている側?」となり、順番を定義できなくなります。

結果として、ダウンタイムが避けられない状況に陥ることがあります。

循環参照が引き起こす問題の一部を紹介しました。

厄介なのは循環参照は短期的には問題が顕在化しないケースが多くあることです。ある問題だけを解決するには手っ取り早い解決方法に見えることがあるので、つい誘惑に負けてしまいそうになります。しかし「循環参照しているサービスはそれすなわちひとつのサービス」になってしまうので、なんのために苦労してマイクロサービスアーキテクチャを採用しているのかが分からなくなってしまいます。言い換えると循環参照が増えていくと、そのシステムはそう遠くない未来に「マイクロサービスのメリットを潰してデメリットだけ残した悪いところどりのシステム」になってしまいます。

Circular dependencyの検知

循環参照を防ぐにはどうしたら良いでしょうか。「各サービスが循環しないように各エンジニアが気をつける」のももちろん良いのですが、サービスの数が多くなってくるとかなりきつくなります。どのサービスがどのサービスに依存しているのかを人間がすべて正確に把握しつづけるのは難しく、コストも高いため、システムに慣れていてもついうっかり発生させてしまうことがあります。

メルカリShopsではCI(Continuous Integration)によるBuild時に、Circular Dependencyを検知した場合に自動的にFailさせるというアプローチを取っています。もちろん自動的に依存関係を解消してくれるわけではありませんが、暗黙的にマイクロサービス間のCircular Dependencyができてしまうことを防いでいます。

この記事の執筆時点でメルカリShopsは40から50程度のサービスがありますが、循環参照はひとつもなく稼働できています。

Service Dependencies Graph

Circular dependency検知プログラムの副産物として、Service Dependencies GraphもBuildのたびに自動生成されるようになっています。

サービスが多いと依存の線が多くなってやや分かりにくくなっていますが、ざっくりとどれが「多く依存されているサービス」や「上位のサービス」なのかが視覚的に分かりやすくなっています。

こういった図をPlantUMLといったツールで人間がメンテナンスしていくというアプローチも良いとは思いますが、常に追加開発があるシステムの場合、時間が経つとかなりの確率で図が陳腐化していきます。(みな他に優先度の高いタスクに追われ、メンテナンスされなくなってゆくからです)

メルカリShopsは今後も新しいマイクロサービスが増えていくことは確実なので、「自動化されている」ということが非常に重要なポイントとなります。

さらに図だけでは依存関係が正確に分かりづらいため、以下のような依存/被依存を示すテーブルも自動的に生成されるようになっています。

Shop

Depends on Depends on indirectly
auth
business
account
customer
Depended from Depended from indirectly
application
contact
product
shipping
order
payment
report
sale
review

mono-repository

メルカリShopsではMonorepoを採用しています。個人的にはMonorepoはマイクロサービスととても相性が良いと感じます。通常、サービスを分ける場合、Repositoryも分けることが多いですが、規模が大きくなりRepositoryがあまりに多く分かれてくるとそれぞれの連携が難しくなってきます。

一例ではありますが、例えばサービス全体をローカル環境で動かしたい場合、Repositoryが分かれているとそれぞれのRepositoryをCloneして、Buildして…といった作業が必要になってきます。数個のサービスなら手作業でもなんとかなりますが、数十個といった単位となると、動くサービスを構築するだけでも一苦労です。

メルカリShopsではMonorepoの構成にBazelを採用しているので、数多くのマイクロサービスをコマンド一発でBuildすることが可能です。さらにdocker-composeと組み合わせることで、すべてのサービスが稼働するローカル開発環境を一発で立ち上げることも出来ます。

開発環境をストレスなく継続的に構築できることは開発のスピードに大きく貢献するので、マイクロサービスを設計する際はそのあたりも意識してゆくと良いでしょう。

なお、メルカリShopsでのmono-repositoryの運用についてはソウゾウCTOの@suguruが執筆した「メルカリShops の技術スタックと、その選定理由」にてより詳しく説明していますので、興味のある方はチェックしてみてください。

おわりに

いかがだったでしょうか。メルカリShopsでのマイクロサービスとどう向き合っているか、その一部を紹介させていただきました。これからマイクロサービスに挑戦していきたいエンジニアの方たちとって少しでも参考になれば幸いです。

マイクロサービスは難しさもありますが、うまく採用していくことで、拡張性、メンテナンス性をもったサステナビリティなシステムを構築することが可能なアーキテクチャであると思います。

本文でも言及していますが、設計の肝となるところは「いかに影響範囲、関心事を局所化していくか」であると言えるでしょう。時間が経っても気持ちよく開発し続けることが出来るシステムの構築に、ぜひみなさんもチャレンジしてみて頂ければと思います。

なお、メルカリShopsではメンバーを募集中です。メルカリShopsの開発に興味を持ったり、マイクロサービスによる開発にチャレンジしてみたいという方がいれば、ぜひこちらも覗いてみてください。またカジュアルに話だけ聞いてみたい、といった方も大歓迎です。こちらの申し込みフォームよりぜひご連絡ください!

また、2021/08/18から2021/09/28にかけて「ソウゾウ TECH TALK」というイベントが開催されます。テーマを分け、技術的な知見を共有しあうことを目的とした勉強会です。興味のある方はぜひご参加ください!

明日は「Team Topologies in Souzoh – launch phase to growth phase」というタイトルでEngineer Managerの@motokieeさんが登場予定です。どうぞお楽しみに!