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") } }