日記はScrapboxに移動しました。

gRPCでgdbmにネットワークインタフェイスを持たせる

先日、HTTP/2とProtocol BuffersをベースにしたRPCフレームワークgRPCがリリースされた。

Microservicesがなんちゃらいわれる昨今だが、その実現のためには、設計面におけるベストプラクティスはもとより、実装面においても課題がある。すなわち、サービス間でどのようにオーバーヘッドが少なく、帯域を浪費しない通信を実現するかということ。そんな折Googleが、上記のリンク先にある通り「うちらめっちゃMicroservicesだし」ってんで、まさに「これだ!」という技術スタックでいい感じのものを出してくれた。

gdbmにRPCしてみる

とりあえず試してみたいので、簡単にできそうな例として、gdbmにネットワークインタフェイスをもたせてRPCしてみる、ってのをやってみた。


kentaro/grpc-gdbm · GitHub

インタフェイスを定義する

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もあるし、いろいろ環境は整っているなという感じがする。

Leave a Reply

Your email address will not be published. Required fields are marked *