$30 off During Our Annual Pro Sale. View Details »

connect-go で面倒くささと戦う / 2024-08-27 #newmo_layerx_go

connect-go で面倒くささと戦う / 2024-08-27 #newmo_layerx_go

newmo × LayerX "Go"同 勉強会
https://layerx.connpass.com/event/323385/

Masayuki Izumi

August 27, 2024
Tweet

More Decks by Masayuki Izumi

Other Decks in Programming

Transcript

  1. © 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
  2. © LayerX Inc. 5 ▸ プロダクト: お客様に1つのパッケージとして価値を提供するソフトウェア製品 ‐ (「バクラク申請」など、右下の図における ◦

    を指すイメージ) ▸ サービス: 論理的あるいは物理的に分けられた1つのサーバアプリケーション ‐ マイクロサービス・アーキテクチャにおける 「サービス」という単語と概ね同等 発表内での用語の定義 今日の話の前提: 用語の定義
  3. 6 © LayerX Inc. 今日の話の前提: バクラクのバックエンド API の移り変わり Product1 Webapp

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

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

    Product2 Webapp Product1 Service Product2 Service Product3 Webapp Product3 Service こっちの情報も見せたい… こっちの情報を使いたい… OpenAPI GraphQL
  6. 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 を使い始めた
  7. © 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
  8. © LayerX Inc. 12 protobuf-go のコード生成 Connect #とは message GreetRequest

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

    { rpc Greet(GreetRequest) returns (GreetResponse) {} } こういうサービス定義から…
  10. © 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がやってくれる
  11. © 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
  12. © 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 #とは
  13. © LayerX Inc. 17 ▸ スキーマ駆動自体は gRPC でもできる ▸ 一方で、プロトコルからくる仕様・実装により別の複雑性が発生していた

    ▸ Connect(connect-go) を使うことで、その gRPC の複雑さを回避するぞ! …というのが今日の内容です Why Connect(connect-go) Connect #とは
  14. © LayerX Inc. 19 非同期API・非同期ジョブ バクラクでの Connect 応用例: 非同期API・非同期ジョブ ▸

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

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

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

    Connect server Subscriber Worker ▸ 非同期でもちゃんとスキーマ定義できる ‐ 雑に json.Marshal するのではなく、 同期APIと同じメンタルモデルで型定義を。 非同期ジョブ・非同期APIもAPI! ▸ 雑デバッグが楽! ‐ Publisher がまだなくてもおもむろに cURL で動かせる!
  18. © LayerX Inc. 24 ローカルでの起動がどんどん大変になる問題 バクラクでの Connect 応用例: ローカルでの起動を楽にする TenantService

    CardTransactionService WorkflowService NotificationService ▸ プロダクトが発展する、あるいは増えると サービスも増える ▸ いまの開発に必要なのはこれとこれと… と 起動していくのは面倒 ▸ ポート番号を予約してくのも面倒
  19. © LayerX Inc. 25 ローカルでは1つのHTTP serverに 全Connect serverを乗せる バクラクでの Connect

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

    応用例: ローカルでの起動を楽にする TenantService NotificationService CardTransactionService ▸ ローカルでは全サービスを1つに まとめることで、起動を楽にしている ▸ cURLで雑デバッグしたいときも ポート番号を1つだけ覚えておけばいい ▸ デプロイ時は 論理的なサービス = 物理的なサービス
  21. © 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 を生成する関数を各サービスに生成して、
  22. © LayerX Inc. 28 バクラクでの Connect 応用例: ローカルでの起動を楽にする var ServiceHandlerOptions

    = map[string]func() ([]connect.Option, error){ tenantv1connect.TenantServiceName: tenant_v1_tenant.HandlerOptions, // 全サービスの HandlerOptions 関数を突っ込んでおく } 全サービスの HandlerOptions 関数を突っ込んだ map をコード生成する あとはこれを使って Connect server を作るだけ
  23. © 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
  24. © LayerX Inc. 31 全サービスを1つにまとめると、ビルドはどんどん重くなる バクラクでの Connect 応用例: ローカルでの起動を速くする ▸

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

    ▸ メインの port を listen してるプロセスがリクエストを受けたら、 ‐ 対象のサービスをビルドし ‐ Unix domain socket を listen する形で起動して ‐ そこにリクエストを流す! ▸ 最小限しかビルドしないので速い! ▸ これも go-all と似たような感じで実装を生成する
  26. © LayerX Inc. 34 ローカルでは1つのHTTP serverに 全Connect serverを乗せる バクラクでの Connect

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

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

    応用例: デプロイをさらに楽にする TenantService NotificationService CardTransactionService ▸ デプロイ時は 論理的なサービス == 物理的なサービス 👉 …じゃなくてもいいのでは?
  29. © LayerX Inc. 38 複数の論理的なサービスを1つのデプロイメントにまとめる バクラクでの Connect 応用例: デプロイをさらに楽にする TenantService

    CardTransactionService NotificationService ▸ 非機能要件面で分ける理由がない場合は、 1つのデプロイメントに 複数の論理的なサービスを乗せることも可 WorkflowService
  30. © 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 活用例
  31. © LayerX Inc. 42 まとめ もともとのモチベーション ▸ スキーマ駆動自体は gRPC でもできる

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