From 8b8880ee3e1cef2b95e5fb3e67bde4beb4066830 Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Sun, 9 Feb 2020 16:40:51 +0100 Subject: [PATCH 01/18] Add prefork util --- prefork/prefork.go | 181 ++++++++++++++++++++++++++++++++++++++++ prefork/prefork_test.go | 162 +++++++++++++++++++++++++++++++++++ 2 files changed, 343 insertions(+) create mode 100644 prefork/prefork.go create mode 100644 prefork/prefork_test.go diff --git a/prefork/prefork.go b/prefork/prefork.go new file mode 100644 index 0000000000..ad83c98adb --- /dev/null +++ b/prefork/prefork.go @@ -0,0 +1,181 @@ +package prefork + +import ( + "flag" + "net" + "os" + "os/exec" + "runtime" + + "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/reuseport" +) + +const preforkChildFlag = "prefork-child" +const defaultNetwork = "tcp4" + +var Child bool + +// Prefork implements fasthttp server prefork +// +// Preforks master process (with all cores) between several child processes +// increases performance significantly, because Go doesn't have to share +// and manage memory between cores +// +// WARNING: Does not recommended for servers with in-memory cache, +// because the cache will be duplicated in each process +type Prefork struct { + Addr string + + // The network must be "tcp", "tcp4" or "tcp6". + // + // By default is "tcp4" + Network string + + // Flag to use a listener with reuseport, if not a File Listener will be used + // + // It's disabled by default + Reuseport bool + + ServeFunc func(ln net.Listener) error + ServeTLSFunc func(ln net.Listener, certFile, keyFile string) error + ServeTLSEmbedFunc func(ln net.Listener, certData, keyData []byte) error + + ln net.Listener + files []*os.File +} + +func init() { // nolint:gochecknoinits + flag.BoolVar(&Child, preforkChildFlag, false, "is child proc") +} + +// New wraps the fasthttp server to run with prefork processes +func New(s *fasthttp.Server) *Prefork { + return &Prefork{ + Network: defaultNetwork, + ServeFunc: s.Serve, + ServeTLSFunc: s.ServeTLS, + ServeTLSEmbedFunc: s.ServeTLSEmbed, + } +} + +func (p *Prefork) listen() (net.Listener, error) { + runtime.GOMAXPROCS(1) + + if p.Network == "" { + p.Network = defaultNetwork + } + + if p.Reuseport { + return reuseport.Listen(p.Network, p.Addr) + } + + return net.FileListener(os.NewFile(3, "")) +} + +func (p *Prefork) setTCPListenerFiles() error { + addr, err := net.ResolveTCPAddr(p.Network, p.Addr) + if err != nil { + return err + } + + tcplistener, err := net.ListenTCP(p.Network, addr) + if err != nil { + return err + } + + p.ln = tcplistener + + fl, err := tcplistener.File() + if err != nil { + return err + } + + p.files = []*os.File{fl} + + return nil +} + +func (p *Prefork) prefork() error { + strCmd := os.Args[0] + chErr := make(chan error, 1) + + if !p.Reuseport { + if err := p.setTCPListenerFiles(); err != nil { + return err + } + + defer p.ln.Close() + } + + for i := 0; i < runtime.NumCPU(); i++ { + cmd := exec.Command(strCmd, append(os.Args[1:], "-"+preforkChildFlag)...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.ExtraFiles = p.files + + go func() { + chErr <- cmd.Run() + }() + } + + return <-chErr +} + +// ListenAndServe serves HTTP requests from the given TCP addr +func (p *Prefork) ListenAndServe(addr string) error { + p.Addr = addr + + if Child { + ln, err := p.listen() + if err != nil { + return err + } + + p.ln = ln + + return p.ServeFunc(ln) + } + + return p.prefork() +} + +// ListenAndServeTLS serves HTTPS requests from the given TCP addr +// +// certFile and keyFile are paths to TLS certificate and key files. +func (p *Prefork) ListenAndServeTLS(addr, certKey, certFile string) error { + p.Addr = addr + + if Child { + ln, err := p.listen() + if err != nil { + return err + } + + p.ln = ln + + return p.ServeTLSFunc(ln, certFile, certKey) + } + + return p.prefork() +} + +// ListenAndServeTLSEmbed serves HTTPS requests from the given TCP addr +// +// certData and keyData must contain valid TLS certificate and key data. +func (p *Prefork) ListenAndServeTLSEmbed(addr string, certData, keyData []byte) error { + p.Addr = addr + + if Child { + ln, err := p.listen() + if err != nil { + return err + } + + p.ln = ln + + return p.ServeTLSEmbedFunc(ln, certData, keyData) + } + + return p.prefork() +} diff --git a/prefork/prefork_test.go b/prefork/prefork_test.go new file mode 100644 index 0000000000..1703582fbb --- /dev/null +++ b/prefork/prefork_test.go @@ -0,0 +1,162 @@ +package prefork + +import ( + "fmt" + "math/rand" + "net" + "reflect" + "runtime" + "testing" + + "github.com/valyala/fasthttp" +) + +func getAddr() string { + Child = true + + return fmt.Sprintf(":%d", rand.Intn(9000-3000)+3000) +} + +func Test_New(t *testing.T) { + s := &fasthttp.Server{} + p := New(s) + + if p.Network != defaultNetwork { + t.Errorf("Prefork.Netork == %s, want %s", p.Network, defaultNetwork) + } + + if reflect.ValueOf(p.ServeFunc).Pointer() != reflect.ValueOf(s.Serve).Pointer() { + t.Errorf("Prefork.ServeFunc == %p, want %p", p.ServeFunc, s.Serve) + } + + if reflect.ValueOf(p.ServeTLSFunc).Pointer() != reflect.ValueOf(s.ServeTLS).Pointer() { + t.Errorf("Prefork.ServeTLSFunc == %p, want %p", p.ServeTLSFunc, s.ServeTLS) + } + + if reflect.ValueOf(p.ServeTLSEmbedFunc).Pointer() != reflect.ValueOf(s.ServeTLSEmbed).Pointer() { + t.Errorf("Prefork.ServeTLSFunc == %p, want %p", p.ServeTLSEmbedFunc, s.ServeTLSEmbed) + } +} + +func Test_listen(t *testing.T) { + p := &Prefork{ + Addr: getAddr(), + Reuseport: true, + } + ln, err := p.listen() + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + ln.Close() + + if p.Network != defaultNetwork { + t.Errorf("Prefork.Network == %s, want %s", p.Network, defaultNetwork) + } + + procs := runtime.GOMAXPROCS(0) + if procs != 1 { + t.Errorf("GOMAXPROCS == %d, want %d", procs, 1) + } +} + +func Test_setTCPListenerFiles(t *testing.T) { + p := &Prefork{ + Addr: getAddr(), + Network: defaultNetwork, + } + err := p.setTCPListenerFiles() + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if p.ln == nil { + t.Fatal("Prefork.ln is nil") + } + + p.ln.Close() + + if len(p.files) != 1 { + t.Errorf("Prefork.files == %d, want %d", len(p.files), 1) + } +} + +func Test_ListenAndServe(t *testing.T) { + s := &fasthttp.Server{} + p := New(s) + p.Reuseport = true + p.ServeFunc = func(ln net.Listener) error { + return nil + } + + addr := getAddr() + + err := p.ListenAndServe(addr) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + p.ln.Close() + + if p.Addr != addr { + t.Errorf("Prefork.Addr == %s, want %s", p.Addr, addr) + } + + if p.ln == nil { + t.Error("Prefork.ln is nil") + } +} + +func Test_ListenAndServeTLS(t *testing.T) { + s := &fasthttp.Server{} + p := New(s) + p.Reuseport = true + p.ServeTLSFunc = func(ln net.Listener, certFile, keyFile string) error { + return nil + } + + addr := getAddr() + + err := p.ListenAndServeTLS(addr, "./key", "./cert") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + p.ln.Close() + + if p.Addr != addr { + t.Errorf("Prefork.Addr == %s, want %s", p.Addr, addr) + } + + if p.ln == nil { + t.Error("Prefork.ln is nil") + } +} + +func Test_ListenAndServeTLSEmbed(t *testing.T) { + s := &fasthttp.Server{} + p := New(s) + p.Reuseport = true + p.ServeTLSEmbedFunc = func(ln net.Listener, certData, keyData []byte) error { + return nil + } + + addr := getAddr() + + err := p.ListenAndServeTLSEmbed(addr, []byte("key"), []byte("cert")) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + p.ln.Close() + + if p.Addr != addr { + t.Errorf("Prefork.Addr == %s, want %s", p.Addr, addr) + } + + if p.ln == nil { + t.Error("Prefork.ln is nil") + } +} From 18d765b23b6a4e533db9f40442e91092a08a32dd Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Sun, 9 Feb 2020 16:53:31 +0100 Subject: [PATCH 02/18] Fix --- prefork/prefork.go | 25 +++++++++++-------------- prefork/prefork_test.go | 9 +++++++-- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/prefork/prefork.go b/prefork/prefork.go index ad83c98adb..8d0990be3a 100644 --- a/prefork/prefork.go +++ b/prefork/prefork.go @@ -59,7 +59,9 @@ func New(s *fasthttp.Server) *Prefork { } } -func (p *Prefork) listen() (net.Listener, error) { +func (p *Prefork) listen(addr string) (net.Listener, error) { + p.Addr = addr + runtime.GOMAXPROCS(1) if p.Network == "" { @@ -96,7 +98,8 @@ func (p *Prefork) setTCPListenerFiles() error { return nil } -func (p *Prefork) prefork() error { +func (p *Prefork) prefork(addr string) error { + p.Addr = addr strCmd := os.Args[0] chErr := make(chan error, 1) @@ -124,10 +127,8 @@ func (p *Prefork) prefork() error { // ListenAndServe serves HTTP requests from the given TCP addr func (p *Prefork) ListenAndServe(addr string) error { - p.Addr = addr - if Child { - ln, err := p.listen() + ln, err := p.listen(addr) if err != nil { return err } @@ -137,17 +138,15 @@ func (p *Prefork) ListenAndServe(addr string) error { return p.ServeFunc(ln) } - return p.prefork() + return p.prefork(addr) } // ListenAndServeTLS serves HTTPS requests from the given TCP addr // // certFile and keyFile are paths to TLS certificate and key files. func (p *Prefork) ListenAndServeTLS(addr, certKey, certFile string) error { - p.Addr = addr - if Child { - ln, err := p.listen() + ln, err := p.listen(addr) if err != nil { return err } @@ -157,17 +156,15 @@ func (p *Prefork) ListenAndServeTLS(addr, certKey, certFile string) error { return p.ServeTLSFunc(ln, certFile, certKey) } - return p.prefork() + return p.prefork(addr) } // ListenAndServeTLSEmbed serves HTTPS requests from the given TCP addr // // certData and keyData must contain valid TLS certificate and key data. func (p *Prefork) ListenAndServeTLSEmbed(addr string, certData, keyData []byte) error { - p.Addr = addr - if Child { - ln, err := p.listen() + ln, err := p.listen(addr) if err != nil { return err } @@ -177,5 +174,5 @@ func (p *Prefork) ListenAndServeTLSEmbed(addr string, certData, keyData []byte) return p.ServeTLSEmbedFunc(ln, certData, keyData) } - return p.prefork() + return p.prefork(addr) } diff --git a/prefork/prefork_test.go b/prefork/prefork_test.go index 1703582fbb..dfa47d892f 100644 --- a/prefork/prefork_test.go +++ b/prefork/prefork_test.go @@ -40,10 +40,11 @@ func Test_New(t *testing.T) { func Test_listen(t *testing.T) { p := &Prefork{ - Addr: getAddr(), Reuseport: true, } - ln, err := p.listen() + addr := getAddr() + + ln, err := p.listen(addr) if err != nil { t.Fatalf("Unexpected error: %v", err) @@ -51,6 +52,10 @@ func Test_listen(t *testing.T) { ln.Close() + if p.Addr != addr { + t.Errorf("Prefork.Addr == %s, want %s", p.Addr, addr) + } + if p.Network != defaultNetwork { t.Errorf("Prefork.Network == %s, want %s", p.Network, defaultNetwork) } From fae1d8e3d733858faafb5f8dffc6ce8c8d89c213 Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Sun, 9 Feb 2020 16:59:24 +0100 Subject: [PATCH 03/18] Fix --- prefork/prefork.go | 11 ++++++----- prefork/prefork_test.go | 9 +++++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/prefork/prefork.go b/prefork/prefork.go index 8d0990be3a..53461af9a8 100644 --- a/prefork/prefork.go +++ b/prefork/prefork.go @@ -75,13 +75,15 @@ func (p *Prefork) listen(addr string) (net.Listener, error) { return net.FileListener(os.NewFile(3, "")) } -func (p *Prefork) setTCPListenerFiles() error { - addr, err := net.ResolveTCPAddr(p.Network, p.Addr) +func (p *Prefork) setTCPListenerFiles(addr string) error { + p.Addr = addr + + tcpAddr, err := net.ResolveTCPAddr(p.Network, p.Addr) if err != nil { return err } - tcplistener, err := net.ListenTCP(p.Network, addr) + tcplistener, err := net.ListenTCP(p.Network, tcpAddr) if err != nil { return err } @@ -99,12 +101,11 @@ func (p *Prefork) setTCPListenerFiles() error { } func (p *Prefork) prefork(addr string) error { - p.Addr = addr strCmd := os.Args[0] chErr := make(chan error, 1) if !p.Reuseport { - if err := p.setTCPListenerFiles(); err != nil { + if err := p.setTCPListenerFiles(addr); err != nil { return err } diff --git a/prefork/prefork_test.go b/prefork/prefork_test.go index dfa47d892f..a117de69d8 100644 --- a/prefork/prefork_test.go +++ b/prefork/prefork_test.go @@ -68,15 +68,20 @@ func Test_listen(t *testing.T) { func Test_setTCPListenerFiles(t *testing.T) { p := &Prefork{ - Addr: getAddr(), Network: defaultNetwork, } - err := p.setTCPListenerFiles() + addr := getAddr() + + err := p.setTCPListenerFiles(addr) if err != nil { t.Fatalf("Unexpected error: %v", err) } + if p.Addr != addr { + t.Errorf("Prefork.Addr == %s, want %s", p.Addr, addr) + } + if p.ln == nil { t.Fatal("Prefork.ln is nil") } From 77bdaf9fb2251617d0e954db067d305dd99db927 Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Mon, 10 Feb 2020 09:45:30 +0100 Subject: [PATCH 04/18] Fixes --- prefork/prefork.go | 16 ++++++++-------- prefork/prefork_test.go | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/prefork/prefork.go b/prefork/prefork.go index 53461af9a8..c38e79d1fc 100644 --- a/prefork/prefork.go +++ b/prefork/prefork.go @@ -14,7 +14,7 @@ import ( const preforkChildFlag = "prefork-child" const defaultNetwork = "tcp4" -var Child bool +var child bool // Prefork implements fasthttp server prefork // @@ -22,8 +22,8 @@ var Child bool // increases performance significantly, because Go doesn't have to share // and manage memory between cores // -// WARNING: Does not recommended for servers with in-memory cache, -// because the cache will be duplicated in each process +// WARNING: using prefork prevents the use of any global state! +// Things like in-memory caches won't work. type Prefork struct { Addr string @@ -46,7 +46,7 @@ type Prefork struct { } func init() { // nolint:gochecknoinits - flag.BoolVar(&Child, preforkChildFlag, false, "is child proc") + flag.BoolVar(&child, preforkChildFlag, false, "is child proc") } // New wraps the fasthttp server to run with prefork processes @@ -112,7 +112,7 @@ func (p *Prefork) prefork(addr string) error { defer p.ln.Close() } - for i := 0; i < runtime.NumCPU(); i++ { + for i := 0; i < runtime.GOMAXPROCS(0); i++ { cmd := exec.Command(strCmd, append(os.Args[1:], "-"+preforkChildFlag)...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -128,7 +128,7 @@ func (p *Prefork) prefork(addr string) error { // ListenAndServe serves HTTP requests from the given TCP addr func (p *Prefork) ListenAndServe(addr string) error { - if Child { + if child { ln, err := p.listen(addr) if err != nil { return err @@ -146,7 +146,7 @@ func (p *Prefork) ListenAndServe(addr string) error { // // certFile and keyFile are paths to TLS certificate and key files. func (p *Prefork) ListenAndServeTLS(addr, certKey, certFile string) error { - if Child { + if child { ln, err := p.listen(addr) if err != nil { return err @@ -164,7 +164,7 @@ func (p *Prefork) ListenAndServeTLS(addr, certKey, certFile string) error { // // certData and keyData must contain valid TLS certificate and key data. func (p *Prefork) ListenAndServeTLSEmbed(addr string, certData, keyData []byte) error { - if Child { + if child { ln, err := p.listen(addr) if err != nil { return err diff --git a/prefork/prefork_test.go b/prefork/prefork_test.go index a117de69d8..099b1f7116 100644 --- a/prefork/prefork_test.go +++ b/prefork/prefork_test.go @@ -12,7 +12,7 @@ import ( ) func getAddr() string { - Child = true + child = true return fmt.Sprintf(":%d", rand.Intn(9000-3000)+3000) } From 082e7156bae1a5cc9a70c731984b8c2e5bfab283 Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Mon, 10 Feb 2020 12:22:35 +0100 Subject: [PATCH 05/18] Add README --- prefork/README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 prefork/README.md diff --git a/prefork/README.md b/prefork/README.md new file mode 100644 index 0000000000..e18377c81f --- /dev/null +++ b/prefork/README.md @@ -0,0 +1,27 @@ +# Prefork + +Server prefork implementation. + +Preforks master process between several child processes increases performance, because Go doesn't have to share and manage memory between cores. + +**WARNING: using prefork prevents the use of any global state!. Things like in-memory caches won't work.** + +- How it works: + +```go +import ( + "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/prefork" +) + +server := &fasthttp.Server{ + // Your configuration +} + +// Wraps the server with prefork +preforkServer := prefork.New(server) + +if err := preforkServer.ListenAndServe(":8080"); err != nil { + panic(err) +} +``` From 6dacc602bb1a3a0f59b7ba8fcdbfaa190b01d31d Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Mon, 10 Feb 2020 12:25:19 +0100 Subject: [PATCH 06/18] Fix typo --- prefork/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prefork/README.md b/prefork/README.md index e18377c81f..b4831c3c81 100644 --- a/prefork/README.md +++ b/prefork/README.md @@ -11,7 +11,7 @@ Preforks master process between several child processes increases performance, b ```go import ( "github.com/valyala/fasthttp" - "github.com/valyala/fasthttp/prefork" + "github.com/valyala/fasthttp/prefork" ) server := &fasthttp.Server{ From c9f06d0d7ecbe39c06ba3da707e937ca3ea8503b Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Mon, 10 Feb 2020 12:59:55 +0100 Subject: [PATCH 07/18] Update README --- prefork/README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/prefork/README.md b/prefork/README.md index b4831c3c81..38b4148c34 100644 --- a/prefork/README.md +++ b/prefork/README.md @@ -25,3 +25,52 @@ if err := preforkServer.ListenAndServe(":8080"); err != nil { panic(err) } ``` + +## Benchmarks + +Environment: + +- Machine: MacBook Pro 13-inch, 2017 +- OS: MacOS 10.15.3 +- Go: go1.13.6 darwin/amd64 + +Handler code: + +```go +func requestHandler(ctx *fasthttp.RequestCtx) { + // Simulate some hard work + time.Sleep(100 * time.Millisecond) +} +``` + +Results: + +- **WITH** prefork + +```bash +$ wrk -c 1000 -t 4 -d 30s http://localhost:8080 +Running 30s test @ http://localhost:8080 + 4 threads and 1000 connections + Thread Stats Avg Stdev Max +/- Stdev + Latency 104.35ms 1.81ms 114.01ms 73.68% + Req/Sec 2.40k 198.27 2.55k 94.19% + 286846 requests in 30.03s, 25.44MB read + Socket errors: connect 0, read 62, write 0, timeout 0 +Requests/sec: 9551.60 +Transfer/sec: 867.48KB +``` + +- **WITHOUT** prefork + +```bash +$ wrk -c 1000 -t 4 -d 30s http://localhost:8080 +Running 30s test @ http://localhost:8080 + 4 threads and 1000 connections + Thread Stats Avg Stdev Max +/- Stdev + Latency 104.39ms 2.62ms 119.24ms 66.75% + Req/Sec 2.13k 581.16 2.65k 83.23% + 253666 requests in 30.06s, 22.50MB read + Socket errors: connect 0, read 903, write 88, timeout 0 +Requests/sec: 8439.21 +Transfer/sec: 766.45KB +``` From 634c07f47e5431be13aaa7d674e6512586d8d184 Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Mon, 10 Feb 2020 13:01:30 +0100 Subject: [PATCH 08/18] Fix typo --- prefork/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prefork/README.md b/prefork/README.md index 38b4148c34..d0744cc1cb 100644 --- a/prefork/README.md +++ b/prefork/README.md @@ -38,8 +38,8 @@ Handler code: ```go func requestHandler(ctx *fasthttp.RequestCtx) { - // Simulate some hard work - time.Sleep(100 * time.Millisecond) + // Simulates some hard work + time.Sleep(100 * time.Millisecond) } ``` From 5fd2cf7b7f5826a794208a2e3ead54968e19985d Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Tue, 11 Feb 2020 09:24:24 +0100 Subject: [PATCH 09/18] Update README --- prefork/README.md | 54 +++++++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/prefork/README.md b/prefork/README.md index d0744cc1cb..31f2c43c31 100644 --- a/prefork/README.md +++ b/prefork/README.md @@ -43,34 +43,48 @@ func requestHandler(ctx *fasthttp.RequestCtx) { } ``` +Test command: + +```bash +$ wrk -H 'Host: localhost' -H 'Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7' -H 'Connection: keep-alive' --latency -d 15 -c 512 --timeout 8 -t 4 http://localhost:8080 +``` + Results: -- **WITH** prefork +- prefork ```bash -$ wrk -c 1000 -t 4 -d 30s http://localhost:8080 -Running 30s test @ http://localhost:8080 - 4 threads and 1000 connections +Running 15s test @ http://localhost:8080 + 4 threads and 512 connections Thread Stats Avg Stdev Max +/- Stdev - Latency 104.35ms 1.81ms 114.01ms 73.68% - Req/Sec 2.40k 198.27 2.55k 94.19% - 286846 requests in 30.03s, 25.44MB read - Socket errors: connect 0, read 62, write 0, timeout 0 -Requests/sec: 9551.60 -Transfer/sec: 867.48KB + Latency 4.75ms 4.27ms 126.24ms 97.45% + Req/Sec 26.46k 4.16k 71.18k 88.72% + Latency Distribution + 50% 4.55ms + 75% 4.82ms + 90% 5.46ms + 99% 15.49ms + 1581916 requests in 15.09s, 140.30MB read + Socket errors: connect 0, read 318, write 0, timeout 0 +Requests/sec: 104861.58 +Transfer/sec: 9.30MB ``` -- **WITHOUT** prefork +- **non**-prefork ```bash -$ wrk -c 1000 -t 4 -d 30s http://localhost:8080 -Running 30s test @ http://localhost:8080 - 4 threads and 1000 connections +Running 15s test @ http://localhost:8080 + 4 threads and 512 connections Thread Stats Avg Stdev Max +/- Stdev - Latency 104.39ms 2.62ms 119.24ms 66.75% - Req/Sec 2.13k 581.16 2.65k 83.23% - 253666 requests in 30.06s, 22.50MB read - Socket errors: connect 0, read 903, write 88, timeout 0 -Requests/sec: 8439.21 -Transfer/sec: 766.45KB + Latency 6.42ms 11.83ms 177.19ms 96.42% + Req/Sec 24.96k 5.83k 56.83k 82.93% + Latency Distribution + 50% 4.53ms + 75% 4.93ms + 90% 6.94ms + 99% 74.54ms + 1472441 requests in 15.09s, 130.59MB read + Socket errors: connect 0, read 265, write 0, timeout 0 +Requests/sec: 97553.34 +Transfer/sec: 8.65MB ``` From 5dc4c2b896e7608c1366f5b26c2e7c7e1e862877 Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Wed, 12 Feb 2020 11:49:18 +0100 Subject: [PATCH 10/18] Check if is a child from os.Args --- prefork/prefork.go | 27 ++++++++++++++++++--------- prefork/prefork_test.go | 31 +++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/prefork/prefork.go b/prefork/prefork.go index c38e79d1fc..9eb446e2f9 100644 --- a/prefork/prefork.go +++ b/prefork/prefork.go @@ -11,11 +11,9 @@ import ( "github.com/valyala/fasthttp/reuseport" ) -const preforkChildFlag = "prefork-child" +const preforkChildFlag = "-prefork-child" const defaultNetwork = "tcp4" -var child bool - // Prefork implements fasthttp server prefork // // Preforks master process (with all cores) between several child processes @@ -45,8 +43,19 @@ type Prefork struct { files []*os.File } -func init() { // nolint:gochecknoinits - flag.BoolVar(&child, preforkChildFlag, false, "is child proc") +func init() { //nolint:gochecknoinits + flag.Bool(preforkChildFlag[1:], false, "is a child process") +} + +// IsChild checks if the current thread/process is a child +func IsChild() bool { + for _, arg := range os.Args[1:] { + if arg == preforkChildFlag { + return true + } + } + + return false } // New wraps the fasthttp server to run with prefork processes @@ -113,7 +122,7 @@ func (p *Prefork) prefork(addr string) error { } for i := 0; i < runtime.GOMAXPROCS(0); i++ { - cmd := exec.Command(strCmd, append(os.Args[1:], "-"+preforkChildFlag)...) + cmd := exec.Command(strCmd, append(os.Args[1:], preforkChildFlag)...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.ExtraFiles = p.files @@ -128,7 +137,7 @@ func (p *Prefork) prefork(addr string) error { // ListenAndServe serves HTTP requests from the given TCP addr func (p *Prefork) ListenAndServe(addr string) error { - if child { + if IsChild() { ln, err := p.listen(addr) if err != nil { return err @@ -146,7 +155,7 @@ func (p *Prefork) ListenAndServe(addr string) error { // // certFile and keyFile are paths to TLS certificate and key files. func (p *Prefork) ListenAndServeTLS(addr, certKey, certFile string) error { - if child { + if IsChild() { ln, err := p.listen(addr) if err != nil { return err @@ -164,7 +173,7 @@ func (p *Prefork) ListenAndServeTLS(addr, certKey, certFile string) error { // // certData and keyData must contain valid TLS certificate and key data. func (p *Prefork) ListenAndServeTLSEmbed(addr string, certData, keyData []byte) error { - if child { + if IsChild() { ln, err := p.listen(addr) if err != nil { return err diff --git a/prefork/prefork_test.go b/prefork/prefork_test.go index 099b1f7116..9a70b7abcb 100644 --- a/prefork/prefork_test.go +++ b/prefork/prefork_test.go @@ -4,6 +4,7 @@ import ( "fmt" "math/rand" "net" + "os" "reflect" "runtime" "testing" @@ -11,12 +12,38 @@ import ( "github.com/valyala/fasthttp" ) -func getAddr() string { - child = true +func init() { //nolint:gochecknoinits + setUp() +} + +func setUp() { + os.Args = append(os.Args, preforkChildFlag) +} +func tearDown() { + os.Args = os.Args[:len(os.Args)-1] +} + +func getAddr() string { return fmt.Sprintf(":%d", rand.Intn(9000-3000)+3000) } +func Test_IsChild(t *testing.T) { + v := IsChild() + if !v { + t.Errorf("IsChild() == %v, want %v", v, true) + } + + tearDown() + + v = IsChild() + if v { + t.Errorf("IsChild() == %v, want %v", v, false) + } + + setUp() +} + func Test_New(t *testing.T) { s := &fasthttp.Server{} p := New(s) From 7e62e07d0d0128ca2e5a0a30caf4e7b0acbe930b Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Wed, 12 Feb 2020 12:04:00 +0100 Subject: [PATCH 11/18] Remove Addr field --- prefork/prefork.go | 10 ++-------- prefork/prefork_test.go | 31 ++++++++++++++++++------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/prefork/prefork.go b/prefork/prefork.go index 9eb446e2f9..bb3f7ea2f8 100644 --- a/prefork/prefork.go +++ b/prefork/prefork.go @@ -23,8 +23,6 @@ const defaultNetwork = "tcp4" // WARNING: using prefork prevents the use of any global state! // Things like in-memory caches won't work. type Prefork struct { - Addr string - // The network must be "tcp", "tcp4" or "tcp6". // // By default is "tcp4" @@ -69,8 +67,6 @@ func New(s *fasthttp.Server) *Prefork { } func (p *Prefork) listen(addr string) (net.Listener, error) { - p.Addr = addr - runtime.GOMAXPROCS(1) if p.Network == "" { @@ -78,16 +74,14 @@ func (p *Prefork) listen(addr string) (net.Listener, error) { } if p.Reuseport { - return reuseport.Listen(p.Network, p.Addr) + return reuseport.Listen(p.Network, addr) } return net.FileListener(os.NewFile(3, "")) } func (p *Prefork) setTCPListenerFiles(addr string) error { - p.Addr = addr - - tcpAddr, err := net.ResolveTCPAddr(p.Network, p.Addr) + tcpAddr, err := net.ResolveTCPAddr(p.Network, addr) if err != nil { return err } diff --git a/prefork/prefork_test.go b/prefork/prefork_test.go index 9a70b7abcb..89329ae595 100644 --- a/prefork/prefork_test.go +++ b/prefork/prefork_test.go @@ -25,7 +25,7 @@ func tearDown() { } func getAddr() string { - return fmt.Sprintf(":%d", rand.Intn(9000-3000)+3000) + return fmt.Sprintf("0.0.0.0:%d", rand.Intn(9000-3000)+3000) } func Test_IsChild(t *testing.T) { @@ -79,8 +79,9 @@ func Test_listen(t *testing.T) { ln.Close() - if p.Addr != addr { - t.Errorf("Prefork.Addr == %s, want %s", p.Addr, addr) + lnAddr := ln.Addr().String() + if lnAddr != addr { + t.Errorf("Prefork.Addr == %s, want %s", lnAddr, addr) } if p.Network != defaultNetwork { @@ -105,14 +106,15 @@ func Test_setTCPListenerFiles(t *testing.T) { t.Fatalf("Unexpected error: %v", err) } - if p.Addr != addr { - t.Errorf("Prefork.Addr == %s, want %s", p.Addr, addr) - } - if p.ln == nil { t.Fatal("Prefork.ln is nil") } + lnAddr := p.ln.Addr().String() + if lnAddr != addr { + t.Errorf("Prefork.Addr == %s, want %s", lnAddr, addr) + } + p.ln.Close() if len(p.files) != 1 { @@ -137,8 +139,9 @@ func Test_ListenAndServe(t *testing.T) { p.ln.Close() - if p.Addr != addr { - t.Errorf("Prefork.Addr == %s, want %s", p.Addr, addr) + lnAddr := p.ln.Addr().String() + if lnAddr != addr { + t.Errorf("Prefork.Addr == %s, want %s", lnAddr, addr) } if p.ln == nil { @@ -163,8 +166,9 @@ func Test_ListenAndServeTLS(t *testing.T) { p.ln.Close() - if p.Addr != addr { - t.Errorf("Prefork.Addr == %s, want %s", p.Addr, addr) + lnAddr := p.ln.Addr().String() + if lnAddr != addr { + t.Errorf("Prefork.Addr == %s, want %s", lnAddr, addr) } if p.ln == nil { @@ -189,8 +193,9 @@ func Test_ListenAndServeTLSEmbed(t *testing.T) { p.ln.Close() - if p.Addr != addr { - t.Errorf("Prefork.Addr == %s, want %s", p.Addr, addr) + lnAddr := p.ln.Addr().String() + if lnAddr != addr { + t.Errorf("Prefork.Addr == %s, want %s", lnAddr, addr) } if p.ln == nil { From 0b63ce96acee9a80976c605f86d5b1f3024ef415 Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Wed, 12 Feb 2020 12:16:31 +0100 Subject: [PATCH 12/18] Set default network --- prefork/prefork.go | 4 ++++ prefork/prefork_test.go | 10 ++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/prefork/prefork.go b/prefork/prefork.go index bb3f7ea2f8..d67a21e8ae 100644 --- a/prefork/prefork.go +++ b/prefork/prefork.go @@ -81,6 +81,10 @@ func (p *Prefork) listen(addr string) (net.Listener, error) { } func (p *Prefork) setTCPListenerFiles(addr string) error { + if p.Network == "" { + p.Network = defaultNetwork + } + tcpAddr, err := net.ResolveTCPAddr(p.Network, addr) if err != nil { return err diff --git a/prefork/prefork_test.go b/prefork/prefork_test.go index 89329ae595..4f359dbbf7 100644 --- a/prefork/prefork_test.go +++ b/prefork/prefork_test.go @@ -95,9 +95,7 @@ func Test_listen(t *testing.T) { } func Test_setTCPListenerFiles(t *testing.T) { - p := &Prefork{ - Network: defaultNetwork, - } + p := &Prefork{} addr := getAddr() err := p.setTCPListenerFiles(addr) @@ -110,12 +108,16 @@ func Test_setTCPListenerFiles(t *testing.T) { t.Fatal("Prefork.ln is nil") } + p.ln.Close() + lnAddr := p.ln.Addr().String() if lnAddr != addr { t.Errorf("Prefork.Addr == %s, want %s", lnAddr, addr) } - p.ln.Close() + if p.Network != defaultNetwork { + t.Errorf("Prefork.Network == %s, want %s", p.Network, defaultNetwork) + } if len(p.files) != 1 { t.Errorf("Prefork.files == %d, want %d", len(p.files), 1) From 6a430e80d2eb807ce5259d7fe35b9027d80e671d Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Wed, 12 Feb 2020 12:24:30 +0100 Subject: [PATCH 13/18] Fix typo --- prefork/prefork.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prefork/prefork.go b/prefork/prefork.go index d67a21e8ae..5ee8b1d6ef 100644 --- a/prefork/prefork.go +++ b/prefork/prefork.go @@ -42,7 +42,7 @@ type Prefork struct { } func init() { //nolint:gochecknoinits - flag.Bool(preforkChildFlag[1:], false, "is a child process") + flag.Bool(preforkChildFlag[1:], false, "Is a child process") } // IsChild checks if the current thread/process is a child From a8c75433d1d5b0888c3a2604ef72f4737eeda83a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Andr=C3=A9s=20Virviescas=20Santana?= Date: Wed, 12 Feb 2020 12:39:04 +0100 Subject: [PATCH 14/18] Update prefork/prefork.go Co-Authored-By: Erik Dubbelboer --- prefork/prefork.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prefork/prefork.go b/prefork/prefork.go index 5ee8b1d6ef..539b81563f 100644 --- a/prefork/prefork.go +++ b/prefork/prefork.go @@ -120,7 +120,7 @@ func (p *Prefork) prefork(addr string) error { } for i := 0; i < runtime.GOMAXPROCS(0); i++ { - cmd := exec.Command(strCmd, append(os.Args[1:], preforkChildFlag)...) + cmd := exec.Command(os.Args[0], append(os.Args[1:], preforkChildFlag)...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.ExtraFiles = p.files From 49fe00b23ccd9429dbf9b35c62e729df86388fbf Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Wed, 12 Feb 2020 12:44:24 +0100 Subject: [PATCH 15/18] Fixes --- prefork/prefork.go | 2 +- prefork/prefork_test.go | 28 ++++++++++++++++++---------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/prefork/prefork.go b/prefork/prefork.go index 539b81563f..c30badbc27 100644 --- a/prefork/prefork.go +++ b/prefork/prefork.go @@ -29,6 +29,7 @@ type Prefork struct { Network string // Flag to use a listener with reuseport, if not a File Listener will be used + // See: https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/ // // It's disabled by default Reuseport bool @@ -108,7 +109,6 @@ func (p *Prefork) setTCPListenerFiles(addr string) error { } func (p *Prefork) prefork(addr string) error { - strCmd := os.Args[0] chErr := make(chan error, 1) if !p.Reuseport { diff --git a/prefork/prefork_test.go b/prefork/prefork_test.go index 4f359dbbf7..12f84452db 100644 --- a/prefork/prefork_test.go +++ b/prefork/prefork_test.go @@ -12,10 +12,6 @@ import ( "github.com/valyala/fasthttp" ) -func init() { //nolint:gochecknoinits - setUp() -} - func setUp() { os.Args = append(os.Args, preforkChildFlag) } @@ -30,18 +26,18 @@ func getAddr() string { func Test_IsChild(t *testing.T) { v := IsChild() - if !v { - t.Errorf("IsChild() == %v, want %v", v, true) + if v { + t.Errorf("IsChild() == %v, want %v", v, false) } - tearDown() + setUp() v = IsChild() - if v { - t.Errorf("IsChild() == %v, want %v", v, false) + if !v { + t.Errorf("IsChild() == %v, want %v", v, true) } - setUp() + tearDown() } func Test_New(t *testing.T) { @@ -125,6 +121,8 @@ func Test_setTCPListenerFiles(t *testing.T) { } func Test_ListenAndServe(t *testing.T) { + setUp() + s := &fasthttp.Server{} p := New(s) p.Reuseport = true @@ -149,9 +147,13 @@ func Test_ListenAndServe(t *testing.T) { if p.ln == nil { t.Error("Prefork.ln is nil") } + + tearDown() } func Test_ListenAndServeTLS(t *testing.T) { + setUp() + s := &fasthttp.Server{} p := New(s) p.Reuseport = true @@ -176,9 +178,13 @@ func Test_ListenAndServeTLS(t *testing.T) { if p.ln == nil { t.Error("Prefork.ln is nil") } + + tearDown() } func Test_ListenAndServeTLSEmbed(t *testing.T) { + setUp() + s := &fasthttp.Server{} p := New(s) p.Reuseport = true @@ -203,4 +209,6 @@ func Test_ListenAndServeTLSEmbed(t *testing.T) { if p.ln == nil { t.Error("Prefork.ln is nil") } + + tearDown() } From 769b6454f650dbcdabb04a866615ef98721dd23f Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Wed, 12 Feb 2020 12:52:15 +0100 Subject: [PATCH 16/18] Add comment --- prefork/prefork.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/prefork/prefork.go b/prefork/prefork.go index c30badbc27..47ef052e94 100644 --- a/prefork/prefork.go +++ b/prefork/prefork.go @@ -43,6 +43,8 @@ type Prefork struct { } func init() { //nolint:gochecknoinits + // Definition flag to not break the program when the user adds their own flags + // and runs `flag.Parse()` flag.Bool(preforkChildFlag[1:], false, "Is a child process") } From 5735586908ff8b919ad794af8ad00ff168982a0d Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Wed, 12 Feb 2020 12:55:36 +0100 Subject: [PATCH 17/18] Fix typo --- prefork/prefork.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prefork/prefork.go b/prefork/prefork.go index 47ef052e94..7bdd08e9c3 100644 --- a/prefork/prefork.go +++ b/prefork/prefork.go @@ -59,7 +59,7 @@ func IsChild() bool { return false } -// New wraps the fasthttp server to run with prefork processes +// New wraps the fasthttp server to run with preforked processes func New(s *fasthttp.Server) *Prefork { return &Prefork{ Network: defaultNetwork, From 69f33616eb291b49e5f183f317761580cd400213 Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Wed, 12 Feb 2020 12:58:59 +0100 Subject: [PATCH 18/18] Fix typo --- prefork/prefork.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prefork/prefork.go b/prefork/prefork.go index 7bdd08e9c3..8fcc842d18 100644 --- a/prefork/prefork.go +++ b/prefork/prefork.go @@ -28,7 +28,7 @@ type Prefork struct { // By default is "tcp4" Network string - // Flag to use a listener with reuseport, if not a File Listener will be used + // Flag to use a listener with reuseport, if not a file Listener will be used // See: https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/ // // It's disabled by default