知識ゼロから始める gRPC のサーバーサイド開発


はじめに

スターティングgRPC という本を読んだので、そのアウトプット記事を書きます。
言語は Go です。

本を読む前の筆者のレベル感は以下であり、想定読者も同様になります。

  • Go は書いたことある
  • gRPC は 1mm もわからん

本記事では gRPC 未経験者が最低限のサーバーサイド開発をできるレベルに到達することを目標とします。

ベースは スターティングgRPC ですが、最低限の内容のみを抜き出しています。
特に「そもそも gRPC とはどいう技術か?」「既存技術と比較したときのメリット・デリットは?」「gRPC の採用判断はどのようにすべきか?」などは本の方にかなりわかりやすく書かれているため、ぜひ一読をおすすめします。

筆者は Docker でサクっと環境を作り学習しました。
学習時のソースコードは GitHub にアップロード しています。

本記事の範囲

本記事では gRPC とは何か?から始まり、実際の実装の流れまでを記述します。
実装では .proto ファイルを書き、コードを自動生成し、生成されたインターフェースを実装することになります。
最後に gPRC クライントでの動作確認を行います。

以下、本記事の範囲の詳細になります。

含むもの

  • gRPC とは何か?
  • gRPC の長所は何か?
  • .proto ファイルの書き方
  • gRPC サーバー実装

含まないもの

  • PRC とは何か?
  • REST との違いは何か?
  • gRPC の短所は何か?
  • gRPC が対応しているストリーミング形式
  • 双方向/双方向ストリーミングの実装方法

くどいようですが、上記はすべて スターティングgRPC に書いてあります。
本当におすすめです。

gRPC とは何か?

gRPC とは、以下の2つを実現する RPC フレームワークである。

  1. 高速な API 通信
  2. スキーマ駆動家初

現在、マイクロサービス間の内部通信の実現方法として有力視されている。

なぜ gRPC を使用するのか?

REST と比較し、2つの長所があるためである。

① HTTP/2 による高速通信

gRPC では HTTP/2 を採用しており、 HTTP/1.1 を採用する REST よりも高速である。
HTTP/2 ではデータ形式はバイナリであり、コネクションの接続・切断を都度行わなくて良い点が特徴である。

② スキーマ駆動開発による高生産性

gRPC では Protocol Buffers を採用している。
Protocol Buffers ではスキーマを定義し、コードを自動生成する。

スキーマ定義 → 自動生成の仕組みにより、API ドキュメントの管理が不要となる。
スキーマ = API ドキュメントとなるためである。

クラアントがサービスを利用したい場合、スキーマを見ればすべて書いてある。
バックエンドがサービスを追加・変更したい場合、スキーマを書けばコードを自動生成できる。

.proto ファイルの記述

Protocol Buffers では、スキーマは *.proto というファイルに独自の IDL で記述する。
実際の書き方は以下の記事で。

protoc によるコード生成

前提条件

Go を利用する場合、以下の3つのパッケージが必要である。

  • コンパイラ
    • protoc
  • コードジェネレータ
    • protoc-gen-go
    • protoc-gen-go-grpc

protoc のインストール

Mac の場合、以下でインストールできる。

$ brew install protobuf

コードジェネレータのインストール

Go をインストール済みの場合、以下でインストールできる。

$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

生成コマンド

protoc コマンドでコードの自動生成が可能である。

$ protoc --go_out=path/to/output_dir --go-grpc_out=path/to/output_dir --go-grpc_opt=require_unimplemented_servers=false path/to/.proto_file

Go と gRPC のオプションを指定

Go と gRPC の2つのオプションを指定していることに注意。
Brotocol Buffers は何も Go 専門でもなければ、 gRPC 専門でもない。
そのため、生成先コードとして両方を指定する必要がある。

require_unimplemented_servers オプション

デフォルトで true である。

英語で調べても情報が出てこないので、正直あまり理解していない。
ざっくりインターフェースが実装されていない場合にエラーを返すメソッドと捉えている。
true を指定した場合は実装が求められるが、何を実装すればよいかイマイチわからない。

