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/*.proto
pb.goファイルを参照してサーバーとクライアントの実装
生成されたpb.goに含まれるinterfaceに沿って、実処理とserverとclientを実装します :)
service (実処理)
作られたxxx.pb.goのinterfaceを満たすように実装します。
xxx.pb.go
type CatServer interface {
GetMyCat(context.Context, *GetMyCatMessage) (*MyCatResponse, error)
}
serviceって名称は、公式やprotoの呼称から取ってきただけなのでお好みで。
package service
import (
"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")
}
server
gRPC関連で書く必要があるコードは
  • port listen
  • 作った実処理の登録,serve
だけです。interceptor chain等は用途に応じて。
package main
import (
"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)
}
client
package main
import (
"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
Like what you read? Give eureka_developers a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.