Upgrade to Pro — share decks privately, control downloads, hide ads and more …

gRPC Federation を利用した巨大なBFFサービスに対するリアーキテクチャの試み ...

mercari
October 14, 2023

gRPC Federation を利用した巨大なBFFサービスに対するリアーキテクチャの試み / Testing the Re-Architecture on Giant BFF Services That Use gRPC Federation

メルペイではアプリケーションからのリクエストに対して複数のマイクロサービスの呼び出し結果を集約して返すバックエンドサービスが存在します。メルペイのリリースから4年以上が経ち、多数のマイクロサービスに依存しコードが肥大化していく中で、このサービスを運用するチームの責任の所在やメンテナンスコストが問題になりました。そこでチームの責任が明確になるように肥大化したサービスを複数のサービスに分割する計画を進めています。その際にメンテナンスコストが高くならないよう、gRPC プロトコルを対象とした Federated Architecture を構築できる gRPC Federation という仕組みを開発し、導入を検討している話をします。

Merpay operates using a backend service that collects and returns the results of calling multiple microservices in response to requests from the application. More than four years after the release of Merpay, amid dependencies on multiple microservices and a ballooning codebase, questions arose about which team is responsible for this service and about who was responsible for absorbing maintenance costs for operating this service. To address this and clarify which teams were responsible for the service, we came up with a plan to divide the ballooning service into multiple services. This session will talk about how we are considering developing and implementing a mechanism called gRPC Federation, which can build a federated architecture that targets gRPC protocols to keep maintenance costs from becoming too high.

------
Merpay & Mercoin Tech Fest 2023は3日間のオンライン技術カンファレンスです。
IT企業で働くソフトウェアエンジニアおよびメルペイ・メルコインの技術スタックに興味がある方々を対象に2023年8月22日(火)、23日(水)、24日(木)の3日間、開催します。 Merpay & Mercoin Tech Fest は事業との関わりから技術への興味を深め、プロダクトやサービスを支えるエンジニアリングを知ることができるお祭りです。

今年のテーマは「Unleash Fintech」。 メルペイ・メルコインのこれまでの技術的な取り組みはもちろん、メルカリグループのFintech事業における新たな挑戦をお伝えします。 セッションでは事業を支える組織・技術・課題などへの試行錯誤やアプローチなど多面的にご紹介予定です。

メルペイ・メルコインが今後どのようにUnleash(解放)していくのか、ぜひ見に来てください。

■イベント関連情報
- 公式ウェブサイト:https://events.merpay.com/techfest-2023/
- 申し込みページ:https://mercari.connpass.com/event/286670/
- Twitterハッシュタグ: #MerpayMercoinTechFest
■リンク集
- メルカリ・メルペイイベント一覧:https://mercari.connpass.com/
- メルカリキャリアサイト:https://careers.mercari.com/
- メルカリエンジニアリングブログ:https://engineering.mercari.com/blog/
- メルカリエンジニア向けTwitterアカウント:https://twitter.com/mercaridevjp
- 株式会社メルペイ:https://jp.merpay.com/

mercari

October 14, 2023
Tweet

More Decks by mercari

Other Decks in Technology

