組織的な観点でスケーラブルなコードにする

森國泰平氏(以下、森國):こんにちは。私はメルカリでマイクロサービスの開発をしている、森國と申します。今日は私が担当しているListing Serviceというサービスについて、どのようにマイクロサービス化を進めているかをお話いたします。

まず最初に、Listing Serviceについて簡単にご紹介します。

Listing Serviceは、メルカリのなかでも出品機能を担当するマイクロサービスとなっています。現在メルカリが進めているPHPのモノリスからGoのマイクロサービスへ移行する、最初のプロジェクトとなっています。

現在、Listing Serviceでは、1つのHTTPエンドポイントを対象として、マイクロサービス化を進めています。Listing Serviceが提供するのはHTTPのエンドポイントなんですが、Listing Serviceが他のサービスと通信する際には、gRPCを使用しています。

次に、メルカリのマイクロサービス化の進め方の戦略について、2つのポイントをお話します。

1つ目が、APIの互換性を保ちながらサービスを分割していくということです。APIの互換性を保つことによってカナリアリリース、つまりは段階的にPHPからGoのコードに移行していくことができます。

カナリアリリースをすることによって、万が一バグなどが最初に見つかった場合にも、PHPのコードに戻すこともできますし、そもそも影響範囲を少なくすることができるので、お客さまに対する負担というものも低下することができます。

2つ目の点として、チームとして独立した開発を可能にするということがあります。マイクロサービス化の目的は、ただコードを分割するということではなくて、長期的にスケール可能な組織を作り上げるということです。

そのため、Listing Serviceの開発を進めるなかでも、ただただ目の前の問題にとらわれすぎるのではなく、長期的な組織の在り方などを意識しながら、開発を進めていく必要があります。

3つのテストによって保たれる互換性、コードの再配置や移行について

これに対して、Listing Serviceがどのように実際に開発を進めているのかを紹介します。

まず、APIの互換性を保つということについては、3つのテストによって、互換性が保たれていることを保証しようとしています。

1つ目が、既存のAPIに対するアプリ、要するにiOSやAndroidの自動テストがあります。これをListing Serviceにもそのまま適用することによって、クライアントからの動作が変わっていないことを確認します。

2つ目のテストとして、QAチームによる手動の確認を行います。1つ目のアプリの自動テストだけではカバーしきれない範囲や細かいチェックなどを、QAチームの人の手によって行います。

3つ目は、AQAチームによるインテグレーションテストツールの開発と、それを使ったAPIの自動テストの構築を計画しています。

2つ目のチームとして独立した開発を可能にするという点については、そもそもListing Serviceで扱いづらいものは、出品に関するロジックだけなので、現在のエンドポイントに含まれているすべてのコードをListing Serviceに持ってくるというわけではなく、別のサービスに配置しなおしたりしています。

あるいは、そのままPHPのコードに残しておいて、Listing ServiceからWebhook経由でコードを実行するようなことにしています。そして、このPHPに残ったコードについては、適切なマイクロサービスが誕生した時に、コードの移行を考えています。

DB共有に関する安全面の課題

次に、Listing Serviceの開発を進めるにあたって直面した、4つの点について紹介します。

1つがGCPからさくらのMySQLへの接続、2つ目がトランザクションの分割、3つ目が機能追加・修正への追従、4つ目がPHPの例外との互換性です。それぞれについて、少し詳しく話していきます。

1つ目のGCPからさくらのMySQLへの接続についてです。

まず前提として、マイクロサービスというのは、ぞれぞれのサービスが独立したデータベースを持つべきだとは思います。ただし、今回のメルカリのようにモノリスからマイクロサービスに移行するという過程においては、どうしても一時的にデータベースの共有をしなければならない場合もあると思います。

メルカリのマイクロサービスは、基本的にはGCPの上で動いているんですが、既存のメルカリのPHPのコードであったりMySQLというのは、さくらのクラウド上で動いています。このGCPとさくらのMySQL間の接続をどうするかということが、課題になりました。

