はじめに
こんにちは、計測プラットフォーム部バックエンドチームの高木(@TAKAyuki_atkwsk)です。ZOZOMATシステムではAPIのリバースプロキシとしてEnvoyおよび付随するgRPC-JSON transcoderを導入しています。これらによって依存するサービスにレガシーなサーバーが存在していても部分的にgRPCを採用しモダンなアーキテクチャを広めようとしていることについて紹介します。
なお、本記事は以下記事のシリーズとなっておりますので合わせてご覧いただけるとZOZOSUITやZOZOMATなど計測プラットフォーム部の取り組みについてより深くご理解頂けるかと思います。
ZOZOMATシステムとEnvoyについて
ZOZOMATシステム構成におけるEnvoyの役割について簡単に説明しておきます。クライアント(ZOZOTOWNアプリ等)とZOZOMAT APIサーバー(以下ZOZOMATサーバー)の間にリバースプロキシとしてEnvoyを配置し主に以下の役割を担っています。
- TLSの終端
- ルーティング
- gRPCリクエストの負荷分散
- HTTP JSON APIとgRPCの相互変換
gRPCをシステムに導入する際、負荷分散の観点でZOZOMATサーバーの前段にEnvoyを置く構成を想定していました。以下の記事がZOZOMATシステムの構成として似ているものだったので参考にさせていただきました。
- Envoy プロキシを使用して GKE 上で gRPC サービスの負荷分散を行う | ソリューション | Google Cloud
- Amazon EKSでgRPCサーバを運用する - 一休.com Developers Blog
先ほど挙げたEnvoyが担当する役割の中の「HTTP JSON APIとgRPCの相互変換」機能、これがgRPC-JSON transcoderなのですが詳しくは記事の後半で触れていきます。ちなみにリバースプロキシとしてはnginxも候補として有力ではありますが、上記の事例に後押しされてEnvoyを使うことにしたという経緯がありました。
また、ZOZOMATのシステム構成についてはこちらの記事で詳しく説明されていますので合わせてご覧ください。
ZOZOSUITを振り返って
ZOZOMATでの取り組みを紹介する前にZOZOSUITでの課題について触れておきます。ZOZOSUITはZOZOMAT以前に発表された体のサイズを測るツールおよびシステムで、このシステムはZOZOTOWNアプリ、ZOZOTOWNサーバーと連携します。
ZOZOSUITで扱うデータの中に3Dデータというものがあります。これは、計測時にZOZOTOWNアプリで生成されてZOZOSUITサーバーに送られます。また計測結果を参照する際にもZOZOSUITサーバーからZOZOTOWNサーバーを経由し最終的にZOZOTOWNアプリに送られます。
この3Dデータはサイズが大きいためデータ送信で少なからず時間がかかってしまうことが課題でした。
また、データのシリアライズ方法に関してはZOZOTOWNアプリとZOZOSUITサーバー間ではmsgpack、ZOZOTOWNサーバーとZOZOSUITサーバー間ではJSONを利用していました。このため、サーバー側の開発者はmsgpackで扱うための実装やJSONで扱うための実装をエンドポイントごとに意識する必要がありました。
以下の図では各コンポーネントとプロトコル・シリアライズ方法の関連を示しています。
ZOZOMATでの解決策
ZOZOMATでもZOZOSUIT同様の3Dデータを利用するため、前述の問題を解決しておく必要がありました。そこで、ZOZOMATではgRPC + protocol buffersを利用することにしました。
大きなサイズのデータ送信に関して検証するためシリアライズ方式によるデータサイズの比較を簡単に行いました。今回は3Dデータを表す以下のようなデータを利用し、それぞれの方式でシリアライズを行いました。
{ "faces": [ [1444,1453,1435], [1444,1435,1416], ... ], "verticies": [ [0.19088252916100196,-0.025855744239442646,0.0743707061820768], [0.1902995247773912,-0.029133848767236424,0.07462623400745416], ... ], "rings": [ [ [0.06820038723992675,0.01848489483093979,-0.37634277191524135], [0.06906485745721565,0.02409594156229787,-0.3780734263629579], ... ], ... ] }
結果は以下の通りで、JSONと比較してmsgpackとprotocol buffersはそれぞれ約半分のサイズになることが分かりました。
方式 | サイズ(bytes) |
---|---|
JSON | 431,413 |
msgpack | 210,388 |
protocol buffers | 211,895 |
msgpackとprotocol buffersではサイズに大きな違いは見られませんでした。ただ、gRPCを利用することでHTTP/2のヘッダー圧縮など効率的な通信が見込まれるため全体としてはデータ送信について更なる改善が期待できるという判断になりました。
また、protocol buffersのメッセージとサービスを定義するprotoファイルをバックエンドおよびアプリ開発者の間で共有しAPI定義の連携漏れを防ぐことができる利点もありました。さらに、protocコマンドによってprotoファイルからバックエンド・iOS/Androidアプリ開発用言語に対応したインタフェースのソースコードが自動生成されます。そのためスキーマ変更によるメンテナンスコストも低くなった印象でした。
gRPC導入にあたっての課題
gRPCを導入してめでたしめでたし。とはいかないもので、ZOZOTOWNアプリではgRPC実装ができる見込みだったのですが連携するZOZOTOWNサーバーではgRPCの実装が厳しい見込みでした。ZOZOTOWNサーバーは現在VBScriptで構成されるレガシーシステムであり、リプレイスを進めているためです。結果、ZOZOTOWNサーバーとZOZOMATサーバーの通信方式については課題が残った状態でした。
しかし、これらの検討を行いながら調査していた時にEnvoyの機能の1つにgRPC-JSON transcoderというものがあることに気がつきました。これは、HTTP JSON APIクライアントがEnvoyにリクエストを送るとgRPCサービスにプロキシしてくれるものです。
ZOZOTOWNアプリからはgRPCで、ZOZOTOWNサーバーからはJSON APIでリクエストされます。途中にあるEnvoyでJSONのリクエストをgRPCに変換してくれるのでZOZOMATサーバーはリクエストをgRPCで統一して扱うことができるようになります。以下の図は各コンポーネントとそれぞれの間のリクエスト形式の関係になります。
似たようなアプローチのツールの1つにgrpc-gatewayがあります。こちらもHTTP JSON APIリクエストをgRPCに変換して背後にあるgRPCサーバーへプロキシするものです。以下の図にgrpc-gatewayを利用する構成の例を示します。gRPCとHTTP JSON APIでリクエストの経路を分離する場合やEnvoyではないコンポーネントを使う場合は良いかもしれません。一方ZOZOMATシステムの場合、両者とも同じ経路となるためEnvoyを利用、かつgRPC-JSON transcoderを有効にした構成となっています。
gRPC-JSON transcoderの利用方法
HTTP JSON互換にするためにはprotoファイルに設定を追加する必要があります。ここではZOZOMATで足のサイズを測ると見れるようになるシューズの相性度1を取得するAPIを例として紹介します。以下にシューズの相性度を取得する疑似rpcを定義したprotoファイルを示します。
// シューズの相性度を取得するrpcのサンプル定義 syntax = "proto3"; package shoes; // importする必要がある import "google/api/annotations.proto"; service Shoes { rpc ListMatchingRates (ListMatchingRatesRequest) returns (MatchingRates) { // HTTP JSONでやり取りしたい場合はここを追加する option (google.api.http) = { get: "/shoes/{item_id}/sessions/{session_id}/matching-rates" }; } } message ListMatchingRatesRequest { string item_id = 1; string session_id = 2; } message MatchingRates { string item_id = 1; string session_id = 2; repeated MatchingRate matching_rates = 3; } message MatchingRate { string size_label = 1; float rate = 2; }
ここでのポイントはrpcに対して option (google.api.http)
を追加してHTTP JSON APIエンドポイントの定義を行うことです。上記の例ですと https://[domain]/shoes/123abc/sessions/abc456/matching-rates というエンドポイントに対してGETでリクエストできるようになります。gRPC-JSON transcoderを経由したレスポンスは以下のように MatchingRates
のデータ構造がJSON化されたものになります。
{ "itemId": "123abc", "sessionId": "abc456", "matchingRates": [ { "sizeLabel": "25", "rate": 0.9543 }, { "sizeLabel": "24.5", "rate": 0.8561 } ] }
これらの設定をgRPC-JSON transcoderに渡すためには、protocでdescriptor setというものを生成しEnvoyの設定で参照します。詳しくはこちらのドキュメントを参考にしてください。
gRPC-JSON transcoderを採用しての振り返り
採用してみて良かったポイントを挙げてみます。
- gRPCを使って通信する前提で設計や実装が可能になった
- ZOZOTOWNサーバーのようなgRPC未対応のクライアントからでもHTTP JSON APIでアクセス可能になった
HTTP JSON API用のエンドポイントをZOZOMATサーバー側に実装する方法ではリクエストボディを処理で扱うデータ型に変換する部分がgRPCとJSONの2種類考える必要があります。一方、リクエストがgRPCのみになるとその部分については考えなくてよくなるため実装がよりシンプルになります。
さらに、HTTP JSON APIにおいてもprotocol buffersによってインタフェースの定義を管理できる恩恵を受けられます。
また、将来的にZOZOTOWNサーバーがgRPC対応するという場合を考えると、gRPC-JSON transcoderの設定やprotoファイル上から google.api.http
オプションを削除する必要がありますが、ZOZOMATサーバーの実装およびEnvoyの設定は何一つ変える必要がないため移行もスムーズになると考えられます。
一方、注意すべきポイントとして int64
で扱う値はJSONのレスポンスで数値を表す文字列の値に変換される点です。こちらのドキュメントにprotocol buffersとJSONの対応表が載っており、JSONのレスポンスはこれに沿う形で出力されます。2
さいごに
今回はEnvoyのgRPC-JSON transcoderを利用して部分的にHTTP JSON APIを生かしながらgRPCでコンポーネント間通信を実現することを紹介しました。個人的にはEnvoyを導入するのは初めてだったので、今のところZOZOMATシステムの構成でうまく機能していてホッとしています。何らかの制約により一部のコンポーネントでgRPCが導入できない場合でもこのような相互変換する層があると全体としてうまくいくというのは、gRPCに限らず他の技術においても応用できそうだなと思います。
計測プラットフォーム部バックエンドチームでは、ZOZOMATでより精度の高いサイズを推奨するバックエンドエンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください!
-
相性度とは、サイズ感に満足できる確率です。足のサイズが同じ人でも、シューズを履いた場合に、どのサイズがピッタリと感じるかは、シューズの形、足型、締め付け感や、ゆとりの好み等により異なります。そこでZOZOではZOZOMATの計測データ、シューズデータ、お客様にお答えいただいたアンケートデータなどを元に、お客様がそのシューズを履いた場合に、サイズ感に満足いただける確率を「相性度」として、サイズ別に表示しています。(ZOZOTOWN >【ZOZOMAT】相性度の高いシューズ ページの説明より引用)↩
-
When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the proto to JSON conversion must follow the proto3 specification. (https://github.com/googleapis/googleapis/blob/master/google/api/http.proto#L288-L290 より引用)↩