Slide 1

Slide 1 text

Session Title gRPC Federation を利用した 巨大なBFFに対する リアーキテクチャの試み Masaaki Goshima Engineering Productivity

Slide 2

Slide 2 text

Masaaki Goshima / @goccy 2012年に株式会社MIXIに入社。ウェブやアプリの フロントエンドからバックエンドまで 技術領域問わずいろいろなものを作る。 2012、2013 年に YAPC::Asia で自作のPerl処理 系に関して登壇。その後ゲーム系のベンチャー企業 でテックリードを務め、2020年メルペイに入社。OSS 開発が好きで最近は Go の OSS をよく書いてい る。夢は 10k stars。 株式会社メルペイ Engineering Productivity

Slide 3

Slide 3 text

メルペイでのBFFの変遷 01 gRPC Federation 02 Agenda

Slide 4

Slide 4 text

メルペイでの BFF の変遷

Slide 5

Slide 5 text

BFFの役割とメリット・デメリット ● Backend For Frontend の略 ● クライアントに特化したレスポンスを返す専用サービス ● クライアントは BFF との やりとりだけ考えれば良い ● バックエンドはクライアントを意 識せずにレスポンスを 返して良い ● 多数のチームがひとつの BFF を 開発するため、保守しにくい巨大 なモノリスになりやすい ● 多数のチームが関わるため、責任 が曖昧になりやすい メリット デメリット SamNewman&Associates.「Pattern: Backends For Frontends」. https://samnewman.io/patterns/architectural/bff,(2023/07/14)

Slide 6

Slide 6 text

メルペイでのBFF ● メルペイでは API Gateway の直下に BFF として Merpay API というサービスが存在する ● Merpay API は多数のマイクロサービスの結果を集約し、 クライアントに最適化された結果を返す API Gateway Merpay API Service A Client Service B Service C

Slide 7

Slide 7 text

Merpay API のオーナーシップ問題 ● メルペイリリースから4年以上が経ち、Merpay API に多数のチー ムが機能追加するうちにオーナーシップが不明確に ● 応急処置として特定のチームが Merpay API 全体の責任を持つ状 態になっているが、巨大すぎて保守するコストが無視できないレベ ルに Merpay API 機能追加 運用・保守

Slide 8

Slide 8 text

Merpay API Re-Architecture Project ● Merpay API がもつすべての API に対して責任を持つチームを明確に し、その単位で Merpay API を分割して管理するプロジェクト Merpay API BFF A BFF B BFF C BEFORE AFTER

Slide 9

Slide 9 text

BFFを低いコストで開発・運用するために ● Merpay API Re-Architecture Project により、複数の BFF を作ること が決まる ○ チームの責任が明確になり、BFF の開発や保守に割く コストを低くしたい要求が高まる ● BFF は責務がシンプルなので、多くが定型作業になる特徴がある ○ 自動化などの恩恵をうけやすい ● BFF を低コストで開発・運用する何らかの仕組みが求められる ○ e.g.) Federated Architecture ( Apollo Federation ) ○ 定型作業の自動化

Slide 10

Slide 10 text

Apollo ( GraphQL ) Federationの検討 ● GraphQL を用いた Federated Architecture を構築する仕組み メルペイでの検討 ● gRPC だけで運用してきたため、GraphQL を導入するコストが高い ○ 各マイクロサービスに対応する形で GraphQL サーバーを 導入する必要がある ○ バックエンド開発者全員に GraphQL の知識が必要 ○ GraphQL に対する運用・監視の知識が必要 ● 採用を見送ることに

Slide 11

Slide 11 text

gRPC Federation ● gRPC を用いた Federated Architecture を構築する仕組みを開発 BFF ( gRPC Server ) Service A Service B Service C gRPC gRPC Federation generate

Slide 12

Slide 12 text

gRPC Federation

Slide 13

Slide 13 text

gRPC Federation ● gRPC を用いた Federated Architecture を構築する仕組み ● Protocol Buffers のオプションで表現する ● Go 製の gRPC サーバを自動生成する ● Protocol Buffers で表現できないロジックがある場合は、 Go でその部分だけ記述できる ● BFF のように自身でデータを持たず、マイクロサービスの 呼び出し結果を集約して返すようなサービスで有効

