golangでgRPCを使ったAPIServerを作ってみる
はじめに
この記事は、 eureka Advent Calendar 2017 5日目の記事です。
こんにちは、エウレカでCTO(*Cat Tech Officer)を営む傍、サーバーサイドエンジニアとして
日々生計を立てている@marnie-eureです。
2015年にgRPCが発表されてから、2年が経ちましたね。
国内外の大手企業でのgRPCの事例も増えてきました。
これからのご時勢、gRPCの一つも使えないのでは、愛する猫たちを養っていくことも難しいかもしれない。
そんな危機感にかられたので、今回はふわふわっとgRPCを使ってAPIを作ってみようと思います。
- (注) CTO(Chief Tech Officer)は@kaneshinなので
猫以外の話
はそちらにお問い合わせください。
gRPC Basics
gRPC?
gRPCは、Googleによって開発されたRPCフレームワークです。
HTTP/2を使用した通信層(ProtocolBuffersでシリアライズ)とProtocolBuffers(標準)としたテンプレートコードの生成がセットで提供されています。
勿論、HTTP2のstreamもサポートしています。
gRPCのRPC方式は以下の通り。
- Unary RPC (1リクエスト1レスポンス)
- Server streaming RPC (1つのリクエストに複数レスポンス)
- Client streaming RPC (複数のリクエストに一つのレスポンス)
- Bidirectional streaming RPC(双方向)
対応言語/platformも幅広く
- C++
- go
- Ruby
- Android Java
- PHP
- Objective-C
等の複数言語をサポートしています。
実装してみる
eurekaでは主にgolangを採用してますので、golangで実装します :)
前準備
- gRPCをインストールします。
go get -u google.golang.org/grpc
- protoファイルからコード生成をするコンパイラ(protoc)をインストールします。
protocのダウンロードはos別にこちらから
私の環境がosx-x86_64なので今回は protoc-3.5.0-osx-x86_64.zip
を利用します。
PATHの通ったディレクトリに解凍したディレクトリの/bin
の中のバイナリを移してあげてください。
- protocのGo用のプラグインをインストールします。
go get -u github.com/golang/protobuf/protoc-gen-go
.proto
ファイルにインターフェースを定義する
gRPCをベースにした開発では、まずはIDLを使ってprotoにAPIの定義を書きます。
ProtocolBuffer以外もサポートはしているようですが、
ツール周りやドキュメントが一番手厚いし、標準に寄り添って行きたい民なので、今回はprotocolBufferで.proto
ファイルを作ります。
syntax = "proto3";service Cat {
rpc GetMyCat (GetMyCatMessage) returns (MyCatResponse) {}
}message GetMyCatMessage {
string target_cat = 1;
}message MyCatResponse {
string name = 1;
string kind = 2;
}proto3の型や各言語の型の対応はgoogleのドキュメント、基本のscalar型以外を使いたい場合はgoogle/protobufをimportする感じで。.proto ファイルからserver、client,interface等のコードを生成する定義から各言語のベースとなるコードの自動生成をします。protocコマンドを実行します。protoc --go_out=plugins=grpc:../pb cat.proto成功すると xxxx.pb.go が生成されます。xxxx.pb.go ファイルには、protoで定義した以下が含まれています。
- request
- response
- client,serverのinterface
- registerMethod
プラットフォームまたいでも、同一の定義からこの辺のコードが生成できるのは楽ですね :)
proto3のscalar型がgoのtime型やint型などに対応してたら嬉しかったのですが、今時点では対応してなかったのがやや辛み...protocによるdocument生成protocでdocumentの生成もできます。わーい。protoc --doc_out=html,index.html:./ proto/*.protopb.goファイルを参照してサーバーとクライアントの実装生成されたpb.goに含まれるinterfaceに沿って、実処理とserverとclientを実装します :)service (実処理)作られたxxx.pb.goのinterfaceを満たすように実装します。xxx.pb.gotype CatServer interface {
GetMyCat(context.Context, *GetMyCatMessage) (*MyCatResponse, error)
}serviceって名称は、公式やprotoの呼称から取ってきただけなのでお好みで。package serviceimport (
"context"
"errors"
pb "marnie_playground/grpc-sample/pb"
)type MyCatService struct {
}func (s *MyCatService) GetMyCat(ctx context.Context, message *pb.GetMyCatMessage) (*pb.MyCatResponse, error) {
switch message.TargetCat {
case "tama":
//たまはメインクーン
return &pb.MyCatResponse{
Name: "tama",
Kind: "mainecoon",
}, nil
case "mike":
//ミケはノルウェージャンフォレストキャット
return &pb.MyCatResponse{
Name: "mike",
Kind: "Norwegian Forest Cat",
}, nil
}return nil, errors.New("Not Found YourCat")
}servergRPC関連で書く必要があるコードは
- port listen
- 作った実処理の登録,serve
だけです。interceptor chain等は用途に応じて。package mainimport (
"log"
"net"pb "marnie_playground/grpc-sample/pb"
"marnie_playground/grpc-sample/service""google.golang.org/grpc"
)func main() {
listenPort, err := net.Listen("tcp", ":19003")
if err != nil {
log.Fatalln(err)
}
server := grpc.NewServer()
catService := &service.MyCatService{}
// 実行したい実処理をseverに登録する
pb.RegisterCatServer(server, catService)
server.Serve(listenPort)
}clientpackage mainimport (
"context"
"fmt"
"log"pb "marnie_playground/grpc-sample/pb""google.golang.org/grpc"
)func main() {
//sampleなのでwithInsecure
conn, err := grpc.Dial("127.0.0.1:19003", grpc.WithInsecure())
if err != nil {
log.Fatal("client connection error:", err)
}
defer conn.Close()
client := pb.NewCatClient(conn)
message := &pb.GetMyCatMessage{"tama"}
res, err := client.GetMyCat(context.TODO(), message)
fmt.Printf("result:%#v \n", res)
fmt.Printf("error::%#v \n", err)
}FactoryMethodとClientInterfaceが提供されているので、Connection作成してメソッドを呼び出すだけです。ビルド & テスト出来あがったclientとserverをそれぞれgo build,実行すれば出来上がり。./client
result:&cat.MyCatResponse{Name:"tama", Kind:"mainecoon"}
error::<nil>middleware(interceptor)logging,auth,recovery的な物はinterceptorとかmiddleware的な物を作ってやれると
既存のプロジェクトからの移行もスムーズかなーと思っていたので、調べてみました。interceptorは型定義されてますので、下記を満たすようなインターフェースで実装します。google.golang.org/grpc/interceptor.go
//UnaryRPCならこっち
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
//StreamRPCならこっち
type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error利用するRPC方式によってgrpc.UnaryServerInterceptor() ないし grpc.StreamInterceptor()で
grpc.ServerOptionに変換すればOKです :)大雑把なイメージは以下のような感じ。<br />func MiddlewareFunc(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error)
// 処理かく
}func main() {
//中略
middleware := &YourMiddleware{}
opt := []grpc.ServerOption{grpc.UnaryInterceptor(MiddlewareFunc)}
server := grpc.NewServer(opt...)
}grpc-middleware に何個か実装されたmiddleware(cf. logrus,validator)がありますので、
独自のmiddlewareを実装する場合はこの辺参考にすれば良いのかなと思います :)まとめ
- 主要な通信層の処理は提供されているので、HTTP/2関連の実装は必要ない。
- protoからボイラーテンプレートコードやドキュメントも作られるのは楽。
- middlewareも増えている。
コードの自動生成によって本来時間をかけるべき、機能実装に集中できる
HTTP/2による高速化/stream用途や、protobuff標準でのコード生成という所が包括的に提供されるといった恩恵を受けられるのは大きなメリットだなぁと感じます。フルスタックフレームワークと較べるのは、主旨が異なるので要件や選定において重視する所次第って感じですが、マイクロフレームワークを使うような構成を検討していたり、ストリーミング通信が要件に含まれているのであれば採用するメリットははあると考えています :)細々、下記のような気になる所はありましたが
- proto3がgoの型を全て網羅してるわけではなさそうなので、applicationLayerの既存コードの型合わない場合は変換層とか必要そう。
- curlでポチッと、みたいなのが使えなくなったので、テストがちょっと大変。grpc-gatewayでも使うべきなのかしら。
まぁデメリットと言うほどではないかな〜って気も。
テスト手段やエコシステム、ミドルウェアは今後、充実していく気もするし、既に必要十分ではあると思うので。あとがき今回はチュートリアル的な所と周辺情報を通して基本的な流れと所感を書いてみましたが、
eurekaでは実際に一部のmicro serviceのgRPC導入/移行を進行しています。
(既存コードとの兼ね合いで記事までに間に合いませんでした。てへぺろgo+gRPCでの開発やeurekaに興味が持てた方(猫愛があれば尚良)は、是非お気軽にお話に来てくだされば!それでは、2017年も残り少ないですが、日々猫への感謝を忘れず、過ごしていきましょう!!参考にした資料/Repsitory Thanks! :)grpc goDoc
grpc.io
grpc-middleware
grpc-echosystem
google/protobuf
protocol-buffers docs