理系学生日記

おまえはいつまで学生気分なのか

gRPC サーバを golang でつくるときの基本を押さえる

gRPC サーバを作ることになったので、まずは基本的なところを押さえようと思います。 いろいろ調べつつ書いているのですが、gRPC 初心者なので、間違ったところがあれば指摘していただきたいです。

gRPC の種類

gRPC の種類としては、以下の 4 つがあります。

  1. Unary RPC
  2. Server streaming RPC
  3. Client streaming RPC
  4. Bidirectional streaming RPC

Unary RPC が、1 リクエスト 1 レスポンス型のよく見る API コールです。Rest API なんかに相当するのがこれですね。

では、他の種類はどういうことなんだというと、複数のリクエストあるいはレスポンスを一度に返却できるというタイプのものです。 例えば、指定されたファイルのメタ情報を返却する ls-l という RPC を公開するとして、複数のファイルの情報は一度に取得したいですよね。 こういうときはリクエストを一度に複数送り、それに対応するレスポンスを複数返却する、4. の Bidirectional streaming RPC なんかがハマると思います。

この ls-l の例だと、まだリクエストとレスポンスが 1:1 対応しますが、もちろん 1:多なんてのもできます。 複数の解を持つ微分方程式を解くサービスを公開する場合は発見した解を順次クライアントに返却するので、2. Server streaming RPC が使えると思います。

.proto からのサーバ・ソースの自動生成

例えば、1. の Unary RPC と 4. の Bidirectional streaming RPC の IDL を作ってみます。 gRPC のサービスは、.proto ファイルを IDL として定義します。今回は RPC を 2 つ持つ以下のような Greeter サービスを作ってみました。

  • SayHello: 一人の名前を受け取って挨拶する RPC (Unary RPC)
  • SayHelloToMany: 複数人の名前を受け取って全員に挨拶する RPC (Bidirectional streaming RPC)
syntax = "proto3";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}

  // Sends greetings to many people.
  rpc SayHelloToMany (stream HelloRequest) returns (stream HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

読みやすいのでだいたい分かると思いますが、stream という語をリクエストやレスポンスに付けることで、 それらが streaming に対応していることが示せます。

IDL からは、protoc コマンドと各言語のプラグインを使用してサーバとクライアントのソースを自動生成することができます。 今回は、golang を使うので、以下のような形でコンパイルします。

$ protoc -I. helloworld.proto --go_out=plugins=grpc:helloworld

これで、helloworld ディレクトリに helloworld.pb.go という golang のソースが生成されます。

gRPC におけるエラー処理

gRPC においては、エラーは status オブジェクトで返却することになっています。 この status オブジェクトは、エラーの種類を示す code と、どういうエラーかを示す message から構成されます。 どういう code が定義されているのか、gRPC のライブラリ/アプリケーションのどちらがが返すのかは、 Status codes and their use in gRPC に整理されています。

さて、これをどうやって golang に落としていくかですが、これは RPC Errors がまとまっています。 gRPC サーバからのエラーは status.Status を返却することになっています。 ここで表現される Status はもちろん code を内包しており、それは codes.Code で定義されています。

というわけで、エラーが発生した場合は

st := status.New(codes.NotFound, "some description")
err := st.Err()

とか

err := status.Error(codes.NotFound, "some description")

といった形で error を生成し、それを gRPC のメソッドハンドラから返却することになります。

参考

メタデータ

リクエストパラメータやレスポンスパラメータには出現しない付加的な情報については、gRPC の metadata という概念で表されます。 用途とともに HTTP ヘッダと似たもので、基本的には key-value のペアになります。サーバ実装上でどういう風にアクセスできるかはもちろん言語に依るのですが、 golang の場合は metadata の中で

type MD map[string][]string

と定義されています。いわゆる MultiMap ですね。

サーバからは、context.Context 経由で metadata にアクセスできます。

// retrieve metadata
md, _ := metadata.FromIncomingContext(ctx)
postscripts := md.Get("postscript")

また、同様にレスポンスに書き込むこともできます。

// send reply with metadata
grpc.SendHeader(ctx, metadata.Pairs("postscript", ps))

参考文献

リフレクション

gRPC には Server Reflection Protocol というプロトコルが定義されています。 これはクライアントが実行時に動的に gRPC の情報を得られるようにするというものです。 ここで言っている情報というのは、以下のようなものです。

  1. gRPC サーバがどういうメソッド(RPC)を公開しているのか
  2. メソッドのリクエスト/レスポンスのインタフェースがどうなっているのか

結果としてクライアントへ事前に .proto を渡さなくても、クライアントが gRPC を理解できるようになります。 元々は、CLI ツールから gRPC を呼び出せるように、という目的だそうです。 このあたりの CLI ツールについては、たぶん次のエントリで書くことになります。

The primary usecase for server reflection is to write (typically) command line debugging tools for talking to a grpc server.

reflection の有効化については、reflection モジュールを使えば 1 行で済みます。

// in main()

grpcServer := grpc.NewServer()
pb.RegisterGreeterServer(grpcServer, newServer())

// Register reflection service on gRPC server.
reflection.Register(grpcServer)

参考文献