gRPCとは
2015/2 にGoogleが公開したGoogle内でも使用されているRPCフレームワークであり、下記の恩恵をうけることができます
- Protocol BufferのIDLを書くことによって、通信形式の型が保証された通信を行うことができる
- gRPCをサポートしている言語であれば、異なる言語間でも通信が可能(C++, Java, Go, Python, Ruby, Node.js, Android Java, C#, Objective-C, PHP)
- HTTP/2で通信を行うためストリームの多重化、フロー制御等ができる
弱点としては、
- 実装難度が高い点、
- protocが動く環境に制限されるためフロントエンドのjsでは動かない
というものがあるため、
- サーバーサイドのマイクロサービス間の通信
- 長期運用や規模の大きいサービスで通信の保証をする必要がある
ものに適している通信方式と思います
今回やってみたこと
検証ということで、ソシャゲでよくあるガチャの簡単なマイクロサービスを作成してみました。
- 複数のCardをサーバーに送る
- その中からランダムに一つ選ぶ(今回は単純にランダムなものを選んでいます)(本来のソシャゲの場合はかくりt..おっと誰か来たようだ)
- サーバーから一つカードを返却し、返答ステータスも返却します(今回は例外処理は書いてないので常に同じRetCodeが返ってきます)
この実装によって
- 基本的なgRPCの実装
- 複数言語でのgRPCの挙動
- 配列的な型のパターン
- Request Responseで同じ型を使うパターン
を確認します。
ソースコードはこちら https://github.com/kotamat/grpc-gacha
フロー
各種解説
ProtocolBuffer
設定ファイルは下記のようになっています。
syntax = "proto3"; // PBのバージョン
package gacha; // 全体の名前空間
service Gacha{
rpc Lottery (Request) returns (Response) {}
}
message Card {
string name = 1;
}
message Request {
repeated Card cards = 1; // 配列の指定
}
message Response {
Card card = 1; //他のmessageをパラメータとする
int32 ret_code = 2;
}
service
の中の rpcに続くものは、実行関数
( 引数
) returns ( 返り値
) {}
という順番になっており引数、返り値の型を宣言しているものが messsage
に続く物となっています。
message
にはパラメータを複数指定することができ、また他のmessage
をパラメータとすることもできます
パラメータに配列を指定したい場合は、repeated
を添字にすることにより、0個以上のパラメータを受け取ることができます。
パラメータは 添字
型
パラメータ名
= 処理インデックス
;
という順番となっており添字はproto3ではoptional
とrepeated
が使用可能です。 ( required
は後方互換性の問題から削除されました )
gRPCでは通信のオーバーヘッドを防ぐために、処理インデックスを元に通信を行うため、処理インデックス(整数値)を指定する必要があります。
ProtocolBufferのビルド
ProtocolBufferは各言語ごとにファイルを生成し、実際の処理で使用できるようにする必要があります。
ビルドは protoc
というOS依存のファイルのインストールと、
言語別のビルド用プラグインをインストールする必要があります。
Golangの場合は
$ go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
でインストールできます。他の言語は公式サイトを御覧ください
http://www.grpc.io/docs/quickstart/go.html
実際の処理(Golang)
サーバーサイド
各種言語によって実装方法はことなりますが、
基本的に
- Listen処理、サーバーの立ち上げ、
- ProtocolBufferで定義した関数の処理
を記載するだけです。
Listen処理
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGachaServer(s, &server{})
s.Serve(lis)
}
ProtocolBufferで定義した関数の処理
func (s *server) Lottery(ctx context.Context, in *pb.Request) (*pb.Response, error) {
if len(in.Cards) < 1 {
return &pb.Response{Card:nil, RetCode:0}, errors.New("empty cards")
}
rand.Seed(time.Now().UnixNano())
chosenKey := rand.Intn(len(in.Cards))
return &pb.Response{Card: in.Cards[chosenKey], RetCode: 1}, nil
}
クライアントからの情報は *pb.Request
に乗っかっているのでそれをゴニョゴニョして *pb.Response
に返せばオッケーです。
クライアントサイド
クライアントサイドも基本的には下記の処理を書きます。
- gRPCコネクションの作成
- 送信データの整形
- 関数の実行
gRPCコネクションの作成
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGachaClient(conn)
送信データの整形
// define cards
cards := []*pb.Card{
&pb.Card{Name: "card1"},
&pb.Card{Name: "card2"},
}
関数の実行
r, err := c.Lottery(context.Background(), &pb.Request{Cards: cards})
if err != nil {
log.Fatalf("could not get card %v", err)
}
log.Printf("gain card: %v", r.Card.Name)
実行
予めサーバーを立ち上げておいた上で、
クライアントを実行します。
go run server/server.go
go run client/client.go
まとめ
‐ マイクロサービスを作る際に通信のフォーマットを保証する方法としてgRPCはある程度担保してくれる
- 多言語間の通信もしやすい
課題
- 環境整備(特にprotocol bufferをビルドするツールのインストール)が言語によっては大変だった。
- PHPでもやろうとしたが、DockerHubに公開されている Dockerfileだけだと protoc-gen-phpが入っていなかった(?)ため、一旦断念.. https://github.com/grpc/grpc-docker-library/tree/master/0.11/php
- 本番環境で適応するには、例外処理や複雑な処理に対する知見を貯める必要がありそう。