Merpay & Mercoin Tech Fest 2023 は、事業との関わりから技術への興味を深め、プロダクトやサービスを支えるEngineeringを知ることができるお祭りで、2023年8月22日(火)からの3日間、開催しました。セッションでは、事業を支える組織・技術・課題などへの試行錯誤やアプローチを紹介していきました。
この記事は、「gRPC Federation を利用した巨大なBFFに対するリアーキテクチャの試み」の書き起こしです。
@goccy:それでは「gRPC Federation を利用した巨大なBFFに対するリアーキテクチャの試み」というタイトルでメルペイ Engineer ProductivityチームのGoshimaが発表します。よろしくお願いします。
初めに自己紹介です。@goccyというアカウントで活動しています。2012年に新卒で株式会社MIXIに入り、ウェブやアプリのフロントエンドからバックエンドまで技術領域を問わずいろいろなものを作ってきました。
2012年や2013年にYAPC::Asiaで自作のPerl処理系に関して登壇したり、5年くらい勤めた後に転職し、ゲーム系のベンチャー企業でテックリードを務め、3年くらい働いた後、2020年にメルペイに入社しました。OSS開発が好きで、最近はGoのOSSをよく書いています。夢は1万スターを達成することで現在5000スターを超えたくらいです。写真は我が家の愛犬です。
今日のアジェンダです。最初にメルペイでのBFFの変遷です。メルペイが今までBFFをどのように扱い、これからどう扱っていきたいかを話します。
また、その中で課題が出てきたので、それを解決するために、弊社で使っているgRPC Federationについても解説します。
最初に、メルペイでのBFFの変遷についてです。
はじめに、BFFの役割とメリット・デメリットについて話します。BFFは「Backend for Frontend」の略です。文字通り、フロントエンド、つまりクライアントに特化したレスポンスを返す専用のサービスです。
BFFのメリットは、クライアントがBFFとのやり取りだけを考えて通信すればいいので、他のバックエンドサービスを意識せずBFFに集中できることです。一方、バックエンドからもクライアントを意識せずにレスポンスを好きに返すことができ、BFF側でレスポンスをいい感じにクライアントに合う形に集約して返すので、バックエンドを楽に実装できるという特徴があります。
一方、多数のチームが一つのBFFを開発する体制になりがちです。そのため、保守しにくい巨大なモノリスになりやすいというデメリットがあります。多数の人が関わることで、責任の所在が曖昧になりやすいことも課題です。
参考記事:Pattern: Backends For Frontends
続いて、メルペイでBFFをどのように扱ってきたかを解説します。
メルペイではAPI Gatewayの直下にBFFとしてMerpay APIというサービスが存在します。Merpay APIは、多数のマイクロサービスの結果を集約して、クライアントに最適化された結果を返します。
GatewayとMerpay APIのようなBFFを一つにして管理している会社はたくさんありますが、Merpay APIが巨大なため、弊社では責務をそれぞれGatewayとMerpay APIで明確に分けるためにサービスとして独立させ、別のチームが責任を持ってメンテナンスしています。
メルペイのリリースから4年以上が経ち、Merpay APIに多数のチームが機能を追加していくうちに、徐々にオーナーシップが不明確になっていくという課題が出てきました。
現在では応急処置として特定のチームがMerpay API全体の責任を持つ状態になっていますが、Merpay APIが巨大すぎて、保守するコストが無視できないレベルになってきました。
この図のように、Merpay APIに対して多くのチームが機能開発をしていますが、メンテナンス・運用するチームが少数のため、負担が大きくなっています。
これを解決するために、今年「Merpay APIリアーキテクチャプロジェクト」がスタートしました。
Merpay APIが持つ全てのAPIに対して、ひとつずつAPIに責任を持つチームを明確にし、Merpay APIを複数のBFFに分割することで管理するプロジェクトです。
Before(図の左側)のように、Merpay APIに対していろいろなチームが開発している体制から、Merpay APIのAPI一つ一つに、どのチームの持ち物なのかを明確にし、チームの持ち物に対して新しくBFFを切り、チームとBFFを一対一で対応させ、責任を明確化する構成を作ります。
このプロジェクトによって、複数のBFFを作ることになりました。これによってチームとBFFの対応関係が明確になるので、対応チームの「BFFの開発・保守に割くコストを低くしたい」という要求がどんどん高まることが考えられます。
一方で、BFFは責務がシンプルなため、多くが定型作業になると予想されます。そのため、自動化などの恩恵を受けやすいことを踏まえ、BFFを低コストで開発運用する何らかの仕組みが求められていると考えています。例えば、GraphQL FederationのようなFederated Architectureを使う、定型作業の自動化を進める、などの方法が考えられます。
そこで、GraphQL Federation(Apollo Federation)を検討しました。これはGraphQLを用いたFederated Architectureを構築する仕組みで、近年いろいろな会社で採用されています。
導入を検討したものの、今までgRPCだけで巨大なサービスを運用しており、新しくGraphQLを導入するコストが高すぎるという課題がありました。
各マイクロサービスに対応する形でGraphQLサーバーを導入する際のコストが高い、バックエンド開発者全員にGraphQLの知識を新しくインストールする必要がある、GraphQLに対する運用監視の知識が新しく必要になるなどの課題があり、結果的に採用を見送ることにしました。
では、どうするのか。Federated Architectureを構築するためにgRPCを使う方法がないなら、作ればいいんです。そこでgRPCを用いたFederated Architectureを構築する仕組みを開発しています。それをgRPC Federationと呼んでいます。
その仕組みを使って、gRPCサーバーであるBFFを自動生成します。BFFは今まで通りgRPCプロトコルを用いて、配下のサービスと通信します。
それでは、gRPC Federationについて説明します。
gRPC Federationは、gRPCを用いたFederated Architectureを構築する仕組みです。Protocol Buffersのオプションで振る舞いを記述します。結果として、自動生成によってgRPCサーバーを作ることができます。
ものによってはProtocol Buffers上だけでは表現できない複雑なロジックを持つ場合もありますが、その部分についてはGoで書くことが可能です。Protocol BuffersとGoのハイブリッドで記述するイメージです。
BFFのように自身でデータを持たず、マイクロサービスの呼び出し結果を集約して返すサービスで有効ではないかと考えています。
最初にgRPC Federationを作るにあたっての設計思想について簡単に触れます。
Protocol Buffers上のレスポンスに注目して開発しました。レスポンスに相当するmessageを取得するために、gRPCメソッドを呼び出すと考えます。普通はgRPCメソッドを呼んだ結果がレスポンスになるので逆になるのですが、考え方としてmessageに注目したかったので、レスポンスに着目しています。
もう一つは、全てのmessageにはそれを取得するためのgRPCメソッドが必ず存在すると考えることです。message と gRPCメソッドは、必ず1対1に紐づけ可能であるという前提を置いて作っています。
gRPC Federationで「どんな体験を提供したいか」も最初に考えています。
一つは「BFFを構築する上で必要な、典型作業を自動化したい」ということ。具体的にはBFFと依存先のサービス間の大量の型変換を自動化したり、サービスの依存関係をProtocol Buffersの解析だけで把握できるようにしたいと考えています。gRPC Federationが想定している使い方をしてもらえれば、メソッド単位の依存関係もわかるようになると考えています。
「BFFと依存先サービス間の型変換を自動化したい」というモチベーションについて説明します。まず、BFFを構築する上で、依存先のサービスにあるmessageを返したいときに同じmessageをBFF側にも作る必要があります。
これをしないと、BFFの呼び出し元が依存先のサービスを常に意識する必要があるため、設計としてBFFを置いている意味がなくなってしまいます。
ただ、このような設計にすることで、BFF側の立場としてはパッケージを跨いだ同じ型の変換作業が大量に発生してしまいます。gRPC Federationでは、これらの対応関係を記述することで、変換処理を自動生成したいと考えています。
もう一つ、サービスの依存関係をProtocol Buffersの解析だけで把握できるようにするモチベーションについて説明します。
マイクロサービスアーキテクチャにおいて、サービス間の依存関係を把握できるといろいろなメリットがあります。メソッド単位での依存関係がわかることで、パフォーマンスの理論値を算出したり、リファクタリングの影響範囲を把握したりと、いろいろなことができるようになります。
gRPC Federationが提供する専用のGoのライブラリや、Protocol Buffersのリフレクション機能などを利用することで、依存関係を機械的に取得できるようになり、いろいろな用途に使えると考えています。
簡単にgRPC Federationに付属しているツールの一部を紹介します。
Protocol Buffersのプラグインとして動作させるように、protoc-gen-grpc-federationというツールがあり、protoc-gen-goやprotoc-gen-go-grpcといったプラグインと組み合わせて使うことで、gRPCのBFFサーバーを自動生成します。
linterはスタンドアローンのツールで、Protocol Buffersのコンパイルをした上で、gRPC Federationの記述ミスを指摘してくれるものです。静的解析ではなくコンパイルするので正確に解析できます。
他にもlanguage-serverを最初から用意しており、gRPC Federationのオプション記述を支援してくれるので、linterによるエラーや補完がエディタ上ですぐに利用できます。
続いてgRPC Federationの具体的な使い方を説明します。
Post ServiceとUser Service、それからBFFとして作るFederation Serviceという三つのサービスを使って説明します。
図のように、Federation Serviceという今回作るサービスにpost idが送られたら、それをPost Serviceに渡すことによってPost messageを取得し、Post messageに存在しているuser_idというフィールドの値をもとに、それを使ってUser Serviceに問い合わせし、User messageを取得します。postとuserという二つのmessageを合成して、postの中にuserが含まれるmessageにした上で返すサービスを考えていきます。
続いて、各サービスのProtocol Buffersの定義についてです。
一番左側がFederation Serviceに相当するProtocol Buffers内容です。GetPostというメソッドがあり、中身には Post messageが入ります。Post messageには User messageが直接入る構成です。
右側二つはPost ServiceとUser Serviceの内容です。それぞれGetPost / GetUser と自身が管理する message を返すメソッドを一つずつ持ちます。Post message にはuser_idというフィールドがあります。これらを使って説明していきます。
gRPC FederationのProtocol Buffersのオプションについて簡単に説明します。Protocol Buffersのsyntaxのservice、message、fieldそれぞれに合わせてオプションを用意しています。
serviceに対してはgrpc.federation.serviceが、messageに対してはgrpc.federation.messageが存在します。それぞれについて説明します。
まず、grpc.federation.serviceというオプションです。これはgRPC Federationの自動生成対象となるサービスを指定するために利用します。機能として、dependenciesというセクションがあり、どのサービスに依存しているかを定義できます。
この例であれば、Federation Serviceにgrpc.federation.serviceというオプションをつけると、dependencieを用いてPost ServiceとUser Serviceに対して依存関係があることを定義できます。
grpc.federation.messageオプションは、一番重要なオプションです。大きくresolverとmessagesにわかれています。message自身の各フィールドに割り当てる値自体を取得するための定義を書くオプションです。
resolverはgRPCメソッドを呼び出すための定義です。この定義によってmessageとメソッドが一対一で紐づきます。resolverだけでは自身のmessageに割り当てる値を取得しきれないことがあります。そのときにmessagesを用いて他の message への依存関係を定義できます。 messages は複数指定することが可能です。
この二つを組み合わせることで、必要な値を手に入れます。
最初に、resolverから見ていきます。左側がFederation Serviceの定義で、右側がPost ServiceのProtocol Buffersの定義です。赤枠で囲われたresolverというセクションについて話します。
最初に、resolverはgRPCメソッドを定義します。methodというフィールドに対して呼び出すメソッドの名前をFQDNで指定します。今回であればPost messageを作るためにGetPostというPost Serviceのメソッドを呼び出したいので、この形で指定します。右側のPostServiceの赤くハイライトした部分が対応している部分です。
続いて、メソッドを呼び出すためには、リクエスト時の値を指定する必要があります。赤枠のrequestという箇所でそれを指定しています。右側のPost ServiceのGetPostRequestというmessageに対応しており、GetPostRequestの中身を埋めていく作業になります。fieldにフィールド名を書いて、byでpost_idフィールドに対する値を指定します。
最後に、呼び出した結果のうち、どの値をどのような名前でgRPC Federation中で参照していくかを定義するために、response を定義します。これはPostService側のGetPostReplyというmessageの内容と対応しています。
次にそれぞれのフィールドについて説明します。nameで、取得したレスポンスをどういう名前で参照するかを定義します。ここでresという名前をつけているので、この名前でGetPostReplyのpostフィールドの値を参照するという意味になります。
次にfieldです。そのレスポンスのうちにどのフィールドを採用するかを指定します。ここではpostというフィールドを指定しているので、GetPostReplyの中のpostフィールドの値だけを使うという意味です。
最後に、「autobind: true」について説明します。
レスポンスの各フィールドと同じ名前・同じ型のフィールドがBFF側にも存在するならば、フィールドのバインディングをできるだけ省略した方がいいという考え方のもと、右側のPostService側のPost messageにあるid・title・contentというフィールドと同じ名前・同じ型のフィールドが左側のFederationService側にも存在するので、それらの値を自動的にバインドするという機能です。これによって option の記述を大幅に省略することができます。
次にmessagesのセクションについて説明します。Post messageを作る上でresolverだけでは足りません。肝心のuserの値はまだ取得できていない状況です。
最初にmessagesの中で、nameに着目します。これは依存するmessage、今回であればFederation Service側のUser messageに依存したいので、User messageに対して、取得したときにどういった名前で扱うかを指定します。
ここではuという名前を付けているので、この名前でUser messageを参照していきます。
次に、参照するmessage自体を書かないといけないので、messageにUserと書くと、右側の赤枠で囲われた User messageを指すという意味になります。
次に、一番重要なargsについてです。右側のUser messageを見てください。こちらにもgRPC Federationのオプションが書かれていて、中ではGetUserメソッドを呼び出し、userを取得して自身のフィールドにバインドするということが書かれています。
GetUserメソッドを呼び出すためにuser_idが必要です。この値を User message に渡すことを考えなければなりません。これを実現するためにargsを利用し、依存messageを取得する際に必要になるパラメータを指定します。このパラメータを、gRPC Federationでは「メッセージ引数」と呼んでいます。
argsの中にはnameがあります。ここでは、messageに対する名前を指定することができ、依存先のmessage、この場合User message側でこの名前に「$.」というプレフィックスをつけることで参照できます。
では、引数の値はどうやって指定するのでしょうか。これは、byで指定できます。byで指定する値自体はどうやって受け取るのかというと、resolverでレスポンスに対してつけたresという名前を参照し、レスポンスのuser_idフィールドを参照することを示すためにres.user_idと書くと指定できます。これにより、User message側にGetPostメソッドのレスポンスにあるuser_idの値が渡ります。
最後に、grpc.federation.fieldというオプションについて説明します。messageオプションによってフィールドのバインディングに必要な情報が集まりました。
最後にfiledオプションで定義した名前や、自身のmessageに対するmessage引数などを参照しながら値をフィールドに紐付けます。Protocol Buffersの定義にあるように、messagesで書かれているUser messageに対して「u」という名前をつけていますが、この名前を使ってgrpc.federation.field のbyという機能を使って4番目のuserフィールドにUser messageの値を紐付けています。
最後にレスポンスにオプションを追加して完成です。最初に設計思想でレスポンスのmessageに着目したいという話をしました。
今まで作ってきたPost messageを作っただけだと、まだgRPCメソッドであるGetPostの全実装を完成できていません。
レスポンスのmessageであるGetPostReplyに対してオプションを追加し、レスポンスの実装が完成していることを示すことが必要です。
特殊な仕様として、レスポンスのメッセージ引数に相当するものは、gRPCメソッドのリクエスト側のmessageの各フィールドになるというものがあります。GetPostReplyというmessageを作るためにオプションを書いていきますが、grpc.federation.messageというオプションの中で、リクエストに対応するGetPostRequest messageの各フィールドを参照できるので、$.id でリクエストの内容を参照できます。
その結果取得した Post messageに対してpという名前をつけているので、pをgrpc.federation.fieldというオプションで参照して、自身の1番目のpostフィールドに紐付けて完成になります。
他にもいろいろな機能が実装されています。他のパッケージに定義されている messageを参照することでgRPC Federationの資産を再利用したり、複雑なロジックを定義したい場合は、messageやfieldオプションの中で「custom_resolver = true」と記述すると、その部分だけGoで実装することができます。
他にもgrpc.federation.methodというオプションでメソッドレベルの制御ができるようになり、例えばタイムアウトを設定できます。また、oneofに対しては専用のgrpc.federation.oneofというオプションを用意し、oneof内で条件分岐を定義できるようにすることも考えています。
実はgRPC FederationはOSSとして公開していまして、grpc-federationというリポジトリで、誰でも利用することができるようになっています。
現在アルファバージョンですが、今年中に社内の本番環境で活用できるように改善を続けている状態で、随時更新しています。
Federated Architectureを構築する上で、GraphQLに代わる一つの解として gRPC Federation を選択できるように頑張っていこうと考えています。
現時点ではプルリクエストは受け付けておりませんが、機能要望や、改善案、使用感などのフィードバックはウェルカムですので、issueやTwitterのコメントで反応してくれると嬉しいです。
grpc-federation:https://github.com/mercari/grpc-federation
最後に、発表のまとめです。マイクロサービスアーキテクチャにおけるBFFの重要性とメリットデメリットについて触れました。
メルペイでは、巨大なBFFのオーナーシップ問題を解決するために、いくつかのBFFに分割することを考えています。各BFFの開発を効率的に行うためにgRPC Federationという仕組みを作っています。gRPC Federationを使ったシンプルなBFFの構築例を示しました。こちらはOSSになっており、誰でも使えるようになっているため、ぜひ使っていただいてフィードバックをお待ちしています。
それでは本発表を終わります。ご清聴ありがとうございました。