もちろんMySQLのポートを外部のインターネットに公開してしまえば、GCPから接続することは可能なんですが、それはセキュリティなどの要件的にあまりやりたくはないということで、課題となりました。

そこで我々がどのように進めているかというと、MySQLをgRPC経由で触るためのSQL on gRPCのようなサービスと、それを使うためのGoのクライアントライブラリを開発しています。gRPCを使ってMySQLを触ることによって、既存のgRPC上の認証・認可の仕組みに乗ることができるので、セキュアに通信を行うことができます。

この作成しているサービスでは、基本的にはCRUDの基本的な操作のみを提供し、JoinやTransactionといった複雑な処理は対応していません。実際にコードなどを見ていきます。

まず、こちらにはSELECT文に対応するProtocol Buffersのリクエストとレスポンスを載せています。

リクエストにはSQLで指定するカラム名だったりテーブル名、あるいは、WHERE句などが指定できます。

これをサービスに投げることによって、レスポンスにRowの配列が返ってきます。

このRowというのは、データベースの1行に相当する、カラム名とその値の配列のようなものとなっています。そして、このメッセージをうまく扱うために、Goのライブラリを開発しました。

Goのライブラリは、メソッドチェーンを使って、SQLのようにプロトバフのリクエストを組み立てることができます。

そして、このクライアントのもう1つの機能として、レスポンスのProtocol BuffersをGoのstructにマッピングするための、O/Rマッパーのような機能も備えています。これを使うことによって、Protocol Buffersというものをあまり意識せずに、SQLに近いかたちでMySQLを扱うことができるようになっています。

分割を前提に開発を進めていく

次に、トランザクションの分割についてお話します。Listing Serviceでは、基本的には出品に関する商品のデータの更新をメインとしたいんですけども、既存のAPIのコードには商品だけでなく、例えば配送情報だったり、分析用のデータなどが1つのトランザクションで更新されていました。

最終的にマイクロサービス化が進んでいくと、それぞれのサービスが独立したデータベースを持つことになるので、もちろんトランザクションは使えなくなります。そのため、この段階からトランザクションの分割っていうものを意識しながら、開発を進めていく必要があります。

そこで我々がどうしたかというと、まずどうしても商品に関連するようなデータについては、トランザクションが必要だと判断しました。そのため、さくら上にItem Serviceというサービスを作って、このItem Serviceはトランザクションを扱うようにしました。

それ以外についても、お客さまの目に直接触れるようなデータについては、できるだけリクエスト内で同期的に処理をするようにしています。それ以外の内部データなどについては、Cloud PubSubを使った非同期の結果整合性を受け入れるようにしています。

具体的に見ていくと、例えば、出品に関する機能があった時に、そのなかでトランザクションを貼って、例えば商品や画像、配送情報、分析用データなどが更新されていたとします。

その時に、商品についてはトランザクションが必要なので、Item Serviceが担保する。画像についてはお客さまの目に直接触れるものなので、トランザクションからは出すものの、できるだけ同期的に更新をする。

配送情報などについては、商品が売れるまでは配送されることもないので、例えば非同期の結果整合性を受け入れるようなかたちにする。そして、内部データについても、お客さまには意識してもらう必要はないので、非同期のデータ整合性を受け入れる、というふうに進めていきました。

機能追加・修正に対する追従

次に、機能追加・修正への追従についてです。

Listing Serviceは出品を扱うのですが、出品はメルカリのなかでもコアな機能の1つとなっているので、このマイクロサービス化を進める過程においても、機能の追加だったり修正が行われる場合があります。新しいマイクロサービスではAPIの互換性を保とうとしているため、この機能の追加や修正に対しても追従していく必要があります。

そこで我々がどうしたかというと、もとのPHPのリポジトリ上に変更追跡用のブランチというものを作成しました。そして、Goのコードに移行したコードについては、そのブランチ上から削除しています。

