【Network】Benchmarking Unix Domain Socket

Posted by 西维蜀黍 on 2021-07-16, Last Modified on 2021-10-17

Benchmarking UDS vs loop-back TCP sockets

Redis

  • When the server and client benchmark programs run on the same box, both the TCP/IP loopback and unix domain sockets can be used. Depending on the platform, unix domain sockets can achieve around 50% more throughput than the TCP/IP loopback (on Linux for instance). The default behavior of redis-benchmark is to use the TCP/IP loopback.
  • The performance benefit of unix domain sockets compared to TCP/IP loopback tends to decrease when pipelining is heavily used (i.e. long pipelines).

Refer to https://redis.io/topics/benchmarks

Benchmark in C

Benchmark in Golang

Latency

  • UDS: 100000 pingpongs took 454640274 ns; avg. latency 4546 ns
  • TCP: 100000 pingpongs took 1474698971 ns; avg. latency 14746 ns

https://github.com/eliben/code-for-blog/blob/master/2019/unix-domain-sockets-go/local-latency-benchmark.go

// Latency benchmark for comparing Unix sockets with TCP sockets.
//
// Idea: ping-pong 128-byte packets between a goroutine acting as a server and
// main acting as client. Measure how long it took to do 2*N ping-pongs and find
// the average latency.
//
// Eli Bendersky [http://eli.thegreenplace.net]
// This code is in the public domain.
package main

import (
	"flag"
	"fmt"
	"log"
	"net"
	"os"
	"time"
)

var UnixDomain = flag.Bool("unixdomain", false, "Use Unix domain sockets")
var MsgSize = flag.Int("msgsize", 128, "Message size in each ping")
var NumPings = flag.Int("n", 50000, "Number of pings to measure")

var TcpAddress = "127.0.0.1:13500"
var UnixAddress = "/tmp/benchmark.sock"

// domainAndAddress returns the domain,address pair for net functions to connect
// to, depending on the value of the UnixDomain flag.
func domainAndAddress() (string, string) {
	if *UnixDomain {
		return "unix", UnixAddress
	} else {
		return "tcp", TcpAddress
	}
}

func server() {
	if *UnixDomain {
		if err := os.RemoveAll(UnixAddress); err != nil {
			panic(err)
		}
	}

	domain, address := domainAndAddress()
	l, err := net.Listen(domain, address)
	if err != nil {
		log.Fatal(err)
	}
	defer l.Close()

	conn, err := l.Accept()
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	buf := make([]byte, *MsgSize)
	for n := 0; n < *NumPings; n++ {
		nread, err := conn.Read(buf)
		if err != nil {
			log.Fatal(err)
		}
		if nread != *MsgSize {
			log.Fatalf("bad nread = %d", nread)
		}
		nwrite, err := conn.Write(buf)
		if err != nil {
			log.Fatal(err)
		}
		if nwrite != *MsgSize {
			log.Fatalf("bad nwrite = %d", nwrite)
		}
	}

	time.Sleep(50 * time.Millisecond)
}

func main() {
	flag.Parse()

	go server()
	time.Sleep(50 * time.Millisecond)

	// This is the client code in the main goroutine.
	domain, address := domainAndAddress()
	conn, err := net.Dial(domain, address)
	if err != nil {
		log.Fatal(err)
	}

	buf := make([]byte, *MsgSize)
	t1 := time.Now()
	for n := 0; n < *NumPings; n++ {
		nwrite, err := conn.Write(buf)
		if err != nil {
			log.Fatal(err)
		}
		if nwrite != *MsgSize {
			log.Fatalf("bad nwrite = %d", nwrite)
		}
		nread, err := conn.Read(buf)
		if err != nil {
			log.Fatal(err)
		}
		if nread != *MsgSize {
			log.Fatalf("bad nread = %d", nread)
		}
	}
	elapsed := time.Since(t1)

	totalpings := int64(*NumPings * 2)
	fmt.Println("Client done")
	fmt.Printf("%d pingpongs took %d ns; avg. latency %d ns\n",
		totalpings, elapsed.Nanoseconds(),
		elapsed.Nanoseconds()/totalpings)

	time.Sleep(50 * time.Millisecond)
}

Throughput

  • UDS: Sent 10000 msg in 2097765206 ns; throughput 4766 msg/sec (1249 MB/sec)
  • TCP: Sent 10000 msg in 623791205 ns; throughput 16031 msg/sec (4202 MB/sec)
// Throughput benchmark for Unix sockets with TCP sockets.
//
// Sends large packets from client to server and measures how long each send
// took.
//
// Eli Bendersky [http://eli.thegreenplace.net]
// This code is in the public domain.
package main

import (
	"flag"
	"fmt"
	"log"
	"net"
	"os"
	"time"
)

var TcpAddress = "127.0.0.1:13500"
var UnixAddress = "/tmp/benchmark.sock"

var UnixDomain = flag.Bool("unixdomain", true, "Use Unix domain sockets")
var MsgSize = flag.Int("msgsize", 256*1024, "Size of each message")
var NumMsg = flag.Int("n", 10000, "Number of messages to send")

// domainAndAddress returns the domain,address pair for net functions to connect
// to, depending on the value of the UnixDomain flag.
func domainAndAddress() (string, string) {
	if *UnixDomain {
		return "unix", UnixAddress
	} else {
		return "tcp", TcpAddress
	}
}

func server() {
	if *UnixDomain {
		if err := os.RemoveAll(UnixAddress); err != nil {
			panic(err)
		}
	}

	domain, address := domainAndAddress()
	l, err := net.Listen(domain, address)
	if err != nil {
		log.Fatal(err)
	}
	defer l.Close()

	conn, err := l.Accept()
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	buf := make([]byte, *MsgSize)
	for {
		nread, err := conn.Read(buf)
		if err != nil {
			log.Fatal(err)
		}
		if nread == 0 {
			break
		}
	}

	time.Sleep(50 * time.Millisecond)
}

func main() {
	flag.Parse()

	go server()
	time.Sleep(50 * time.Millisecond)

	// This is the client code in the main goroutine.
	domain, address := domainAndAddress()
	conn, err := net.Dial(domain, address)
	if err != nil {
		log.Fatal(err)
	}

	buf := make([]byte, *MsgSize)
	t1 := time.Now()
	for n := 0; n < *NumMsg; n++ {
		nwrite, err := conn.Write(buf)
		if err != nil {
			log.Fatal(err)
		}
		if nwrite != *MsgSize {
			log.Fatalf("bad nwrite = %d", nwrite)
		}
	}
	elapsed := time.Since(t1)

	totaldata := int64(*NumMsg * *MsgSize)
	fmt.Println("Client done")
	fmt.Printf("Sent %d msg in %d ns; throughput %d msg/sec (%d MB/sec)\n",
		*NumMsg, elapsed,
		(int64(*NumMsg)*1000000000)/elapsed.Nanoseconds(),
		(totaldata*1000)/elapsed.Nanoseconds())

	time.Sleep(50 * time.Millisecond)
}

Refer to https://github.com/eliben/code-for-blog/blob/master/2019/unix-domain-sockets-go/local-throughput-benchmark.go

Reference