Closed15

BufやConnectといった最近のgRPC 開発について調べるメモ

ぱんだぱんだ

公式ドキュメントを読む

イントロ

ProtobufはREST/JSONで開発するAPIよりも多くの利点あるがProtobufを使用して開発するにはなかなか楽ではない。Bufは開発者がアプリケーションロジックに集中できるようにProtocbufまわりの関心ごとを引き受けツールとして公開してくれている。具体的にはBuf CLIBuf Schema Registoryがある。

従来のProtocbufを使用したAPI開発には以下のような課題がある。

  • API設計に一貫性がない。(あんまりピンときてない)
  • 依存関係 ファイル間の依存関係の解決にコピペでやるしかなくバグを引き起こしやすい。これはnpmなしでJavaScriptを書くようなもの
  • 前方互換性と後方互換製を強制できてない
  • スタブの配布が難しい 生成されたコードを配布するか、全てのクライアントがprotocを独立して実行する必要がある。これはprotocbufを採用した開発のハードルを一気に上げる
  • ツールのエコシステムが限られている

Buf CLI

以下のような特徴がある

  • 高性能なProtocbufコンパイラ
  • Linter機能
  • 互換性を強制する変更検出機能
  • 設定可能なテンプレートに基づいてprotocプラグインを呼び出すコードジェネレーター

BSR(Buf Schema Registry)

BSRはProtobuf APIのホスト型SaaSプラットフォームのこと。BSRはProtobufエコシステムに依存関係管理を導入する。また、リモートプラグインやSDK生成の機能によりクライアントコードの生成に関する工程をシンプルにする。

install

Homebrew, npm, dockerなどがあるが今回はHomebrewでインストール。

brew install bufbuild/buf/buf

% buf --version
1.29.0
ぱんだぱんだ

Buf CLI Tutorial Tour

ドキュメント記載のリポジトリをクローンする

% git clone https://github.com/bufbuild/buf-tour

% cd buf-tour/start/getting-started-with-buf-cli/proto

% tree .
.
├── google
│   └── type
│       └── datetime.proto
└── pet
    └── v1
        └── pet.proto

bufはbuf.yamlが設定ファイルとなっている。以下のコマンドでファイルを生成する。

% buf mod init

% cat buf.yaml 
version: v1
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT

buf.yamlはprotoファイルがある場所に配置したほうがいい。protoファイルからprotc互換のコード生成を実行するのにbuf.gen.yamlをprotoディレクトリがある場所に配置する。buf.gen.yamlbuf generateコマンドが指定されたモジュールでどのようにprotocプラグインを実行するかを制御する。

% touch buf.gen.yaml

% cat <<'EOF' > buf.gen.yaml 
version: v1
managed:
  enabled: true
  go_package_prefix:
    default: github.com/bufbuild/buf-tour/gen
plugins:
  - plugin: buf.build/protocolbuffers/go
    out: gen
    opt: paths=source_relative
  - plugin: buf.build/connectrpc/go
    out: gen
    opt: paths=source_relative
EOF

以下のコマンドでコードを自動生成

buf generate proto

protocの複雑なコマンド実行をしなくてよいのがbufの強力なところ。bufの場合はbuf.gen.yamlに全て記載される。

このようにbufはprotocの代替となる優れた簡素化されたコンパイラだが単なるProtobufコンパイラではない。buf CLIはbuf lintコマンドでlint機能を提供している。

% buf lint proto
proto/google/type/datetime.proto:17:1:Package name "google.type" should be suffixed with a correctly formed version, such as "google.type.v1".
proto/pet/v1/pet.proto:42:10:Field name "petID" should be lower_snake_case, such as "pet_id".
proto/pet/v1/pet.proto:47:9:Service name "PetStore" should be suffixed with "Service".

上記の実行結果はいくつかlintルールに引っかかっている。これはbuf.yamlに記載されたDEFAULTルールに基づく。

buf.yaml
version: v1
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT

FIELD_LOWER_SNAKE_CASESERVICE_SUFFIXのルールを修正していく

proto/pet/v1/pet.proto
syntax = "proto3";

package pet.v1;

...

message DeletePetRequest {
-  string petID = 1;
+  string pet_id = 1;
}

message DeletePetResponse {}