Slide 14

Slide 14 text

設計思想 ● レスポンスに注目する ○ レスポンスに相当するメッセージを取得するために gRPC メソッドを呼び出すと考える ○ 処理ではなく結果に注目する ● すべてのメッセージには、それを取得するための gRPC メソッドが必ず存在する ○ メッセージとメソッドはひもづけ可能である

Slide 15

Slide 15 text

どんな体験を提供したいか ● BFF を構築する上で必要な典型的な作業の自動化 ○ BFFと依存先サービス間の型変換作業の自動化 ● サービスの依存関係を Protocol Buffers の解析だけで把握 できるようにする ○ Protocol Buffers 上に BFF が依存するサービスを記述させるこ とで、Protocol Buffers を解析すれば依存するサービスがわかる 状態になる ○ gRPC Federation の使い方次第で、メソッド単位の 依存関係もわかる

Slide 16

Slide 16 text

BFFと依存先サービス間の型変換作業の自動化 ● BFF を構築する上で、依存先のサービスにあるメッセージを 返したい場合、同じメッセージを BFF に作成する必要がある ○ これをしないと、BFFの呼び出し元が依存先のサービスを 意識する必要があるため設計として良くない ○ パッケージをまたいだ同一型の変換が多く発生する ● gRPC Federation では Protocol Buffers 上でメッセージの 対応関係を記述することで、型変換処理をすべて自動生成する

Slide 17

Slide 17 text

サービスの依存関係を Protocol Buffers の解析だ けで把握できるようにする ● マイクロサービスアーキテクチャにおいて、サービス間の 依存関係を把握できると様々なメリットがある ○ メソッド単位で依存関係がわかれば、循環参照の有無、 パフォーマンスの理論値の算出やリファクタリング時の影響範囲の 把握などができる ● 専用の Go のライブラリや Protocol Buffers のリフレクション などを利用することで依存関係を機械的に取得できる

Slide 18

Slide 18 text

gRPC Federation に付属するツール類 ● protoc-gen-grpc-federation ○ protoc プラグイン ○ protoc-gen-go と protoc-gen-go-grpc と組み合わせて使う ● grpc-federation-linter ○ proto のコンパイルをした上で gRPC Federation の記述ミスを指 摘してくれる linter ■ 静的解析ではなく、コンパイルして解析するので正確 ● grpc-federation-language-server ○ gRPC Federation の option 記述を支援する Language Server

Slide 19

Slide 19 text

gRPC Federation の使い方 ● Post Service と User Service を使って得た結果を合成して返す BFF ( FederationService ) を作成する例で考える Federation Service Post Service User Service Post message User message post id post id Post.user_id 1 4 2 3 Post User post + user message : 処理の順序

Slide 20

Slide 20 text

各サービスの Protocol Buffers 定義 package post; service PostService { rpc GetPost (GetPostRequest) returns (GetPostReply){} } message GetPostRequest { string post_id = 1; } message GetPostReply { Post post = 1; } message Post { string id = 1; string title = 2; string content = 3; string user_id = 4; } package user; service UserService { rpc GetUser (GetUserRequest) returns (GetUserReply){} } message GetUserRequest { string user_id = 1; } message GetUserReply { User user = 1; } message User { string id = 1; string name = 2; int age = 3; } package federation; service FederationService { rpc GetPost (GetPostRequest) returns (GetPostReply){} } message GetPostRequest { string id = 1; } message GetPostReply { Post post = 1; } message Post { string id = 1; string title = 2; string content = 3; User user = 4; } message User { string id = 1; string name = 2; int age = 3; } federation.proto post.proto user.proto

Slide 21

Slide 21 text

gRPC Federation Options ● service / message / field などそれぞれに対応した option を使い 分けて記述する ○ service: grpc.federation.service ○ message: grpc.federation.message ○ field: grpc.federation.field

Slide 22

