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
// 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)
}
Reference
- https://eli.thegreenplace.net/2019/unix-domain-sockets-in-go/
- https://redis.io/topics/benchmarks
- https://github.com/rigtorp/ipc-bench/
- https://github.com/eliben/code-for-blog/blob/master/2019/unix-domain-sockets-go/local-latency-benchmark.go