-service PetStore {
+service PetStoreService {
  rpc GetPet(GetPetRequest) returns (GetPetResponse) {}
  rpc PutPet(PutPetRequest) returns (PutPetResponse) {}
  rpc DeletePet(DeletePetRequest) returns (DeletePetResponse) {}
}

残りの1つの警告を見ていく

proto/google/type/datetime.proto:17:1:Package name "google.type" should be suffixed with a correctly formed version, such as "google.type.v1".

これはpackage名のsuffixにバージョンを指定する必要がありそうだが、datetime.protoはプロジェクトファイルではなくgoogleapisが提供している依存関係である。そのため、lintを通すためにパッケージ名を修正することもできないためlintの対象外とする

proto/buf.yaml
 version: v1
 breaking:
   use:
     - FILE
 lint:
   use:
     - DEFAULT

+  ignore:

+    - google/type/datetime.proto

以下のコマンドを実行して無視したいルールの最小セットを出力することもできるので、一旦これをbuf.yamlに記載して、後で直すということもできる。ただし、経験上これをやって後で直すことはない。

% buf lint proto --error-format=config-ignore-yaml
version: v1
lint:
  ignore_only:
    PACKAGE_VERSION_SUFFIX:
      - google/type/datetime.proto

次にbuf breakingコマンドによる変更検知の機能について。protobufの破壊的変更がある場合、影響範囲がそこまで広くなければやってしまったほうがいいとも考えられるが、影響範囲が広かったりすると破壊的変更はなるべく避けたいと思うかもしれない。いずれにせよ、そういった互換性のない変更を検知することでレビュー負荷はかなり下がります。

変更検知の単位はFILEPACKAGEWIREWIEW_JSONとあるがデフォルトはファイル単位で検知するFILEです。

コマンドを実行して変更検知をためすためにpet.protoを以下のように修正する。

pet.proto
 message Pet {

-  PetType pet_type = 1;

+  string pet_type = 1;

  string pet_id = 2;
  string name = 3;
}

以下のコマンドで変更値。比較しているのはgitディレクトリ内のファイルを指定することで以前のバージョンとの比較を実施している。

% buf breaking proto --against "../../.git#subdir=start/getting-started-with-buf-cli/proto"
proto/pet/v1/pet.proto:1:1:Previously present service "PetStore" was deleted from file.
proto/pet/v1/pet.proto:18:3:Field "1" on message "Pet" changed type from "enum" to "string".
proto/pet/v1/pet.proto:35:28:Field "1" with name "pet_id" on message "DeletePetRequest" changed option "json_name" from "petID" to "petId".
proto/pet/v1/pet.proto:35:35:Field "1" on message "DeletePetRequest" changed name from "petID" to "pet_id".

以下のようなGoのプログラムを動かしてみる。

main.go
package main

import (
  "context"
  "fmt"
  "log"
  "net/http"
  petv1 "github.com/bufbuild/buf-tour/gen/pet/v1"
  "github.com/bufbuild/buf-tour/gen/pet/v1/petv1connect"
  connect "connectrpc.com/connect"
  "golang.org/x/net/http2"
  "golang.org/x/net/http2/h2c"
)

const address = "localhost:8080"

func main() {
  mux := http.NewServeMux()
  path, handler := petv1connect.NewPetStoreServiceHandler(&petStoreServiceServer{})
  mux.Handle(path, handler)
  fmt.Println("... Listening on", address)
  http.ListenAndServe(
    address,
    // Use h2c so we can serve HTTP/2 without TLS.
    h2c.NewHandler(mux, &http2.Server{}),
  )
}

// petStoreServiceServer implements the PetStoreService API.
type petStoreServiceServer struct {
  petv1connect.UnimplementedPetStoreServiceHandler
}

// PutPet adds the pet associated with the given request into the PetStore.
func (s *petStoreServiceServer) PutPet(
  ctx context.Context,
  req *connect.Request[petv1.PutPetRequest],
) (*connect.Response[petv1.PutPetResponse], error) {
  name := req.Msg.GetName()
  petType := req.Msg.GetPetType()
  log.Printf("Got a request to create a %v named %s", petType, name)
  return connect.NewResponse(&petv1.PutPetResponse{}), nil
}

プログラムを起動したらbuf curlコマンドでリクエストを送ってみる。もうgrpcurlはいらない。

 % buf curl \
  --schema proto \
  --data '{"pet_type": "PET_TYPE_SNAKE", "name": "Ekans"}' \
  http://localhost:8080/pet.v1.PetStoreService/PutPet
{}

チュートリアルは以上。
Buf CLIのおかげでprotocもgrpcurlもclang-formatもいらないし、lintと互換性チェックまでできる。Bufすげー

ぱんだぱんだ

BSR(Buf Schema Registry)のチュートリアルツアー

BSRにProtobufモジュールをpushするためにBufアカウントにサインアップする。BSRにログインしたらトークンを作成する。作成したら以下のコマンドでログインする。

buf registry login

ユーザー名と作成したトークンを入力しログイン。ログインができたらBSRに新たにリポジトリを作成しサンプルのprotobufモジュールをpushする。

リポジトリが作成できたらbuf.yamlnameフィールドを追加する。

buf.yaml
version: v1

name: buf.build/<USERNAME>/petapis

breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT

以下のコマンドでprotobufモジュールをpushする。

buf push

protobufモジュールをpushすると各ファイルごとにドキュメントとしての役割も果たしますが、モジュール自体を説明するドキュメントはない。これはbuf.mdを作成して再度buf pushすることでモジュール自体の説明を加えることができる。GitHubのREADMEのようなもの

次に依存関係の解決方法を見ていく。googleapisで公開されているprotobufモジュールが用意されているが今までこれはローカルにファイルをコピペして配置していた。これは言うまでもないがモジュールの更新にも追従できずに非常にやっかいだ。

これをBSRを使用することで解決することができる。チュートリアルで用意されているgoogleapisのコピーしてきたファイルを削除し、buf.yamlに以下のように依存関係の記載を追記する。

 deps:
   - buf.build/googleapis/googleapis

追記できたらビルドを実行すると以下のような警告が出る。

% buf build
WARN	Specified deps are not covered in your buf.lock, run "buf mod update":
	- buf.build/googleapis/googleapis
pet/v1/pet.proto:5:8:read google/type/datetime.proto: file does not exist

これはbuf.lockファイルにない依存関係が指定されたことで警告が出ています。buf.lockファイル自体ないので以下のコマンドを実行する。

buf mod update

これでbuf.lockファイルが作成される。これでビルドが通るにようになる。

ビルドは実際のファイルとして出力されるわけではなくメモリ上に保存される。このビルドイメージはこのあとのコード生成などに使われることになる。

次にコード生成。googleapisの依存関係をBSRで公開されているモジュールに置き換えたのでbuf.gen.yamlgoogleapisは指定する必要がある。

buf.gen.yaml
version: v1
managed:
  enabled: true
  go_package_prefix:
    default: github.com/bufbuild/buf-tour/gen
    except:
      - buf.build/googleapis/googleapis
plugins:
  - plugin: buf.build/protocolbuffers/go
    out: gen
    opt: paths=source_relative
  - plugin: buf.build/bufbuild/connect-go
    out: gen
    opt: paths=source_relative

これでコード生成すると依存関係はBSRで管理されているため、googleapisから生成されたコードは含まれない。BSRに公開したモジュールは普通にgo getできる。

go get buf.build/gen/go/junichi-y/petapis/protocolbuffers/go
go get buf.build/gen/go/junichi-y/petapis/connectrpc/go

クライアントのコードは以下のような感じ

client.main.go
package main

import (
  "context"
  "log"
  "net/http"

  // Replace <USERNAME> with your BSR username if username isn't present
  "buf.build/gen/go/junichi-y/petapis/connectrpc/go/pet/v1/petv1connect"
  petv1 "buf.build/gen/go/junichi-y/petapis/protocolbuffers/go/pet/v1"
  connect "connectrpc.com/connect"
)

func main() {
  client := petv1connect.NewPetStoreServiceClient(
    http.DefaultClient,
    "http://localhost:8080",
  )
  res, err := client.PutPet(
    context.Background(),
    connect.NewRequest(&petv1.PutPetRequest{
      PetType: petv1.PetType_PET_TYPE_SNAKE,
      Name:    "Ekans",
    }),
  )
  if err != nil {
    log.Println(err)
    return
  }
  log.Println(res.Msg)
}

チュートリアルは以上
BSRを使うことでprotoc-gen-docもクライアントの人にprotocによるコード生成をお願いしたりする必要がなくなる。Bufすげー

ぱんだぱんだ

Buf CLI

protocを使用したコード生成の課題の一つにprotocやプラグインとの連携の複雑さがある。さまざまなコンパイラやプラグインのバージョンが複雑に絡み合い1台のマシンで安定した環境をローカルに管理・維持することは困難である。チームメンバー間でこれを共有するために酷いbashファイルやMakefileを利用することになる。Buf CLIはこれを解決する

  • protocより2倍速くコンパイルできる
  • ビルド設定を一回すればいい。複雑なコマンドやオプションを覚える必要はない
  • protocよりも柔軟。protocは.protoファイルしか受け付けない。

さらに、BufはProtobufファイルからクライアントコードを生成する際の多くのフラストレーションを軽減する。

  • リモートプラグイン ローカルにプラグインを用意する必要はない。
  • マネージドモード .protoファイルからユーザーや言語固有のProtobufオプションを削除することができる。
  • 生成されたSDK BSRを使用することでコードを生成する必要はない。生成されたSDKをnpmなどの依存関係ツールを使用してモジュールを使用することができる。

protoc-gen-goみたいなローカルプラグインを使う場合、各開発者が自分のマシンに用意することになるので容易に私だけ動かない状態になる。Bufを使うことでBSRに存在するprotocプラグインを使用することができるのでチーム間でのコード生成において一貫性を保てる

以下のような言語固有のoptionをマネージドモードを有効にすることで削除することができる。

syntax = "proto3";

package acme.weather.v1;

option java_multiple_files = true;
option java_outer_classname = "WeatherProto";
option java_package = “com.acme.weather.v1”;
ぱんだぱんだ

Breakeing Change

protoファイルの破壊的変更検知。protoファイルの互換性を維持するために破壊的変更は基本的に避けた方が良い。ただし、影響範囲がそこまで広くなく、クライアント側とちゃんと連携できているのであれば破壊的変更をしてしまったほうがいい場合もある。いずれにせよ破壊的変更を検知できる仕組みが重要である。

破壊的変更検知は以下の3つのタイミングで実行できる。

  • 開発中 エディタと統合することで開発中に早いサイクルで検知できる
  • CI/CD GitHub ActionsのようなCI/CDで実行することができる
  • BSR BSRにpushする際に破壊的変更を検知できる
ぱんだぱんだ

Lint

lintをかけるタイミングは2箇所。

  • 開発中 エディタ統合
  • コードレビュー中 CI/CD(GitHub Actionsなど)

lintのルールはBuf側で用意された以下の3つのレベルを使用することができる。ルールの厳しさは上から下にかけてゆるくなる。

  • DEFAULT
  • BASIC
  • MINIMAL

このルールとは別に以下の2つの制約が用意されている。

  • COMMENTS Protobufスキーマのさまざまな部分にコメントを強制する
buf.yaml
version: v1
lint:
  use:
    - DEFAULT
    - COMMENT_ENUM
    - COMMENT_MESSAGE
  • UNARY_RPC ストリーミングRPCを禁止する

lintはローカルのProtobufファイルだけでなくGitリポジトリやBSRなどを指定することもできる。lintのルールはexceptなどを使いカスタマイズすることもできるし、運用していて途中で導入する場合にlintの警告を無視したい場合などにignoreのようなオプションを使うこともできる。

これはbuf.yamlに以下のような感じで記載する。

version: v1
lint:
    use:
        - DEFAULT
    except:
        - FILE_LOWER_SNAKE_CASE
    ignore:
        - bat
        - ban/ban.proto
    ignore_only:
        ENUM_PASCAL_CASE:
            - foo/foo.proto
            - bar
        BASIC:
            - foo
    enum_zero_value_suffix: _UNSPECIFIED
    rpc_allow_same_request_response: false
    rpc_allow_google_protobuf_empty_requests: false
    rpc_allow_google_protobuf_empty_responses: false
    service_suffix: Service
    allow_comment_ignores: true
ぱんだぱんだ

Buf curl

以下のようにJSON形式でパラメーターを指定してリクエストすることができる。

% buf curl \
    --data '{"sentence": "I feel happy."}' \
    https://demo.connectrpc.com/connectrpc.eliza.v1.ElizaService/Say
{
  "sentence": "Feeling happy? Tell me more."
}

デフォルトのRPCプロトコルはConnect。別のプロトコル、gRPC、gRPC-Webを使用するには-protocolフラグを使用する。

リクエストメタデータは-Hまたは--headerフラグを使用して指定する。

Buf CLIはデフォルトでサーバーリフレクションを対応していることを期待します。もし、サーバーがリフレクションを対応していなければ--schmaフラグを使用してスキーマを指定することができる。

ぱんだぱんだ

Build

buf buildコマンドを実行することでProtobufファイルをビルドする。ビルドはbuf.yamlの設定を考慮して実行される。buf.yamlの設定項目は以下。

version: v1
build:
  excludes:
    - foo/bar

Bufの考えではディレクトリごとに分かれ、buf.yamlが配置されている単位でモジュールとみなしている。複数のモジュールを管理するのにBufはbuf.work.yamlを使用したワークスペースをサポートしている。protocにおいては以下のように複数のディレクトリを-Iオプションで指定しているのと同義である。

protoc \
    -I proto \
    -I vendor/protoc-gen-validate \
    -o /dev/null \
    $(find proto -name '*.proto')

buf buildコマンドは以下の処理を行う。

  • buf.yaml 設定に従ってすべての Protobuf ファイルを検出する。
  • Protobufファイルをメモリにコピーする。
  • すべてのProtobufファイルをコンパイルする。
  • コンパイル結果を設定可能な場所に出力する(デフォルトは/dev/null)。

デフォルトではコンパイル結果を捨ててしまうが-oオプションを指定することで以下の形式で出力できる。

  • JSON
  • text
  • binary
buf build -o image.binpb
buf build -o image.binpb.gz
buf build -o image.binpb.zst
buf build -o image.json
buf build -o image.json.gz
buf build -o image.json.zst
buf build -o image.txtpb
buf build -o image.txtpb.gz
buf build -o image.txtpb.zst
ぱんだぱんだ

BSR(Buf Schema Registry)

BSRはProtobufをチーム内でいい感じに配布するためのレジストリサービス。Docker Hubのようなもの。Protocbufを使用したチーム開発ではクライアントとサーバー間でしばしば食い違う。BSRにProtocbufモジュールを公開しておくことで容易に配布することができるようになった。

また、依存関係の解決も可能となる。現代のプログラミング言語にはいわゆるパッケージマネージャーのようなものが必須であることが多い。JSのnpm、GoのGo モジュール。もうコピペをする必要はない。

そして、これが個人的には嬉しいことだがドキュメントの公開という役割もある。従来であればprotoc-gen-docのようなprotocプラグインを使用してドキュメントを自動生成していたがもうそれも不要になる。

Bufでは意味あるProtobufファイルの集まりをモジュールとして扱い、そのルートパスにはbuf.yamlが配置されることが期待されている。モジュール名はbuf.yamlに記載される。

buf.yaml
version: v1
name: buf.build/acme/weather

nameの値は{Remote}/{Owner}/{Repository}の形式になる。RemoteはBSRのホスティングサーバーのDNSで常にbuf.buildになる。

モジュールの最小構成は以下。

proto/
├── acme
│   └── pkg
│       └── v1
│           └── pkg.proto
├── buf.lock
├── buf.md
├── buf.yaml
└── LICENSE

BSRを使用する時の認証の仕組みとして

  • BUF_TOKEN環境変数を使用する。(CI環境推奨)
  • .netrcファイルを使用する。(開発環境推奨)

がある。いずれにせよBSRによって生成する認証トークンを使用する。認証の優先順位は.netrcファイルよりも環境変数の方が上。環境変数による認証は以下のように複数のトークンを含めることができる。

export BUF_TOKEN=${TOKEN1}@{REMOTE1},${TOKEN2}@{REMOTE2},...

開発環境で認証する場合、以下のようにloginコマンドを実行すればいい。

buf registry login

BSRにProtobufモジュールを公開するとしてGitHubのように複数のブランチがないと開発中のモジュールを本番運用中のモジュールに統合せずに公開するのが難しい。

そのため、BSRはブランチ機能をサポートしている。GitHub Actionsでbuf-push-actionを使用すればいい感じにBSRへのモジュール公開をやってくれる。

name: buf-push
on: push # Apply to all pushes
jobs:
  push-module:
    # Run `git checkout`
    - uses: actions/checkout@v3
    # Install the `buf` CLI
    - uses: bufbuild/buf-setup-action@v1
    # Push the module to the BSR
    - uses: bufbuild/buf-push-action@v1
      with:
        buf_token: ${{ secrets.BUF_TOKEN }}
        # Push as a branch when not pushing to `main`
        branch: ${{ github.ref_name != 'main' }}

公開してあるProtobufモジュールをリモートで参照しコードを自動生成することも可能だが、自動生成後の各言語のコードを直接パッケージ管理ツールで使用することができる。JSであればnpmモジュールとしてだしGoであればGoモジュールとして。

BSRはGitHubのようにタグ管理もサポートしている。以下コマンドでGitのコミットとBSRのモジュールのバージョンを紐づけることができる。

buf push --tag "$(git rev-parse HEAD)"

Protocbufモジュールは他のモジュールを依存関係として追加することができる。これはbuf modコマンドを使用したり、buf.yamlファイル内に依存関係を追加して管理する。その際に、現状の依存関係を固定し再現するためにbuf.lockファイルが作成される。

Buf Studioを使えばweb画面から特定のRPCと対話できる。認証情報もつけれる。

ぱんだぱんだ

gRPC

BufではないけどgRPCについて今一度確認しておく。

gRPCとはgoogleが2015年に開発したオープンソースのRPC。もともとgoogleが社内で使用していた独自のRPCにHTTP2などを組み合わせて標準化したもの。

そもそもRPCとはRemote Procedure Callの略でクライアント、サーバー間の通信プロトコルの一種でありクライアントからサーバー側で登録されている関数をリモートで呼び出すような技術。RPCは古くから存在しておりさまざまなデータフォーマットで利用されている。例えば、XMLを利用するXML-RPCやJSONを利用するJSON-RPCがある。

こういった、 RPCの技術をどうやって使えるのかというとGoで言うならnet/rpcといった標準パッケージが容易されており、サーバー側もクライアント側も実装できるようになっている。

ChatGPTにexampleを出してもらった。

GoによるJSON-RPCの実装例
// サーバーサイド
package main

import (
    "net"
    "net/rpc"
    "net/rpc/jsonrpc"
    "log"
)

type Args struct {
    A, B int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error {
    *reply = args.A * args.B
    return nil
}

func main() {
    arith := new(Arith)
    rpc.Register(arith)

    listener, e := net.Listen("tcp", ":1234")
    if e != nil {
        log.Fatal("listen error:", e)
    }

    for {
        conn, e := listener.Accept()
        if e != nil {
            continue
        }
        go jsonrpc.ServeConn(conn)
    }
}

// クライアントサイド
package main

import (
    "net/rpc/jsonrpc"
    "log"
    "fmt"
)

type Args struct {
    A, B int
}

func main() {
    client, err := jsonrpc.Dial("tcp", "localhost:1234")
    if err != nil {
        log.Fatal("dialing:", err)
    }

    args := Args{7, 8}
    var reply int
    err = client.Call("Arith.Multiply", args, &reply)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d*%d=%d", args.A, args.B, reply)
}

gRPCもJSON-RPCやXML-RPCと同様、RPCという通信プロトコルの1種であるが特徴としてHTTP2通信を使用している点とデータのシリアライズにProtocol Buffersを使用していることが特徴。厳密には通信プロトコルとデータのシリアライズ方法は他のものに置き換え可能らしいがHTTP2、Protocol Buffersのデフォルトの組み合わせで使用されることが多く、その組み合わせをgRPCとして指すことが多い、ように感じる。

gRPCが流行ったのはGoogleが社内で利用していたマイクロサービスのサービス間通信にgRPCの前身となるようなRPC通信が使用されていて、マイクロサービス自体が流行り出したときに注目されたのかなという印象。マイクロサービスとセットで話題になることが多いがモバイルアプリのクラサバ通信に使われたりするのもよく見かける。

gRPCは今までのRPC通信がテキストベースでデータの通信効率が悪く、バイナリデータを扱いにくかったという課題を解決しており、特にデータの転送効率という面が優れており、無数のマイクロサービス内のサービス間通信にまさに適した通信プロトコルとなっている。

gRPCの開発体験についてスキーマ駆動開発の良さがある。これは現場によっては複数のクライアントとサーバー間でAPIのすり合わせをすることのハードルをかなり下げることにつながっていると思っている。gRPCというよりもProtobufの体験の良さが関係しているともいえる。

いずれにせよ、Protobufを用いたスキーマ駆動の開発はクライアントとサーバー間の摩擦をなくし、いい感じに開発できて個人的には気に入っている。protocの複雑さやドキュメント化の問題もBufが解決してくれたように思える。

ちなみにgRPCはCNCF傘下で開発が進められている。

公式
https://grpc.io/

ぱんだぱんだ

Protocol Buffers

上述したgRPCで使用されているデータシリアライゼーション。実際は.protoファイルで作成されるIDL、protocのようなコンパイラが生成するコード、言語固有のランタイム・ライブラリなどの組み合わせを指す。

ProtobufがサポートしているのはC++, C#, Java, Kotlin, Objective-C, PHP, Python, Rubyの8言語。GoとDartはGoogleがサポートしており、実際はprotcのプラグインとして提供されている。それ以外の言語もGithubリポジトリでカスタムプラグインとして公開されているものがある。

https://github.com/protocolbuffers/protobuf/blob/main/docs/third_party.md

公式

https://protobuf.dev/

ちなみに、protoファイルでgoogle/protobuf/any.protoのようなインポートができて、使用できるようになっているがこれはprotobufの標準モジュールとして用意されている。

以下の記事でProtobufがなぜいいのかをかなり深く書いている

https://qiita.com/yugui/items/160737021d25d761b353

ぱんだぱんだ

Connect

ConnectはBufが開発しているブラウザとgRPC互換のHTTP APIを構築するためのライブラリ群である。Connectは複数のプロトコルをサポートしており、gRPCgRPC-webと独自のConnectプロトコルをサポートしている。

対応言語は執筆時点でGo, TS,(JS) Swift, Kotlinが対応されており、GoとTSはBuf自体でも本番運用されており安定している。

他の言語サポートにおいてはConnectはHTTP/1.1でもUnary RPCが動作するように設計されているためcurlやweb標準のfetchでリクエストできるためこれらを利用することもできる。しかし、他のサポート言語のような完全な型安全性は得られない点には気をつけたい。

# curl
curl --header "Content-Type: application/json" \
    --data '{"sentence": "I feel happy."}' \
    https://demo.connectrpc.com/connectrpc.eliza.v1.ElizaService/Say

# fetch
fetch("https://demo.connectrpc.com/connectrpc.eliza.v1.ElizaService/Say", {
  "method": "POST",
  "headers": {"Content-Type": "application/json"},
  "body": JSON.stringify({"sentence": "I feel happy."})
})
  .then(response => { return response.json() })
  .then(data => { console.log(data) })

詳しくはこちら

https://connectrpc.com/docs/curl-and-other-clients

以下はGoのConnectを使用したサーバー側の実装例。

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"

    "connectrpc.com/connect"
    "golang.org/x/net/http2"
    "golang.org/x/net/http2/h2c"

    greetv1 "example/gen/greet/v1" // generated by protoc-gen-go
    "example/gen/greet/v1/greetv1connect" // generated by protoc-gen-connect-go
)

type GreetServer struct{}

func (s *GreetServer) Greet(
    ctx context.Context,
    req *connect.Request[greetv1.GreetRequest],
) (*connect.Response[greetv1.GreetResponse], error) {
    log.Println("Request headers: ", req.Header())
    res := connect.NewResponse(&greetv1.GreetResponse{
        Greeting: fmt.Sprintf("Hello, %s!", req.Msg.Name),
    })
    res.Header().Set("Greet-Version", "v1")
    return res, nil
}

func main() {
    greeter := &GreetServer{}
    mux := http.NewServeMux()
    path, handler := greetv1connect.NewGreetServiceHandler(greeter)
    mux.Handle(path, handler)
    http.ListenAndServe(
        "localhost:8080",
        // Use h2c so we can serve HTTP/2 without TLS.
        h2c.NewHandler(mux, &http2.Server{}),
    )
}

このようなConnectを使用したサーバーに対してcurlのようなHTTP通信もできるし、grpcurlのようなgRPC通信もできるし、protocプラグインであるprotoc-gen-connect-goを使用して生成したクライアントコードを使って通信もできる。

今までのRESTのようなHTTPベースのAPI開発で使用されるようなHTTP HeaderやInterceptor、エラーハンドリングなどもちゃんと実装方針が示されている。詳しくは以下公式を

Connect for Go
https://connectrpc.com/docs/go/getting-started

Connect for web
https://connectrpc.com/docs/web/getting-started

Connectを採用することで今まで通りgRPCを対応しつつ、ConnectやgRPC-webの対応をすることができwebクライアントとのProtobufベースの通信を容易にする。モバイルアプリのサーバーとしてgRPCを採用するケースならば今まで通りgRPCでも問題ないかもしれないが、web側との通信もProtobufを使用したスキーマファーストな開発をしたいというニーズがあれば Connectは採用する価値があるかもしれない。

OpenAPIを利用したクラサバ通信と比較するべき技術かもしれない。

ぱんだぱんだ

gRPC-web

gRPC公式のチュートリアル

https://grpc.io/docs/platforms/web/basics/

gRPCをwebブラウザ環境で使うためのもの。gRPCをwebブラウザ環境上で使う場合、特別なプロキシサーバーを間に挟んでやり取りする必要がある。これはgRPCがHTTPプロトコル仕様のトレーラを多用しているため。トレーラはHTTPプロトコルの仕様に存在するものの、ほとんど使用されておらずWebブラウザを含む多くのHTTP実装がサポートしていない。そのためgRPCチームはレスポンスボディの最後にトレーラをエンコードするgRPC-webプロトコルを導入した。

connect-goで構築されたようなgRPCサーバーはgRPC-webプロトコルをネイティブにサポートしているのでブラウザはサーバーと直接通信することができるが、GoogleのgRPC実装のほとんどはgRPC-webをサポートしていないため標準のgRPCプロトコルとの間で変換するためのプロキシサーバーを必要とし、多くの場合Envoyがプロキシサーバーとして使用される。

ConnectのFAQに詳細あり

https://connectrpc.com/docs/faq#why-do-i-need-a-proxy-to-call-grpc-backends

CloudflareのgRPC対応のブログになぜgRPC-webが必要なのかが書いてあった

https://blog.cloudflare.com/road-to-grpc-ja-jp

ぱんだぱんだ

grpc-gateway

gRPCとRESTの両方を対応するためにgRPC-webの他にgrpc-gatewayを使う方法もある。gRPC-webはwebブラウザがgRPCサーバーと通信できるようにプロキシサーバーを立てて通信したが、grpc-gatewayはprotcプラグインを使用してプロキシサーバー用のコードを自動生成しクライアントとgRPCサーバーの間に入る。grpc-gatewayはGoにしか対応していないっぽいがprotoc-gen-openapiv2などを使用することでOpenAPI対応のREST APIをgRPCと併用してサーバーを構築することなんかもできる。

以下の記事が参考に

https://future-architect.github.io/articles/20220624a/

grpc-gatewayの嬉しいところがよくまとまっている
gRPCとOpenAPI定義をProtobuf一つで管理できるのともろもろのコードを自動生成できるのがいいとのこと

https://zenn.dev/pranc1ngpegasus/articles/2fea24c58614be

ぱんだぱんだ

Envoy

gRPCやProtobufとは直接関係ないけどgRPC-webを利用するのに必要なEnvoy。Envoy自体はマイクロサービスやk8sと同じ文脈で聞くことが多い。nginxと同様OSSのリバプロサーバーを提供するがEnvoyはマイクロサービスのアプリケーションコンテナのサイドカーとして必要な機能を多く含んでいる。

gRPC-webではgRPC非対応のwebブラウザとgRPCサーバー間の通信を可能とするために使用される。

https://qiita.com/seikoudoku2000/items/9d54f910d6f05cbd556d

ちなみにマイクロサービスの文脈でEnvoyと一緒によく聞くIstionはマイクロサービス上でEnvoyをいい感じに扱うのに使われるものらしいのでgRPCでIstioは特に関係ない。

このスクラップは2024/02/06にクローズされました