ファニーボーン

プログラムなどについて書いていこうと思います。

gRPC x Go言語 x Unixドメインソケット

概要

最近の業務でのこと。
Go言語で書かれたサービスが色々なロジックを持つようなってきたので、俗にいうマイクロサービスへの分割を行いました。
分割したサービス同士の通信には、速い速いと噂のgRPC(http://www.grpc.io/)を適用しました。分割後のサービスは同じサーバー内で稼働させることに。
それなら、通信はデフォルトのTCPでなく、Unixドメインソケットでプロセス間通信をしたほうが速いんじゃね?ということで、今回はGo言語でgRPC使う際に、Unixドメインソケットでプロセス間通信する実装方法、また、TCPとのパフォーマンス比較などを書きます。

実装方法

Unixドメインソケットを使用は、コネクションの取得時に指定します。
何も指定しない場合は、以下のような感じ。

// デフォルトでTCP.
conn, err := grpc.Dial("127.0.0.1:9000", grpc.WithInsecure())

grcp.Dialは、DialOptionを指定してコネクションを設定することができます(grpc - GoDoc)。
例えば、WithTimeoutでタイムアウトを、WithCodecでメッセージのコーデックを設定することができます。Unixドメインソケットを使用する場合は以下のように、WithDialerを用いてnet.dialerを指定します。

// dialer を作成して grpc.WithDialer(dialer)でDialオプションを追加する.
dialer := func(a string, t time.Duration) (net.Conn, error) {
	return net.Dial("unix", a)
}
conn, err := grpc.Dial("/tmp/test.sock", grpc.WithInsecure(), grpc.WithDialer(dialer))

パフォーマンス

ローカル環境(MBP15, 2015)で簡単なサーバとクライアントを実装して、10,000回リクエストした場合のレスポンスタイムを比較しました。ソースは末尾。
比較のために、1コネクション、1クライアントで固定。
結果から言うと、案の定、Unixドメインソケットの方が速かったです。TCP: 115 μs / req に対し、Unixドメインソケット: 90 μs /req。

所感

現在、gRPCを用いてサービスを分割したものが、本番環境で動いています。リリース前には負荷試験などを行い、分割前と後で性能を比較しました。
結果は、サービスを分割することにより、ほとんど性能の劣化はなかったです。
gRPCを調べていると、etcdでも使われている様子。関連の記事も面白かったので、etcdのソースを読んでみようと思います。

検証で使ったソース

protoファイル

syntax = "proto3";

package hello;

service Hello {
	rpc SayHello (HelloRequest) returns (HelloReply){}
}

message HelloRequest{
	string name = 1;
}

message HelloReply {
	string message = 1;
}

server側

var network = "unix"
var address = "/tmp/grpc_test.sock"

type server struct{}

// リクエスト(Name)を受け取り、レスポンス(Message)を返す
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	return &pb.HelloReply{Message: fmt.Sprintf("Hello, %s.", in.Name)}, nil
}

func main() {
	lis, err := net.Listen(network, address)
	if err != nil {
		panic(err)
	}
	defer func(){
		if network == "unix" {
			_, err := os.Stat(address)
			if err == nil {
				if err := os.Remove(address); err != nil {
					panic(err)
				}
			}
		}
	}()


	s := grpc.NewServer()
	pb.RegisterHelloServer(s, &server{})
	s.Serve(lis)
}

client側

var network = "unix"
var address = "/tmp/grpc_test.sock"

var requestNum = 100000

func main() {
	// コネクション数は1
	conn, err := genConn(network, address)
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	// クライアント数も1
	client := pb.NewHelloClient(conn)

	// 名前の作成(name_i)
	names := make([]string, requestNum)
	for i := 0; i < requestNum; i++ {
		names[i] = fmt.Sprintf("name_%d", i)
	}

	st := time.Now()
	for i := 0; i < requestNum; i++ {
		_, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: names[i]})
		if err != nil {
			panic(err)
		}
	}
	tt := time.Since(st)
	pt := tt / time.Duration(requestNum)
	fmt.Println(pt)
}

// コネクションを生成
func genConn(network, address string) (*grpc.ClientConn, error) {
	switch network {
	case "tcp":
		// Dial を指定しない場合はでデフォルトでTCP.
		return grpc.Dial(address, grpc.WithInsecure())

	case "unix":
		// dialer を作成して grpc.WithDialer(dialer)でDialオプションを追加する.
		dialer := func(a string, t time.Duration) (net.Conn, error) {
			return net.Dial(network, a)
		}
		return grpc.Dial(address, grpc.WithInsecure(), grpc.WithDialer(dialer))
	default:
		panic("invalid network")
	}
}