go 1.12 では Windows にも Unix ドメインソケットが入るので簡単に使ってみた。

参考

qiita.com

go 1.12 beta1 のインストール

ここ(Google グループ) に書いてある通りにダウンロード。

> go get golang.org/dl/go1.12beta1 
> go1.12beta1 download 

簡単にインストールできるし、Windows でもコマンドが同じなのが便利。

ベンチマーク

単純にサーバーとクライアントを作るのもつまらないので、go test -bench で通信のベンチマークをとってみる

サーバーサイド

net.Listen に渡す引数を tcp から unix に変えて、IPアドレスとポートの代わりにUNIXドメインソケット用ファイルをアドレスとして渡すだけ。

package main

import (
    "log"
    "net"
    "os"
    "path/filepath"
)

var ok = []byte("ok")
var bufSize = 65537

func main() {
    proto := "unix"
    addr := filepath.Join(os.TempDir(), "unixdomainsocketsample")
    os.Remove(addr)

    listener, _ := net.Listen(proto, addr)
    defer listener.Close()

    for {
        func() {
            conn, err := listener.Accept()
            if err != nil {
                log.Fatal(err)
            }
            defer conn.Close()
            buf := make([]byte, bufSize)
            for {
                for {
                    n, err := conn.Read(buf)
                    if err != nil {
                        return
                    }
                    // 終端をチェック
                    if buf[n-1] == '\n' {
                        break
                    }
                }
                conn.Write(ok)
            }
        }()
    }
}

クライアントサイド

ベンチマーク用関数を定義。送信サイズごとに計測

package main

import (
    "log"
    "net"
    "os"
    "path/filepath"
    "strings"
    "testing"
)

func setup() net.Conn {
    proto := "unix"
    addr := filepath.Join(os.TempDir(), "unixdomainsocketsample")
    conn, err := net.Dial(proto, addr)
    if err != nil {
        panic(err)
    }
    return conn
}

func sendBase(conn net.Conn, times int, data []byte) {
    buf := make([]byte, 2)
    for i := 0; i < times; i++ {
        _, err := conn.Write(data)
        if err != nil {
            log.Println(err)
            return
        }
        conn.Read(buf)
    }
}

func BenchmarkSend1Char(b *testing.B) {
    conn := setup()
    defer conn.Close()
    benchmarks := []struct {
        name string
        data []byte
    }{
        {"00001-char", []byte(strings.Repeat("a", 1))},
        {"00127-char", []byte(strings.Repeat("a", 127))},
        {"00256-char", []byte(strings.Repeat("a", 256))},
        {"00512-char", []byte(strings.Repeat("a", 512))},
        {"01024-char", []byte(strings.Repeat("a", 1024))},
        {"02048-char", []byte(strings.Repeat("a", 2048))},
        {"04096-char", []byte(strings.Repeat("a", 4096))},
        {"08192-char", []byte(strings.Repeat("a", 8192))},
        {"16384-char", []byte(strings.Repeat("a", 16384))},
        {"32768-char", []byte(strings.Repeat("a", 32768))},
        {"65536-char", []byte(strings.Repeat("a", 65536))},
        {"131072-char", []byte(strings.Repeat("a", 131072))},
        {"262144-char", []byte(strings.Repeat("a", 262144))},
    }
    for _, bm := range benchmarks {
        var bmData []byte
        b.Run(bm.name, func(b *testing.B) {
            bmData = append(bm.data, '\n') // 終端追加
            b.ResetTimer()
            sendBase(conn, b.N, bmData)
        })
    }
}

計測

計測方法は、サーバー側を起動した後、ベンチコマンドをクライアント側で実行する。

サーバー側実行

> go1.12beta1 build -o server.exe server.go & server.exe

クライアント側

> go1.12beta1 test -bench .

結果

TCP、名前付きパイプとの実行結果を合わせて表にしてみた。TCPは標準の netパッケージを使用。名前付きパイプはMicroSoft/winio(https://github.com/Microsoft/go-winio) の実装を使用 () の中はサーバー側のバッファサイズ size は 送信データのサイズ

size Unix(8192) Unix(65537) Tcp(8192) Tcp(65537) Pipe(8192) Pipe(65537)
1 bytes 19900 ns/op 26439 ns/op 74849 ns/op 77649 ns/op 60250 ns/op 58899 ns/op
127 bytes 19230 ns/op 26299 ns/op 74449 ns/op 77105 ns/op 59733 ns/op 58099 ns/op
256 bytes 19319 ns/op 26499 ns/op 75199 ns/op 77009 ns/op 61233 ns/op 56000 ns/op
512 bytes 19790 ns/op 26119 ns/op 76249 ns/op 77757 ns/op 57700 ns/op 58233 ns/op
1024 bytes 21549 ns/op 27120 ns/op 76349 ns/op 77599 ns/op 60200 ns/op 57533 ns/op
2048 bytes 19020 ns/op 33019 ns/op 86399 ns/op 87699 ns/op 60050 ns/op 57549 ns/op
4096 bytes 25670 ns/op 31639 ns/op 93450 ns/op 96149 ns/op 60600 ns/op 59499 ns/op
8192 bytes 39780 ns/op 34859 ns/op 119399 ns/op 119499 ns/op 61300 ns/op 60166 ns/op
16384 bytes 44699 ns/op 33139 ns/op 169699 ns/op 167199 ns/op 65550 ns/op 63899 ns/op
32768 bytes 62300 ns/op 40339 ns/op 270600 ns/op 259000 ns/op 73649 ns/op 64999 ns/op
65536 bytes 84750 ns/op 44933 ns/op 496664 ns/op 493668 ns/op 86299 ns/op 75300 ns/op
131072 bytes 129299 ns/op 80049 ns/op 905476 ns/op 908032 ns/op 113600 ns/op 80750 ns/op
262144 bytes 213400 ns/op 119999 ns/op 1828996 ns/op 1816518 ns/op 188400 ns/op 117900 ns/op

f:id:bamch0h:20181222141600p:plain
ベンチ比較(Unix, Tcp, Pipe)

TCPUNIXドメインソケットの差が大きいのでTcp(8192)Tpc(65536)を除外してグラフ化

f:id:bamch0h:20181222141626p:plain
ベンチ比較(Unix, Pipe)

Unix ドメインソケット同士の比較だと 4096と8192の間ぐらいで速度が反転する。大きいデータを送信する場合はそのあたりを基準にチューニングする必要があるのかな? パイプとUnix ドメインソケットの比較だと、基本的にはUnixドメインソケットのほうが早いようだ。ただ、サイズが大きくなると速度が同等になるので、大きいデータを一括で送信するようなケースの場合はパイプのほうがいいのかもしれない。

まとめ

今回はUNIXドメインソケットをGO言語で使ってみた。 使うだけだとつまらないのでパフォーマンスを測定し、TCP、名前付きパイプとの速度と比較してみた。 UNIXドメインソケットはTCPより速いみたいなので、内部的な通信ではUNIXドメインソケットを使うほうがいいのかもしれない。 また、Unixドメインソケットはパイプより速いが、大きなデータを一括で送信する場合はパイプでも速度的な違いはない。

今後の展開

Go言語とそれ以外の言語で通信できるようなら色々楽しそうなので そういうことにもチャレンジしていきたい。