※ この記事は2019年11月7日に作成した GoとDockerでLet's try gRPC - LiBz Tech Blog と同じ内容です
はじめに
こんにちは!エンジニアの渡邊です。早いもので、11月でLiBに入社して丸1年がたちました。
このブログへの投稿も4回目になります。
前回の「とってもRailsライクなサーバーレスフレームワーク「Ruby on Jets」を本番環境に導入した話」では、jetsの開発者である**tongueroo氏や、Rubyの生みの親まつもとゆきひろ氏**をはじめ、多くの方にシェアをしていただき大変励みになりました!
みなさんありがとうございました。
今回はGoogleが開発したRPCフレームワーク**gRPC**について書こうと思います。
gRPCとは
Google社内で使用されているstubbyというRPC(Remote Procedure Call)フレームワークをオープンソース化したものです。
gRPCの特徴
- Protocol Buffers(xml,jsonのようなインターフェース記述言語)を利用した高速かつ低用量、異なる言語間でも型保証された通信
- サーバ/クライアント用ソースコードの自動生成
- 多言語対応(C++, Java, Go, Python, Ruby, Node.js, C#, Objective-C, PHP, etc..)
- 通信プロトコルHTTP/2による通信
- Streamingを用いた双方向通信(従来の通信は1Request-1Response)
上記のような特徴から、gRPCはマイクロサービス化されたシステム間の接続やデータ転送についての課題を解決することができ、効率良く通信を行うことができます。
gRPCが解決するマイクロサービスの課題
-
複数のシステム間でインターフェースが統一されている必要がある
-
システムは頻繁に変更があるものなので全システム間の整合性をとることが難しい
-> ProtocolBuffer(.protoファイル)からサーバー/クライアントのソースコードを自動で生成でき、言語も複数選択できる
-
複数のシステムの仕様を把握するためのドキュメント(仕様書, wiki, Swagger, etc..)更新漏れ、記載ミス
-> サービス間の通信はprotoファイルから生成されるため、protoファイルが正しい仕様になる(ソースコードはprotoファイルを元に生成されるため、漏れやミスが発生しない)
-
-
APIリクエストが頻発するためパフォーマンスが劣化しやすくなる
-
複数のシステムに分散されたリソースをそれぞれ取得しに行く必要があるためどうしてもリクエスト回数が増える
-> HTTP/2では一度のTCPのコネクションで複数のリクエスト, レスポンスを取扱うことができる
-
HTTP/1では一度のコネクションで取得できるリソースは一つという仕様(ブラウザ側で複数のTCPを同時に貼るとしてもコネクション数に制限がある)
-> HTTP/2での通信のため、コネクションは捨てられることなく初回接続時のコネクションを使い続けられる
-
gRPCの課題
当然メリットばかりではなくデメリットもあります。
通信規格がHTTP/2ということは、対応していないブラウザやロードバランサはリクエストを受けることが出来ないということです。
ブラウザに関してはgrpc-gatewayやgrpc-webなどのリバースプロキシを行ってくれるライブラリを利用すれば問題ないのですが、
サービス間通信はHTTP2プロトコルで、ブラウザとサーバ間通信のみHTTP1プロトコルになってしまうので混在することに違和感を覚える人もいるのではないでしょうか。
gRPCサーバーの動作確認もやや面倒です。
従来のAPIのような動作確認はcurlで行えましたが、gRPCではcurlは使えません。
(最近はgRPCでもcurlライクに動作確認ができるgRPCurlや、gRPC用のGUIクライアントであるgRPC UIというツールがあるようです)
Let's try gRPC
※コード周りは下記の記事のものを利用させていただきました!
Goで始めるgRPC入門
GolangでgRPCを試してみる
1. 準備
- gRPC
- protoc(.protoファイルからコード生成をするコンパイラ)
- protoc-gen-go(protocのGo用プラグイン)
上記の3つをインストールします。
gRPC
$ go get -u google.golang.org/grpc
protoc
OSによって異なります。こちらからインストールしてください。
protoc-gen-go
$ go get -u github.com/golang/protobuf/protoc-gen-go
Dockerfile
今回はDockerを使用しますのでDockerfileとdocker-compose.ymlも用意します。
FROM golang:1.13.1
RUN apt-get update && apt-get install -y unzip
# Install protobuf
# @see https://github.com/yoshi42662/go-grpc/blob/master/server/Dockerfile
RUN mkdir -p /tmp/protoc && \
curl -L https://github.com/protocolbuffers/protobuf/releases/download/v3.10.0/protoc-3.10.0-linux-x86_64.zip > /tmp/protoc/protoc.zip && \
cd /tmp/protoc && \
unzip protoc.zip && \
cp /tmp/protoc/bin/protoc /usr/local/bin && \
chmod go+rx /usr/local/bin/protoc && \
cd /tmp && \
rm -r /tmp/protoc
WORKDIR /study-grpc
COPY . /study-grpc
RUN go get -u google.golang.org/grpc
RUN go get -u github.com/golang/protobuf/protoc-gen-go
docker-compose.yml
とりあえずコンテナが起動していれば良いのでcommand: bash
にしています
version: '3.7'
services:
study-grpc:
build: .
container_name: "study-grpc"
ports:
- 1234:1234
volumes:
- .:/study-grpc
command: bash
tty: true
2. protoファイルの作成
protoファイルにインターフェースを定義しコードを生成します。
pb/cat.proto
を作成しました。
syntax = "proto3";
service Cat {
rpc GetMyCat (GetMyCatMessage) returns (MyCatResponse) {}
}
message GetMyCatMessage {
string target_cat = 1;
}
message MyCatResponse {
string name = 1;
string kind = 2;
}
syntax = "proto3"
を記載し忘れるとproto2として解釈されるので注意が必要です。
gRPCでは一般的にproto3を利用します。proto2とproto3の違いはこちらの記事がわかりやすかったです。
string name = 1
の数字の部分はタグナンバーです。タグナンバーはフィールドを区別する際に利用されます。一度採番したら変えない方がよいとされているので変更がある場合は新たに採番します。
次にコンテナ内に入りprotoファイルをコンパイルし、ソースコードを生成します。
# コンテナ起動
$ docker-compose up
# コンテナの中に入る
$ docker exec -it study-grpc bash
# protocコマンド実行
$ protoc --go_out=plugins=grpc:. ./pb/cat.proto
pb/cat.pb.go
が生成さていればOKです。
3. server側の処理
server.go
を作成します。
package main
import (
"context"
"errors"
"google.golang.org/grpc"
"log"
"net"
cat "study-grpc/pb"
)
type myCatService struct{}
func (s *myCatService) GetMyCat(ctx context.Context, message *cat.GetMyCatMessage) (*cat.MyCatResponse, error) {
switch message.TargetCat {
case "tama":
return &cat.MyCatResponse{
Name: "tama",
Kind: "Maine Coon",
}, nil
case "mike":
return &cat.MyCatResponse{
Name: "mike",
Kind: "Norwegian Forest Cat",
}, nil
default:
return nil, errors.New("Not Found YourCat..")
}
}
func main() {
port, err := net.Listen("tcp", ":1234")
if err != nil {
log.Println(err.Error())
return
}
s := grpc.NewServer()
cat.RegisterCatServer(s, &myCatService{})
s.Serve(port)
}
4. client側(リクエスト)の処理
client.go
を作成します。
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"log"
cat "study-grpc/pb"
)
func main() {
conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
if err != nil {
log.Fatal("connection error:", err)
}
defer conn.Close()
client := cat.NewCatClient(conn)
message := &cat.GetMyCatMessage{TargetCat: "mike"}
res, err := client.GetMyCat(context.Background(), message)
if err != nil {
log.Fatal(err)
}
fmt.Printf("result:%s\n", res)
}
5. buildして実行
作成したserver.go
とclient.go
をbuildして実行してみましょう。
# コンテナの中に入る
$ docker exec -it study-grpc bash
# server.goをbuild & 実行
$ go build server.go
$ ./server
# client.goをbuild & 実行
$ go build client.go
$ ./client
# buildするのが面倒な場合はgo runでもokです
$ go run server.go
$ go run client.go
実行結果
result:name:"mike" kind:"Norwegian Forest Cat"
最後に
いかがでしたでしょうか?
名前を聞いただけでは「gRPC? ProtocolBuffer? なんだそれ難しそう、、」と思ってしまいますが実際に手を動かしてみると想像していたものよりは簡単だったのではないでしょうか。
Googleだけでなくいくつもの大手企業が採用している実績がありますし、日本の企業での事例もどんどん増えてきています。
マイクロサービス化への課題はいくつもありますが通信部分を解決してくれるgRPCは選択肢のひとつとして、ぜひ覚えておきたいですね。
今回使用したコードはこちらのリポジトリにまとめました。