Kentaro Kuribayashi's blog

Software Engineering, Management, Books, and Daily Journal.

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