gRPCでgdbmにネットワークインタフェイスを持たせる
先日、HTTP/2とProtocol BuffersをベースにしたRPCフレームワーク、gRPCがリリースされた。
Microservicesがなんちゃらいわれる昨今だが、その実現のためには、設計面におけるベストプラクティスはもとより、実装面においても課題がある。すなわち、サービス間でどのようにオーバーヘッドが少なく、帯域を浪費しない通信を実現するかということ。そんな折Googleが、上記のリンク先にある通り「うちらめっちゃMicroservicesだし」ってんで、まさに「これだ!」という技術スタックでいい感じのものを出してくれた。
gdbmにRPCしてみる
とりあえず試してみたいので、簡単にできそうな例として、gdbmにネットワークインタフェイスをもたせてRPCしてみる、ってのをやってみた。
インタフェイスを定義する
Thriftとかああいうのを触ったことがあるひとにはお馴染みのIDL(Interface Definition Language)があって、gRPCの場合はProtocol Buffersを用いて、こんな感じで書く。以下は、gdbmに対して、Insert
, Replace
, Fetch
というRPCを定義している。Protocol Buffersを使うぐらいなのでデータ量-awareな感じだろうから、ほんとは以下のRequest
の定義をもっと厳密にわけた方がいいと思うけど、例なので深く考えない。
syntax = "proto3"; package gdbm; service Gdbm { rpc Insert (Request) returns (Entry) {} rpc Replace (Request) returns (Entry) {} rpc Fetch (Request) returns (Entry) {} } message Request { string key = 1; string value = 2; } message Entry { string key = 1; string value = 2; }
んでもって、この定義からRPCへのクライアントとサーバのコードを生成する。
$ protoc -I ./protos ./protos/gdbm.proto --go_out=plugins=grpc:gdbm
生成された内容は以下の通り:
サーバとクライアントを書く
あとはそれを使ってなんか適当に書いていくだけ。IDLで定義したInsert
, Replace
, Fetch
を実装したstruct
を、上記で生成されたpb.RegisterGdbmServer()
にわたしてやると、RPCをいい感じに受け付けるようになる(生成されたコードに、そういうinterface
が定義されている)。
サーバ:
package main import ( "flag" "fmt" "log" "net" "github.com/cfdrake/go-gdbm" pb "github.com/kentaro/grpc-gdbm/gdbm" "golang.org/x/net/context" "google.golang.org/grpc" ) var port int var file string func init() { flag.IntVar(&port, "port", 50051, "port number") flag.StringVar(&file, "file", "grpc.gdbm", "gdbm file name") flag.Parse() } type server struct { Db *gdbm.Database } func (s *server) Insert(ctx context.Context, in *pb.Request) (*pb.Entry, error) { err := s.Db.Insert(in.Key, in.Value) return &pb.Entry{Key: in.Key, Value: in.Value}, err } func (s *server) Replace(ctx context.Context, in *pb.Request) (*pb.Entry, error) { err := s.Db.Replace(in.Key, in.Value) return &pb.Entry{Key: in.Key, Value: in.Value}, err } func (s *server) Fetch(ctx context.Context, in *pb.Request) (*pb.Entry, error) { value, err := s.Db.Fetch(in.Key) return &pb.Entry{Key: in.Key, Value: value}, err } func main() { lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port)) if err != nil { log.Fatalf("failed to listen: %v", err) } db, err := gdbm.Open(file, "c") if err != nil { log.Panicf("couldn't open db: %s", err) } defer db.Close() s := grpc.NewServer() pb.RegisterGdbmServer(s, &server{Db: db}) s.Serve(lis) }
クライアントは特に述べるまでもない感じ。この例の場合に普通に使うなら、生成されたコードを使っていい感じのAPIを持つライブラリを作って、それを使うことになるだろう。
package main import ( "flag" "fmt" pb "github.com/kentaro/grpc-gdbm/gdbm" "golang.org/x/net/context" "google.golang.org/grpc" "log" ) var port int var key string var value string func init() { flag.IntVar(&port, "port", 50051, "port number") flag.StringVar(&key, "key", "key", "key name") flag.StringVar(&value, "value", "value", "value for key") flag.Parse() } func main() { conn, err := grpc.Dial(fmt.Sprintf("localhost:%d", port)) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c := pb.NewGdbmClient(conn) r, err := c.Replace(context.Background(), &pb.Request{Key: key, Value: value}) if err != nil { log.Fatalf("gdbm error: %v", err) } r, err = c.Fetch(context.Background(), &pb.Request{Key: key}) log.Printf("value for %s: %s", key, r.Value) }
RPCしてみる
そしたら、あとはサーバとクライアントを使ってRPCできる。
サーバを起動する:
$ go run server/main.go
-key
, -value
の引数で指定した値を入れたり出したりするだけのクライアント:
$ go run client/main.go -key foo -value bar 2015/03/03 01:11:12 value for foo: bar
簡単ですね。
使いどころ
この例で示したような、6〜7年前とかにThriftとか使っていた頃のようなユースケースにもあてはまるんだろうけど、いまだとまさにMicroservices用とかStreamingとかに使う感じになるんだろう。というか、そうでないとHTTP/2なうれしさがあんまり見いだせないだろうし。
ともあれ、当時だといろいろとしんどかった記憶があるけれども、いまだと例でも利用したGoもあるしHTTP/2もあるし、いろいろ環境は整っているなという感じがする。