LoginSignup
117
107

More than 5 years have passed since last update.

gRPCを使った簡易的なマイクロサービスを作ってみた

Last updated at Posted at 2016-09-22

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では動かない

というものがあるため、

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

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

今回やってみたこと

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

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

この実装によって

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

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

フロー

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

各種解説

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ではoptionalrepeatedが使用可能です。 ( 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)

サーバーサイド

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

  1. Listen処理、サーバーの立ち上げ、
  2. 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 に返せばオッケーです。

クライアントサイド

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

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

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をビルドするツールのインストール)が言語によっては大変だった。
  • 本番環境で適応するには、例外処理や複雑な処理に対する知見を貯める必要がありそう。
117
107
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
117
107