Slide 1

Slide 1 text

© LayerX Inc. connect-go で面倒くささと戦う 2024-08-27 newmo × LayerX "Go"同 勉強会 @izumin5210

Slide 2

Slide 2 text

© LayerX Inc. 2 ▸ Wantedly, Inc. (2018-04 - 2022-08) ▸ LayerX (2022-09-) ‐ バクラク事業部 Platform Engineering 部 Enabling Team ‐ Backend と Web Frontend 中心にやってます ▸ 最近気になっているパッケージは github.com/pingcap/tidb/parser 画像を入れてね whoami @izumin5210

Slide 3

Slide 3 text

はじめに 前提になるプロダクト「バクラク」とアーキテクチャ概要

Slide 4

Slide 4 text

4 © LayerX Inc. 今日の話の前提: バクラクについて LayerX Company Deck https://speakerdeck.com/layerx/company-deck?slide=23

Slide 5

Slide 5 text

© LayerX Inc. 5 ▸ プロダクト: お客様に1つのパッケージとして価値を提供するソフトウェア製品 ‐ (「バクラク申請」など、右下の図における ○ を指すイメージ) ▸ サービス: 論理的あるいは物理的に分けられた1つのサーバアプリケーション ‐ マイクロサービス・アーキテクチャにおける 「サービス」という単語と概ね同等 発表内での用語の定義 今日の話の前提: 用語の定義

Slide 6

Slide 6 text

6 © LayerX Inc. 今日の話の前提: バクラクのバックエンド API の移り変わり Product1 Webapp Product2 Webapp Product1 Service Product2 Service OpenAPI GraphQL ▸ 初期のプロダクトは go-swagger, gqlgen などでバックエンドサーバを作っていた

Slide 7

Slide 7 text

7 © LayerX Inc. 今日の話の前提: バクラクのバックエンド API の移り変わり Product1 Webapp Product2 Webapp Product1 Service Product2 Service Product3 Webapp Product3 Service OpenAPI GraphQL

Slide 8

Slide 8 text

8 © LayerX Inc. 今日の話の前提: バクラクのバックエンド API の移り変わり Product1 Webapp Product2 Webapp Product1 Service Product2 Service Product3 Webapp Product3 Service こっちの情報も見せたい… こっちの情報を使いたい… OpenAPI GraphQL

Slide 9

Slide 9 text

9 © LayerX Inc. 今日の話の前提: バクラクのバックエンド API の移り変わり Product1 Webapp Product2 Webapp Product1 Service Product2 Service Product3 Webapp Product3 Service OpenAPI GraphQL GraphQL Gateway GraphQL Connect ▸ 初期のプロダクトは go-swagger, gqlgen などでバックエンドサーバを作っていた ▸ プロダクトやプロダクト間連携が増えてきたことに対応するため、 GraphQL Gateway (!= BFF, Node.js 製)を構築し、バックエンドに connect-go を使い始めた

Slide 10

Slide 10 text

Connect #とは

Slide 11

Slide 11 text

© LayerX Inc. 11 ▸ gRPCと互換性をもつ、 HTTP APIを実装するためのフレームワーク ▸ gRPC Protocolとの完全な互換性に加え、 human-readable and debuggableなConnect Protocolをサポート ‐ JSONやProtobufをHTTP/1.1の上でやりとりできる(片方向通信の場合) 💡豆知識 “Connect” がプロジェクト名だが、文脈からわかりづらいときなどは “Connect RPC” と表記してもいいらしい* Connect #とは Connect #とは * https://connectrpc.com/docs/faq

Slide 12

Slide 12 text

© LayerX Inc. 12 protobuf-go のコード生成 Connect #とは message GreetRequest { string name = 1; } こういうmessage定義から… type GreetRequest struct { // private fields Name string `::.` } こういう構造体定義が生成される

Slide 13

Slide 13 text

© LayerX Inc. 13 connect-go のコード生成 Connect #とは service GreetService { rpc Greet(GreetRequest) returns (GreetResponse) {} } こういうサービス定義から…

Slide 14

Slide 14 text