Slide 22 text

grpc.federation.service option ● gRPC Federation の対象となるサービスを指定 ● dependencies option で依存サービスを定義 import "post.proto"; //依存するサービスが存在するファイルを読み込む import "user.proto"; service FederationService { option (grpc.federation.service) = { // <パッケージ名>.<サービス名> で依存サービスを指定する dependencies: [ { service: "post.PostService" }, { service: "user.UserService" } ] }; rpc GetPost (GetPostRequest) returns (GetPostReply) {} }

Slide 23

Slide 23 text

grpc.federation.message option ● resolver / messages option を利用して、自身の各フィールドに割り 当てる値を取得する ● resolver ○ メソッドを呼び出すための定義 ○ この定義により、 message と メソッドがひもづく ○ 1つの message につき 1つだけ記述できる ● messages ○ 依存メッセージの定義 ○ 複数指定できる

Slide 24

Slide 24 text

resolver option package federation; message Post { option (grpc.federation.message) = { resolver { method: "post.PostService/GetPost" request: [ { field: "post_id", by: "$.id" } ] response: [{ name: "res" field: "post" autobind: true }] } }; string id = 1; string title = 2; string content = 3; } package post; service PostService { rpc GetPost (GetPostRequest) returns (GetPostReply){} } message GetPostRequest { string post_id = 1; } message GetPostReply { Post post = 1; } message Post { string id = 1; string title = 2; string content = 3; string user_id = 4; } federation.proto post.proto

Slide 25

Slide 25 text