これによって、例えばPHPのmasterブランチに新しい変更が加えられた場合に、その変更追跡用のブランチにマージした際にコンフリクトが発生するので、新しい変更が行われたことに気付くことができます。そして、この変更追跡用ブランチからすべてのコードを削除することができれば、マイクロサービス化のコードの移行が完了したと判断することができます。具体的に見ていきます。

例えば、この左側にあるPHPのコードを、右にあるGoのコードに移行していくようなことを考えます。例えば、最初にif文のところをGoのコードに移したとします。

すると、左のPHPのブランチからは、対応するコードを削除します。

同様にreturn文についても、例えばGoに移したとすると、PHPからは削除します。

そして、この左の状態のPHPのように、すべてのコードが削除された状態になれば、Goへの移行が完了したと判断することができます。

PHPの例外との互換性

例えば、機能の追加や修正が行われたことを考えてみます。

例えば、新しくPHPでif文に変更があった場合を考えると、これはmasterブランチをこのブランチに対してマージしてくる時に、コンフリクトが発生するので新しい変更に気付くことができて、それをGoのコードに反映することができます。

あるいは、完全に新しい機能が追加された場合を考えると、これはもしかしたらコンフリクトしないかもしれないんですけども、最後に移行が終わったかどうかを確認する際に、対応する箇所だけがPHPのコードに現れることになるので、その部分を新しくGoに移しなおせば、互換性を保つことができます。

最後に、PHPの例外との互換性についてお話します。

これはたぶんメルカリ特有の課題にあたる部分だとは思うんですけども、既存のメルカリのAPIでは、レスポンスの一部にエラーコードとしてPHPの例外名が含まれている場合がありました。

さらに、この例外名というのをクライアントがハンドリングしている部分があったため、互換性を保つGoのAPIからもPHPの例外名を返さなくてはならない、という状況にありました。

さらには、この例外情報というのは、例外の種類によってレスポンスのJSONのかたちが一部異なっていたり、あるいはマイクロサービス化によって、この例外が発生する位置というもの自体が複数のサービスに分割されていくようなことになったため、各サービスがこの例外情報っていうのを意識する必要が出てきました。

そこで我々がどうしたかというと、このPHPの例外情報を扱うためのGoの共有ライブラリを開発しました。

このライブラリを使うことによって、例えばサービス内でエラーが発生した際に、それに対応するPHPの例外情報をgRPCのStatus.detailsっていうところに付与して、レスポンスを返すことができます。

そして、PHP互換のHTTPエンドポイントでは、このgRPCのレスポンスに含まれる例外情報から、直接メルカリのPHP互換のエラー情報のレスポンスを生成することができます。具体的にコードで見ていきます。

これがgRPCサーバーのコードの例です。

例えばエラーが発生した際に、通常のgRPCのエラーコードであるNotFoundに加えて、ここではPHPのNotFoundExceptionのようなものを一緒にメッセージにくっ付けて、エラーのレスポンスを返すことができます。

そして、PHPと互換性を保つHTTPエンドポイントでは、この後ろのサービスから返されてきたエラーからExceptionの情報を取り出して、レスポンスのJSONを直接生成することができます。

これによって、後ろのサービスで発生した例外情報というのを、等価的にクライアントに返すことができるので、各サービスは自分のサービスのなかで発生についてだけ意識すればよくなりました。

以上、マイクロサービス化を進めるにあたって出てきた4つの課題について、ご紹介いたしました。最後にまとめです。

正解がない中で正しさを求める楽しさ

Listing Serviceでは、長期的な組織の課題を解決できるように、かつ、短期的な負債を生んでしまわないようにすることを意識しながら、マイクロサービス化を進めています。

Listing Serviceはメルカリのマイクロサービス化の最初のプロジェクトであるため、資産だったり知識、経験などがそろっていなくて、なかなか大変な部分が多いところはあります。

ただし、正解がない中で、「自分が信じる設計を信じろ!」という精神で、ひたすらただしい姿を目指して全員で開発を進めていくのは、なかなか楽しいものがあるなと思いながら、開発を進めています。以上で、Listing Serviceについての発表を終わります。ありがとうございました。

(会場拍手)