© LayerX Inc. 14 connect-go のコード生成 Connect #とは type GreetServiceClient interface { // クライアントがリクエストするときの interface Greet(context.Context, *connect.Request[v1.GreetRequest]) (*connect.Response[v1.GreetResponse], error) } type GreetServiceHandler interface { // サーバが実装することになる interface Greet(context.Context, *connect.Request[v1.GreetRequest]) (*connect.Response[v1.GreetResponse], error) } こういうのを生成してくれる (un)marshal など「いつもの」な処理はConnectがやってくれる

Slide 15

Slide 15 text

© LayerX Inc. 15 (いろんなところで話しまくってるので圧縮してお届け 📦) ▸ Why APIスキーマ ‐ 自然言語によらず、機械同士・人間同士いずれのコミュニケーションにも使えるプロトコルになる ‐ サーバ・クライアントいずれもコード生成できる場合が多く、スキーマと実装の乖離を防ぐことができる ▸ Why Protobuf ‐ スキーマ記述が簡素で可読性が高い(e.g. OpenAPI と比較し情報密度が高くなりやすい, 一般的なプログラミング言語っぽい, etc.) ‐ 互換性を保ちやすい, 互換性が壊れたことを機械的に検知できる ‐ 拡張性の高さ ‐ message(データ, リソース, モデル)を考える方向に思考が働きやすい(cf. OpenAPI だと operation にベタ書きしちゃう) ▸ Why gRPC ‐ Protobuf で記述した interface の実装に集中できる(json.Unmarshal のような「いつもの」コードは不要) などなど Why APIスキーマ, Why Protobuf, Why gRPC Connect #とは 今さらProtocol Buffersと、手に馴染む道具の話 https://qiita.com/yugui/items/160737021d25d761b353 Protocol Buffers によるプロダクト開発のススメ - API 開発の今昔 https://www.wantedly.com/companies/wantedly/post_articles/309513

Slide 16

Slide 16 text

© LayerX Inc. 16 ▸ Go 実装の出来がいい ‐ サーバは net/http.Handler, クライアントは net/http.Client であり 利用可能な 3rd party パッケージの幅が広い ▸ HTTP/1.1 かつ application/json なリクエストを投げられる ‐ ローカル開発で雑にデバッグできて便利! ‐ 特別なクライアント実装がなくても使えるので、応用が効きやすい(後述) 👉 いずれも gRPC(grpc-go) の抱える複雑性を低減してくれそう Why Connect(connect-go) Connect #とは

Slide 17

Slide 17 text

© LayerX Inc. 17 ▸ スキーマ駆動自体は gRPC でもできる ▸ 一方で、プロトコルからくる仕様・実装により別の複雑性が発生していた ▸ Connect(connect-go) を使うことで、その gRPC の複雑さを回避するぞ! …というのが今日の内容です Why Connect(connect-go) Connect #とは

Slide 18

Slide 18 text

バクラクでの Connect 応用例 1 非同期API・非同期ジョブ

Slide 19

Slide 19 text

© LayerX Inc. 19 非同期API・非同期ジョブ バクラクでの Connect 応用例: 非同期API・非同期ジョブ ▸ スキーマがないことが多い ‐ そのジョブのペイロードの struct を定義してても、安易に変えるとデプロイの間で壊れたり ‐ caller と callee が同じサービスならまだマシだが、別だったらスキーマないと地獄 ▸ ローカル等での雑デバッグ面倒になりがち ‐ (テストを書こうという話もありつつ…)手元でシュッと動かしたいときに面倒になりがち ‐ REPL がある言語ならやりやすいんだけど…

Slide 20

Slide 20 text

© LayerX Inc. 20 非同期API・非同期ジョブにもConnectを バクラクでの Connect 応用例: 非同期API・非同期ジョブ Publisher Connect server service GreetService { rpc Greet(GreetRequest) returns (GreetResponse) {} } 行き先(GreetService/Greet)と ペイロード(GreetRequest)を キューに詰める Subscriber Worker

Slide 21

Slide 21 text

© LayerX Inc. 21 非同期API・非同期ジョブにもConnectを バクラクでの Connect 応用例: 非同期API・非同期ジョブ Publisher Connect server Subscriber Worker キューから取り出した内容を見て Subscriber が適切な Connect Server を呼ぶ Connect Protocol でリクエストすればいいだけ ここがConnectによる通信になる

Slide 22

Slide 22 text

