gRPCとは

20152 にGoogleが公開したGoogle内でも使用されているRPCフレームワークであり、下記の恩恵をうけることができます

  • Protocol BufferのIDLを書くことによって、通信形式の型が保証された通信を行うことができる
  • gRPCをサポートしている言語であれば、異なる言語間でも通信が可能(C++, Java, Go, Python, Ruby, Node.js, Android Java, C#, Objective-C, PHP)
  • HTTP/2で通信を行うためストリームの多重化、フロー制御等ができる

弱点としては、

  • 実装難度が高い点、
  • protocが動く環境に制限されるためフロントエンドのjsでは動かない

というものがあるため、

  • サーバーサイドのマイクロサービス間の通信
  • 長期運用や規模の大きいサービスで通信の保証をする必要がある

ものに適している通信方式と思います

今回やってみたこと

検証ということで、ソシャゲでよくあるガチャの簡単なマイクロサービスを作成してみました。

  1. 複数のCardをサーバーに送る
  2. その中からランダムに一つ選ぶ(今回は単純にランダムなものを選んでいます)(本来のソシャゲの場合はかくりt..おっと誰か来たようだ)
  3. サーバーから一つカードを返却し、返答ステータスも返却します(今回は例外処理は書いてないので常に同じRetCodeが返ってきます)

この実装によって

  • 基本的なgRPCの実装
  • 複数言語でのgRPCの挙動
  • 配列的な型のパターン
  • Request Responseで同じ型を使うパターン

を確認します。 ソースコードはこちら https://github.com/kotamat/grpc-gacha

フロー

スクリーンショット 2016-09-22 18.34.37.png

各種解説

ProtocolBuffer

設定ファイルは下記のようになっています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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ではoptionalrepeatedが使用可能です。 ( requiredは後方互換性の問題から削除されました ) gRPCでは通信のオーバーヘッドを防ぐために、処理インデックスを元に通信を行うため、処理インデックス(整数値)を指定する必要があります。

ProtocolBufferのビルド

ProtocolBufferは各言語ごとにファイルを生成し、実際の処理で使用できるようにする必要があります。 ビルドは protoc というOS依存のファイルのインストールと、 言語別のビルド用プラグインをインストールする必要があります。

Golangの場合は

1
$ go get -u github.com/golang/protobuf/{proto,protoc-gen-go}

でインストールできます。他の言語は公式サイトを御覧ください http://www.grpc.io/docs/quickstart/go.html

実際の処理(Golang)

サーバーサイド

各種言語によって実装方法はことなりますが、 基本的に

  1. Listen処理、サーバーの立ち上げ、
  2. ProtocolBufferで定義した関数の処理

を記載するだけです。

Listen処理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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で定義した関数の処理

1
2
3
4
5
6
7
8
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 に返せばオッケーです。

クライアントサイド

クライアントサイドも基本的には下記の処理を書きます。

  1. gRPCコネクションの作成
  2. 送信データの整形
  3. 関数の実行

gRPCコネクションの作成

1
2
3
4
5
6
	conn, err := grpc.Dial(address, grpc.WithInsecure())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGachaClient(conn)

送信データの整形

1
2
3
4
5
	// define cards
	cards := []*pb.Card{
		&pb.Card{Name: "card1"},
		&pb.Card{Name: "card2"},
	}

関数の実行

1
2
3
4
5
	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)

実行

予めサーバーを立ち上げておいた上で、 クライアントを実行します。

1
go run server/server.go
1
go run client/client.go

まとめ

‐ マイクロサービスを作る際に通信のフォーマットを保証する方法としてgRPCはある程度担保してくれる - 多言語間の通信もしやすい

課題

  • 環境整備(特にprotocol bufferをビルドするツールのインストール)が言語によっては大変だった。
  • 本番環境で適応するには、例外処理や複雑な処理に対する知見を貯める必要がありそう。