Transcript

  1. Masaaki Goshima / @goccy 2012年に株式会社MIXIに入社。ウェブやアプリの フロントエンドからバックエンドまで 技術領域問わずいろいろなものを作る。 2012、2013 年に YAPC::Asia

    で自作のPerl処理 系に関して登壇。その後ゲーム系のベンチャー企業 でテックリードを務め、2020年メルペイに入社。OSS 開発が好きで最近は Go の OSS をよく書いてい る。夢は 10k stars。 株式会社メルペイ Engineering Productivity
  2. BFFの役割とメリット・デメリット • Backend For Frontend の略 • クライアントに特化したレスポンスを返す専用サービス • クライアントは

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

    というサービスが存在する • Merpay API は多数のマイクロサービスの結果を集約し、 クライアントに最適化された結果を返す API Gateway Merpay API Service A Client Service B Service C
  4. Merpay API のオーナーシップ問題 • メルペイリリースから4年以上が経ち、Merpay API に多数のチー ムが機能追加するうちにオーナーシップが不明確に • 応急処置として特定のチームが

    Merpay API 全体の責任を持つ状 態になっているが、巨大すぎて保守するコストが無視できないレベ ルに Merpay API 機能追加 運用・保守
  5. Merpay API Re-Architecture Project • Merpay API がもつすべての API に対して責任を持つチームを明確に

    し、その単位で Merpay API を分割して管理するプロジェクト Merpay API BFF A BFF B BFF C BEFORE AFTER
  6. BFFを低いコストで開発・運用するために • Merpay API Re-Architecture Project により、複数の BFF を作ること が決まる

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

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

    gRPC Server ) Service A Service B Service C gRPC gRPC Federation generate
  9. gRPC Federation • gRPC を用いた Federated Architecture を構築する仕組み • Protocol

    Buffers のオプションで表現する • Go 製の gRPC サーバを自動生成する • Protocol Buffers で表現できないロジックがある場合は、 Go でその部分だけ記述できる • BFF のように自身でデータを持たず、マイクロサービスの 呼び出し結果を集約して返すようなサービスで有効
  10. 設計思想 • レスポンスに注目する ◦ レスポンスに相当するメッセージを取得するために gRPC メソッドを呼び出すと考える ◦ 処理ではなく結果に注目する •

    すべてのメッセージには、それを取得するための gRPC メソッドが必ず存在する ◦ メッセージとメソッドはひもづけ可能である
  11. どんな体験を提供したいか • BFF を構築する上で必要な典型的な作業の自動化 ◦ BFFと依存先サービス間の型変換作業の自動化 • サービスの依存関係を Protocol Buffers

    の解析だけで把握 できるようにする ◦ Protocol Buffers 上に BFF が依存するサービスを記述させるこ とで、Protocol Buffers を解析すれば依存するサービスがわかる 状態になる ◦ gRPC Federation の使い方次第で、メソッド単位の 依存関係もわかる
  12. サービスの依存関係を Protocol Buffers の解析だ けで把握できるようにする • マイクロサービスアーキテクチャにおいて、サービス間の 依存関係を把握できると様々なメリットがある ◦ メソッド単位で依存関係がわかれば、循環参照の有無、

    パフォーマンスの理論値の算出やリファクタリング時の影響範囲の 把握などができる • 専用の Go のライブラリや Protocol Buffers のリフレクション などを利用することで依存関係を機械的に取得できる
  13. 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
  14. 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 : 処理の順序
  15. 各サービスの 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
  16. gRPC Federation Options • service / message / field などそれぞれに対応した

    option を使い 分けて記述する ◦ service: grpc.federation.service ◦ message: grpc.federation.message ◦ field: grpc.federation.field
  17. 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) {} }
  18. grpc.federation.message option • resolver / messages option を利用して、自身の各フィールドに割り 当てる値を取得する •

    resolver ◦ メソッドを呼び出すための定義 ◦ この定義により、 message と メソッドがひもづく ◦ 1つの message につき 1つだけ記述できる • messages ◦ 依存メッセージの定義 ◦ 複数指定できる
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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; }
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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"]; }
  33. レスポンスに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"]; }
  34. その他の機能を少し紹介 (実装予定も含む) • 他パッケージのメッセージ・メソッドの参照 ◦ Protocol Buffers 上で gRPC Federation

    の資産を再利用する • 複雑なロジックの定義 ◦ ProtocolBuffers 上では定義できないため、 message や field option で custom_resolver = true と 記述することで、Go で実装できる • grpc.federation.method option によるメソッドレベルの制御 ◦ timeout の設定 • grpc.federation.oneof option による oneof 内の条件分岐
  35. gRPC Federation の今後 • https://github.com/mercari/grpc-federation • 現在 Alpha version だが、今年中に社内の本番環境で活用できる

    よう改善を続けている • Federated Architecture を構築する上でのひとつの解にしていきたい • PullRequest はまだ受け付けていない ◦ 機能要望・改善案・使用感などのフィードバックはウェルカム ◦ Issue や Twitter などのコメントで反応して欲しい ▪ GitHub: @goccy ▪ Twitter: @goccy54
  36. まとめ • マイクロサービスアーキテクチャにおける BFF の重要性と メリット・デメリットについて触れた • メルペイでは巨大なBFF のオーナーシップ問題を解決するために、いく つかの

    BFF に分割することを考えている • 各 BFF の開発を効率的に行うために、gRPC Federation を開発して いる • gRPC Federation を使ったシンプルな BFF の構築例を示した ◦ フィードバック待ってます