© LayerX Inc. 22 この仕組みがなぜ嬉しいか バクラクでの Connect 応用例: 非同期API・非同期ジョブ Publisher Connect server Subscriber Worker ▸ 非同期でもちゃんとスキーマ定義できる ‐ 雑に json.Marshal するのではなく、 同期APIと同じメンタルモデルで型定義を。 非同期ジョブ・非同期APIもAPI! ▸ 雑デバッグが楽! ‐ Publisher がまだなくてもおもむろに cURL で動かせる!

Slide 23

Slide 23 text

バクラクでの Connect 応用例 2 ローカルでの起動を楽にする

Slide 24

Slide 24 text

© LayerX Inc. 24 ローカルでの起動がどんどん大変になる問題 バクラクでの Connect 応用例: ローカルでの起動を楽にする TenantService CardTransactionService WorkflowService NotificationService ▸ プロダクトが発展する、あるいは増えると サービスも増える ▸ いまの開発に必要なのはこれとこれと… と 起動していくのは面倒 ▸ ポート番号を予約してくのも面倒

Slide 25

Slide 25 text

© LayerX Inc. 25 ローカルでは1つのHTTP serverに 全Connect serverを乗せる バクラクでの Connect 応用例: ローカルでの起動を楽にする TenantService NotificationService CardTransactionService 論理的には別のサービス ローカルでは物理的には1つのサービス TenantService NotificationService CardTransactionService 社内ではgo-allと呼ばれています

Slide 26

Slide 26 text

© LayerX Inc. 26 ローカルでは1つのHTTP serverに 全Connect serverを乗せる バクラクでの Connect 応用例: ローカルでの起動を楽にする TenantService NotificationService CardTransactionService ▸ ローカルでは全サービスを1つに まとめることで、起動を楽にしている ▸ cURLで雑デバッグしたいときも ポート番号を1つだけ覚えておけばいい ▸ デプロイ時は 論理的なサービス = 物理的なサービス

Slide 27

Slide 27 text

