Skip to content

Commit

Permalink
Add prefork utility (#741)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergio Andrés Virviescas Santana committed Feb 12, 2020
1 parent 021c821 commit aa96a47
Show file tree
Hide file tree
Showing 3 changed files with 492 additions and 0 deletions.
90 changes: 90 additions & 0 deletions prefork/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# 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)
}
```

## 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) {
// Simulates some hard work
time.Sleep(100 * time.Millisecond)
}
```

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:

- prefork

```bash
Running 15s test @ http://localhost:8080
4 threads and 512 connections
Thread Stats Avg Stdev Max +/- Stdev
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
```

- **non**-prefork

```bash
Running 15s test @ http://localhost:8080
4 threads and 512 connections
Thread Stats Avg Stdev Max +/- Stdev
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
```
188 changes: 188 additions & 0 deletions prefork/prefork.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
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"

// 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: using prefork prevents the use of any global state!
// Things like in-memory caches won't work.
type Prefork struct {
// 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
// See: https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/
//
// 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
// 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")
}

// 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 preforked processes
func New(s *fasthttp.Server) *Prefork {
return &Prefork{
Network: defaultNetwork,
ServeFunc: s.Serve,
ServeTLSFunc: s.ServeTLS,
ServeTLSEmbedFunc: s.ServeTLSEmbed,
}
}

func (p *Prefork) listen(addr string) (net.Listener, error) {
runtime.GOMAXPROCS(1)

if p.Network == "" {
p.Network = defaultNetwork
}

if p.Reuseport {
return reuseport.Listen(p.Network, addr)
}

return net.FileListener(os.NewFile(3, ""))
}

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
}

tcplistener, err := net.ListenTCP(p.Network, tcpAddr)
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(addr string) error {
chErr := make(chan error, 1)

if !p.Reuseport {
if err := p.setTCPListenerFiles(addr); err != nil {
return err
}

defer p.ln.Close()
}

for i := 0; i < runtime.GOMAXPROCS(0); i++ {
cmd := exec.Command(os.Args[0], 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 {
if IsChild() {
ln, err := p.listen(addr)
if err != nil {
return err
}

p.ln = ln

return p.ServeFunc(ln)
}

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 {
if IsChild() {
ln, err := p.listen(addr)
if err != nil {
return err
}

p.ln = ln

return p.ServeTLSFunc(ln, certFile, certKey)
}

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 {
if IsChild() {
ln, err := p.listen(addr)
if err != nil {
return err
}

p.ln = ln

return p.ServeTLSEmbedFunc(ln, certData, keyData)
}

return p.prefork(addr)
}
Loading

0 comments on commit aa96a47

Please sign in to comment.