緊急性もなさそうなのでオプションで off にすることにより対応。

インターフェースの実装

ptoroc コマンドにより生成されたインターフェースを実装するコードを書く。

ハンドラー

基本的に型は自動生成されているため、行いたい処理を実装するだけでOKである。
コード例は後ほど。

ハンドラーの登録

ハンドラーを登録し、サーバーにエンドポイントを追加する。

server := grpc.NewServer()
api.RegisterXXXServiceServer(server, handler.NewXXXHandle())

server.Serve()

簡単なコード例

以下に簡単なコード例を示す。
パッケージ周りはザルであるが、勘弁願いたい。

実際に動くソースコードは GitHub に置いている。

.proto ファイル

user.proto
syntax = "proto3";
package user;

option go_package = "gen/api";

service UserGetService {
  rpc Get(UserRequest) returns (UserResponse) {}
}

message User {
  int32 id = 1;
  string name = 2;
}

message UserRequest {
  int32 id = 1;
}

message UserResponse {
  User user = 1;
}

ハンドラー

user_handler.go
package handler

import (
	"context"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"user/api/gen/api"
)

type userHandler struct{}

func NewUserHandle() *userHandler {
	return &userHandler{}
}

func (h *userHandler) Get(ctx context.Context, r *api.UserRequest) (*api.UserResponse, error) {
	if r.Id == 0 {
		return nil, status.Errorf(codes.InvalidArgument, "ユーザーの ID を指定してください")
	}

	res := &api.UserResponse{
		User: &api.User{
			Id:   24,
			Name: "John",
		},
	}

	return res, nil
}

サーバー

server.go
package main

import (
	"fmt"
	"log"
	"net"
	"os"
	"os/signal"

	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"

	"user/api/gen/api"
	"user/handler"
)

const port = 50052

func main() {
	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	server := grpc.NewServer()
	api.RegisterUserGetServiceServer(server, handler.NewUserHandle())
	// To debug on grpc_cli.
	reflection.Register(server)

	go func() {
		log.Printf("start gRPC server port: %v", port)
		server.Serve(lis)
	}()

	quit := make(chan os.Signal)
	signal.Notify(quit, os.Interrupt)
	<-quit
	log.Println("stopping gRPC server...")
	server.GracefulStop()
}

gRPC クライアント

動作確認のために gRPC を叩きたくなります。

ただし、cURL ではレスポンスを可読することはできません。
Protocol Bufffers はデータ形式がバイナリだからです。

そのため、gRPC 専用のクライアントを使用する必要があります。
今回は grpc_cli を使用します。

エンドポイントを叩く

grpc_cli のインストール

Mac の場合は以下でインストールできる。

$ brew tap grpc/grpc

$ brew install grpc

叩いてみる

コードの実装例の場合、以下のコマンドでエンドポイントを叩ける。

$ grpc_cli call localhost:50052 user.UserGetService.Get 'id: 1'
connecting to localhost:50052
user {
  id: 24
  name: john
}
Rpc succeeded with OK status

スキーマ定義を見る

サーバーを reflect 可能にする

サーバーを reflection に登録します。

スキーマ定義はデフォルトでは外部呼び出しから見ることはできません。
本番運用では必要ない機能だからです。
この設定をすることで、 gRPC クライアントからスキーマ定義を参照可能になります。

import (
  ...
  "google.golang.org/grpc/reflection"
)

func main() {
  ...
  reflection.Register(server)
}

ls する

grpc_cli ls コマンドでスキーマ定義を見ることができます。
ファイルを見たほうが速いか、cli から見たほうが速いかは人によりそうですね。

$ grpc_cli ls localhost:50052 user.UserGetService -l
filename: user.proto
package: user;
service UserService {
  rpc Get(user.UserRequest) returns (user.UserResponse) {}
}

終わりに

モダンらしいとは聞きつつ中々手がつけられなかった gRPC の学習・解説をしました。
本では一部古くなっていた箇所も改修してコードにしています。

もし役に立ちましたら LGTM 押して頂けると励みになります。