© LayerX Inc. 27 バクラクでの Connect 応用例: ローカルでの起動を楽にする func HandlerOptions(ctx context.Context) ([]connect.Option, error) { svc, err := NewServiceHandler(ctx) if err := nil { return nil, err } path, handler := tenantv1connect.NewTenantServiceHandler(svc, /* common options //) return []connect.Option{ connect.WithHandler(path, handler), connect.WithCloseHandler(svc), }, nil } http.Handler と path を生成する関数を各サービスに生成して、

Slide 28

Slide 28 text

© LayerX Inc. 28 バクラクでの Connect 応用例: ローカルでの起動を楽にする var ServiceHandlerOptions = map[string]func() ([]connect.Option, error){ tenantv1connect.TenantServiceName: tenant_v1_tenant.HandlerOptions, // 全サービスの HandlerOptions 関数を突っ込んでおく } 全サービスの HandlerOptions 関数を突っ込んだ map をコード生成する あとはこれを使って Connect server を作るだけ

Slide 29

Slide 29 text

© LayerX Inc. 29 ローカルでは1つのHTTP serverに 全Connect serverを乗せて、ログは頑張って探しやすくする バクラクでの Connect 応用例: ローカルでの起動を楽にする ▸ 「複数サービスのログが1つの stdout で流れてしまう」というデメリット ‐ 読めねえ ▸ バクラクでは Grafana 上でログ・トレースを探せるようにしている ‐ プロセスマネージャ* でログを OpenTelemetry に変換し、Loki, Tempo に投入 ‐ ログだけでいいならシンプルに Promtail + Loki でも可 * エンジニアオンボーディングを改善するツールの紹介 https://tech.layerx.co.jp/entry/2022/12/12/131507

Slide 30

Slide 30 text

バクラクでの Connect 応用例 2.5 ローカルでの起動を楽に・かつ速くする

Slide 31

Slide 31 text

© LayerX Inc. 31 全サービスを1つにまとめると、ビルドはどんどん重くなる バクラクでの Connect 応用例: ローカルでの起動を速くする ▸ あらゆるサービスをひとまとめにビルドしようとすると、当然ながら重くなる ▸ ローカルでは、いま開発中のサービス以外の要因で待たされたくない ‐ いま開発中 is … リクエストが来たサービス? 👉 リクエストが来たサービスだけビルドする!

Slide 32

Slide 32 text

© LayerX Inc. 32 ビルドはどんどん重くなっていくので、 部分的にビルドできるようにする バクラクでの Connect 応用例: ローカルでの起動を速くする ▸ メインの port を listen してるプロセスがリクエストを受けたら、 ‐ 対象のサービスをビルドし ‐ Unix domain socket を listen する形で起動して ‐ そこにリクエストを流す! ▸ 最小限しかビルドしないので速い! ▸ これも go-all と似たような感じで実装を生成する

Slide 33

Slide 33 text

バクラクでの Connect 活用術 3 デプロイもちょっと楽にする

Slide 34

Slide 34 text

© LayerX Inc. 34 ローカルでは1つのHTTP serverに 全Connect serverを乗せる バクラクでの Connect 応用例: デプロイもちょっと楽にする TenantService NotificationService CardTransactionService ▸ ローカルでは全サービスを1つに まとめることで、起動を楽にしている 👉 全サービスを1バイナリにまとめてるなら AWS 上でも1バイナリだけでいいのでは?

Slide 35

Slide 35 text

© LayerX Inc. 35 1つのバイナリに全Connect serverを乗せて、 起動時に出し分ける バクラクでの Connect 応用例: デプロイもちょっと楽にする ▸ 全てのサービスをまとめたバイナリをAWS 上でも動かせば、 1度ビルドしたバイナリは使い回せる! ‐ ビルド時間, ストレージなどの節約になる! ▸ 実際は振る舞いがちょっと違うので、モードを2つに分けている* ‐ 全サービスが起動するモードと、特定のサービスのみ起動するモード * connect\-go による複数サービスの開発とユニバーサルバイナリによる改善 https://tech.layerx.co.jp/entry/2023/07/18/173901

Slide 36

Slide 36 text

バクラクでの Connect 活用術 3 デプロイをさらに楽にする

Slide 37

Slide 37 text

© LayerX Inc. 37 ローカルでは1つのHTTP serverに 全Connect serverを乗せる バクラクでの Connect 応用例: デプロイをさらに楽にする TenantService NotificationService CardTransactionService ▸ デプロイ時は 論理的なサービス == 物理的なサービス 👉 …じゃなくてもいいのでは?

Slide 38

Slide 38 text

© LayerX Inc. 38 複数の論理的なサービスを1つのデプロイメントにまとめる バクラクでの Connect 応用例: デプロイをさらに楽にする TenantService CardTransactionService NotificationService ▸ 非機能要件面で分ける理由がない場合は、 1つのデプロイメントに 複数の論理的なサービスを乗せることも可 WorkflowService

Slide 39

Slide 39 text

バクラクでの Protobuf 活用術 もはや Connect あんまり関係ないが、 Protobuf 自体も便利!

Slide 40

Slide 40 text

© LayerX Inc. 40 ▸ データを定義してそこから必要な実装を生やすシリーズ ‐ e.g. FeatureFlag, 権限・ロール ▸ Protobuf の message, enum から GraphQL の型を生成 ▸ Protobuf Message に sql.Scanner & driver.Valuer を実装し、 JSON 表現で DB に格納してくれるコードを生成する ▸ サービス間通信の認証情報は Protobuf で payload 定義し、署名を付けている ‐ JWT 的な感じだが、Protobuf なのでサイズを抑えることができる ▸ Connect のサービス定義から Go のサービス実装の scaffolding バクラクでの Protobuf 活用例

Slide 41

Slide 41 text

まとめ

Slide 42

Slide 42 text

© LayerX Inc. 42 まとめ もともとのモチベーション ▸ スキーマ駆動自体は gRPC でもできる ‐ 一方で、プロトコルからくる仕様・実装により別の複雑性が発生していた ▸ Connect(connect-go) を使うことで、その gRPC の複雑さを回避するぞ! Connect によって… ▸ 非同期ジョブ・非同期 API にも Connect を使い、通常の API と同じ開発体験に ▸ ローカルでは全サービスのプロセスを1つにまとめて、起動を楽に ‐ さらにそのうち必要なサービスのみビルドすることで、起動を高速に ▸ 本番でも複数の論理サービスを1つの物理サービスにまとめることでインフラも楽に ▸ その他 Protobuf にいろいろやってもらって、いろいろ便利に