resolver.method option package federation; message Post { option (grpc.federation.message) = { resolver { //呼び出すメソッド名を FQDN で指定する method: "post.PostService/GetPost" request: [ { field: "post_id", by: "$.id" } ] response: [{ name: "res" field: "post" autobind: true }] } }; string id = 1; string title = 2; string content = 3; } package post; service PostService { rpc GetPost (GetPostRequest) returns (GetPostReply){} } message GetPostRequest { string post_id = 1; } message GetPostReply { Post post = 1; } message Post { string id = 1; string title = 2; string content = 3; string user_id = 4; } federation.proto post.proto

Slide 26

Slide 26 text

resolver.request option package federation; message Post { option (grpc.federation.message) = { resolver { method: "post.PostService/GetPost" //リクエストメッセージのフィールドを指定する request: [ { field: "post_id", by: "$.id" } ] response: [{ name: "res" field: "post" autobind: true }] } }; string id = 1; string title = 2; string content = 3; } package post; service PostService { rpc GetPost (GetPostRequest) returns (GetPostReply){} } message GetPostRequest { string post_id = 1; } message GetPostReply { Post post = 1; } message Post { string id = 1; string title = 2; string content = 3; string user_id = 4; } federation.proto post.proto

Slide 27

Slide 27 text

resolver.response option package federation; message Post { option (grpc.federation.message) = { resolver { method: "post.PostService/GetPost" request: [ { field: "post_id", by: "$.id" } ]   //得られたレスポンスのうち、 //どの値をどの名前で参照するか指定する response: [{ name: "res" field: "post" autobind: true }] } }; string id = 1; string title = 2; string content = 3; } package post; service PostService { rpc GetPost (GetPostRequest) returns (GetPostReply){} } message GetPostRequest { string post_id = 1; } message GetPostReply { Post post = 1; } message Post { string id = 1; string title = 2; string content = 3; string user_id = 4; } federation.proto post.proto

Slide 28

Slide 28 text

resolver.response[].name option package federation; message Post { option (grpc.federation.message) = { resolver { method: "post.PostService/GetPost" request: [ { field: "post_id", by: "$.id" } ] response: [{ // "res" という名前で得た値を参照する name: "res" field: "post" autobind: true }] } }; string id = 1; string title = 2; string content = 3; } package post; service PostService { rpc GetPost (GetPostRequest) returns (GetPostReply){} } message GetPostRequest { string post_id = 1; } message GetPostReply { Post post = 1; } message Post { string id = 1; string title = 2; string content = 3; string user_id = 4; } federation.proto post.proto

Slide 29

Slide 29 text

resolver.response[].field option package federation; message Post { option (grpc.federation.message) = { resolver { method: "post.PostService/GetPost" request: [ { field: "post_id", by: "$.id" } ] response: [{ name: "res" // レスポンスのうち、 // post という名前のフィールドを指定する // ( 未指定の場合はレスポンス自体 ) field: "post" autobind: true }] } }; string id = 1; string title = 2; string content = 3; } package post; service PostService { rpc GetPost (GetPostRequest) returns (GetPostReply){} } message GetPostRequest { string post_id = 1; } message GetPostReply { Post post = 1; } message Post { string id = 1; string title = 2; string content = 3; string user_id = 4; } federation.proto post.proto

Slide 30

Slide 30 text

resolver.response[].autobind option package federation; message Post { option (grpc.federation.message) = { resolver { method: "post.PostService/GetPost" request: [ { field: "post_id", by: "$.id" } ] response: [{ name: "res" field: "post" // レスポンスの各フィールドと // 同じ名前・同じ型のフィールドが存在する // 場合、自動的にフィールドへ値を代入する autobind: true }] } }; string id = 1; //res.idの値が代入される string title = 2; //res.title string content = 3; //res.content } package post; service PostService { rpc GetPost (GetPostRequest) returns (GetPostReply){} } message GetPostRequest { string post_id = 1; } message GetPostReply { Post post = 1; } message Post { string id = 1; string title = 2; string content = 3; string user_id = 4; }

Slide 31

Slide 31 text

messages option message Post { option (grpc.federation.message) = { resolver { method: "post.PostService/GetPost" request { field: "post_id", by: "$.id" } response { name: "res", field: "post", autobind: true } } messages { name: "u" message: "User" args: [{ name: "uid" by: "res.user_id" }] } }; } message User { option (grpc.federation.message) = { resolver { method: "user.UserService/GetUser" request { field: "user_id", by: "$.uid" } response { field: "user", autobind: true } } }; string id = 1; string name = 2; int age = 3; } federation.proto

Slide 32

Slide 32 text

messages[].name option message Post { option (grpc.federation.message) = { resolver { method: "post.PostService/GetPost" request { field: "post_id", by: "$.id" } response { name: "res", field: "post", autobind: true } } messages { // 依存メッセージを参照する時の名前を指定する name: "u" message: "User" args: [{ name: "uid" by: "res.user_id" }] } }; } message User { option (grpc.federation.message) = { resolver { method: "user.UserService/GetUser" request { field: "user_id", by: "$.uid" } response { field: "user", autobind: true } } }; string id = 1; string name = 2; int age = 3; } federation.proto

Slide 33

Slide 33 text

messages[].message option message Post { option (grpc.federation.message) = { resolver { method: "post.PostService/GetPost" request { field: "post_id", by: "$.id" } response { name: "res", field: "post", autobind: true } } messages { name: "u" // 依存するメッセージの名前を指定する message: "User" args: [{ name: "uid" by: "res.user_id" }] } }; } message User { option (grpc.federation.message) = { resolver { method: "user.UserService/GetUser" request { field: "user_id", by: "$.uid" } response { field: "user", autobind: true } } }; string id = 1; string name = 2; int age = 3; } federation.proto

Slide 34

Slide 34 text

messages[].args option message Post { option (grpc.federation.message) = { resolver { method: "post.PostService/GetPost" request { field: "post_id", by: "$.id" } response { name: "res", field: "post", autobind: true } } messages { name: "u" message: "User" // 依存メッセージを取得する際に必要になるパラメータを指定する。 // これらのパラメータを「メッセージ引数」と呼ぶ args: [{ name: "uid" by: "res.user_id" }] } }; } message User { option (grpc.federation.message) = { resolver { method: "user.UserService/GetUser" request { field: "user_id", by: "$.uid" } response { field: "user", autobind: true } } }; string id = 1; string name = 2; int age = 3; } federation.proto

Slide 35

Slide 35 text

messages[].args[].name option message Post { option (grpc.federation.message) = { resolver { method: "post.PostService/GetPost" request { field: "post_id", by: "$.id" } response { name: "res", field: "post", autobind: true } } messages { name: "u" message: "User" args: [{ // メッセージ引数の名前を指定する。 // 依存先のメッセージでこの名前に "$." 接頭辞を付けると参照できる。 name: "uid" by: "res.user_id" }] } }; } message User { option (grpc.federation.message) = { resolver { method: "user.UserService/GetUser" // $.uid でメッセージ引数を参照する request { field: "user_id", by: "$.uid" } response { field: "user", autobind: true } } }; string id = 1; string name = 2; int age = 3; } federation.proto

Slide 36

Slide 36 text

messages[].args[].by option message Post { option (grpc.federation.message) = { resolver { method: "post.PostService/GetPost" request { field: "post_id", by: "$.id" } response { name: "res", field: "post", autobind: true } } messages { name: "u" message: "User" args: [{ name: "uid" // メッセージ引数として渡す値を指定する。 by: "res.user_id" }] } }; } message User { option (grpc.federation.message) = { resolver { method: "user.UserService/GetUser" request { field: "user_id", by: "$.uid" } response { field: "user", autobind: true } } }; string id = 1; string name = 2; int age = 3; } federation.proto

Slide 37

Slide 37 text

grpc.federation.field option ● grpc.federation.message option で定義した値やメッセージ引数を 参照し、値をフィールドにひもづける message Post { option (grpc.federation.message) = { resolver { method: "post.PostService/GetPost" request { field: "post_id", by: "$.id" } response { name: "res", field: "post", autobind: true } } messages { name: "u", message: "User", args { name: "uid", by: "res.user_id" } } }; string id = 1; string title = 2; string content = 3; // u で User message を参照してひもづける User user = 4 [(grpc.federation.field).by = "u"]; }

Slide 38

Slide 38 text

レスポンスにoptionを追加して完成 ● Post メッセージを作っただけでは、GetPost メソッドは未完成 ● レスポンスである GetPostReply に option を追加して完成 ○ レスポンスの作り方が決まる => 実装が完成する ● レスポンスのメッセージ引数はリクエストの各フィールドになる message GetPostReply { option (grpc.federation.message) = { // $.id で GetPostRequest の id フィールドを参照できる。 // 参照した値は、そのまま Post メッセージ側で id という名前で参照できるようにする messages { name: "p", message: "Post", args { name: "id", by: "$.id" } } }; Post post = 1 [(grpc.federation.field).by = "p"]; }

Slide 39

Slide 39 text

その他の機能を少し紹介 (実装予定も含む) ● 他パッケージのメッセージ・メソッドの参照 ○ Protocol Buffers 上で gRPC Federation の資産を再利用する ● 複雑なロジックの定義 ○ ProtocolBuffers 上では定義できないため、 message や field option で custom_resolver = true と 記述することで、Go で実装できる ● grpc.federation.method option によるメソッドレベルの制御 ○ timeout の設定 ● grpc.federation.oneof option による oneof 内の条件分岐

Slide 40

Slide 40 text

gRPC Federation の今後 ● https://github.com/mercari/grpc-federation ● 現在 Alpha version だが、今年中に社内の本番環境で活用できる よう改善を続けている ● Federated Architecture を構築する上でのひとつの解にしていきたい ● PullRequest はまだ受け付けていない ○ 機能要望・改善案・使用感などのフィードバックはウェルカム ○ Issue や Twitter などのコメントで反応して欲しい ■ GitHub: @goccy ■ Twitter: @goccy54

Slide 41

Slide 41 text

まとめ ● マイクロサービスアーキテクチャにおける BFF の重要性と メリット・デメリットについて触れた ● メルペイでは巨大なBFF のオーナーシップ問題を解決するために、いく つかの BFF に分割することを考えている ● 各 BFF の開発を効率的に行うために、gRPC Federation を開発して いる ● gRPC Federation を使ったシンプルな BFF の構築例を示した ○ フィードバック待ってます

Slide 42

Slide 42 text

No content