diff --git a/README.md b/README.md index 103a53c7..b80982ce 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi - [http2 server push](#http2-server-push) - [Define format for the log of routes](#define-format-for-the-log-of-routes) - [Set and get a cookie](#set-and-get-a-cookie) + - [Prefork](#prefork) - [Don't trust all proxies](#dont-trust-all-proxies) - [Testing](#testing) - [Users](#users) @@ -2248,6 +2249,47 @@ func main() { } ``` +### Prefork + +Prefork is a functionality which makes use of SO_REUSEPORT and SO_REUSEADDR +socket option feature (which is available on most operation systems) available +for our router. + +Benefits: +- Reduces lock contention between workers accepting new connections. +- Improve performance on multicore systems + +Disadvantages: +- when a worker is stalled by a blocking operation, the block affects not only + connections that the worker has already accepted, but also connection requests + that the kernel has assigned to the worker since it became blocked. + +For actual definition and information about what this feature does and why +it exists in gin, have a look at just the description part of [this](socket-nginx) +release note article which made understanding of this feature a lot easier. + +```go +func main() { + // Creates a gin router with default middleware: + // logger and recovery (crash-free) middleware + router := gin.Default() + + router.GET("/ping", func(c *gin.Context) { + c.String(http.StatusOK, "pong") + }) + + // A number bigger than 1 or no number(in cpus with more + // than one core) should be passed so that this functionality + // actually applies on your api functionality. + // In this case that I did not pass any number, if we assume + // my cpu has 4 cores, so 4 processes will get created to + // serve my api. + router.Prefork() + + router.Run(":8080") +} +``` + ## Don't trust all proxies Gin lets you specify which headers to hold the real client IP (if any), @@ -2380,3 +2422,4 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor * [picfit](https://github.com/thoas/picfit): An image resizing server written in Go. * [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes. * [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system. +* [socket-nginx](https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1): SO_REUSEPORT and SO_REUSEADDR article \ No newline at end of file diff --git a/debug.go b/debug.go index cbcedbc9..32744daf 100644 --- a/debug.go +++ b/debug.go @@ -17,7 +17,7 @@ const ginSupportMinGoVer = 16 // IsDebugging returns true if the framework is running in debug mode. // Use SetMode(gin.ReleaseMode) to disable debug mode. func IsDebugging() bool { - return ginMode == debugCode + return ginMode == debugCode && !IsChild() } // DebugPrintRouteFunc indicates debug log output format. diff --git a/gin.go b/gin.go index 35159d03..1d69eee8 100644 --- a/gin.go +++ b/gin.go @@ -11,6 +11,7 @@ import ( "net/http" "os" "path" + "runtime" "strings" "sync" @@ -150,6 +151,9 @@ type Engine struct { // ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil. ContextWithFallback bool + // Determines if master should create child processes or not + doPrefork bool + delims render.Delims secureJSONPrefix string HTMLRender render.HTMLRender @@ -196,6 +200,7 @@ func New() *Engine { UnescapePathValues: true, MaxMultipartMemory: defaultMultipartMemory, trees: make(methodTrees, 0, 9), + doPrefork: false, delims: render.Delims{Left: "{{", Right: "}}"}, secureJSONPrefix: "while(1);", trustedProxies: []string{"0.0.0.0/0", "::/0"}, @@ -379,6 +384,12 @@ func (engine *Engine) Run(addr ...string) (err error) { address := resolveAddress(addr) debugPrint("Listening and serving HTTP on %s\n", address) + + if engine.doPrefork || IsChild() { + err = engine.prefork(address) + return + } + err = http.ListenAndServe(address, engine.Handler()) return } @@ -645,6 +656,32 @@ func (engine *Engine) handleHTTPRequest(c *Context) { serveError(c, http.StatusNotFound, default404Body) } +func (engine *Engine) prefork(addr string) (err error) { + return prefork(addr, engine) +} + +// Prefork will create 'number' amount of child processes +// which will listen to the same address and port as other +// child processes if 'number' is bigger than 1. +// +// This is a feature which enables use of SO_REUSEPORT +// and SO_REUSEADDR socket option which is available in +// most operation systems out there. +// +// More information: +// https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/# +func (engine *Engine) Prefork(number ...int) { + n := runtime.GOMAXPROCS(0) + if len(number) > 0 { + n = number[0] + } + + if n > 1 { + runtime.GOMAXPROCS(n) + engine.doPrefork = true + } +} + var mimePlain = []string{MIMEPlain} func serveError(c *Context, code int, defaultMessage []byte) { diff --git a/go.mod b/go.mod index e9698339..fe05058f 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/go-playground/validator/v10 v10.11.1 github.com/goccy/go-json v0.10.0 github.com/json-iterator/go v1.1.12 + github.com/libp2p/go-reuseport v0.2.0 github.com/mattn/go-isatty v0.0.16 github.com/pelletier/go-toml/v2 v2.0.6 github.com/stretchr/testify v1.8.1 diff --git a/go.sum b/go.sum index 6d8df64a..51702a99 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/libp2p/go-reuseport v0.2.0 h1:18PRvIMlpY6ZK85nIAicSBuXXvrYoSw3dsBAR7zc560= +github.com/libp2p/go-reuseport v0.2.0/go.mod h1:bvVho6eLMm6Bz5hmU0LYN3ixd3nPPvtIlaURZZgOY4k= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= diff --git a/prefork.go b/prefork.go new file mode 100644 index 00000000..c9cd517a --- /dev/null +++ b/prefork.go @@ -0,0 +1,135 @@ +/* + * MIT License + * + * Copyright (c) 2019-present Fenny and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Code Sources In Here Come From: https://github.com/gofiber/fiber/blob/master/prefork.go + */ +package gin + +import ( + "fmt" + "net/http" + "os" + "os/exec" + "runtime" + "time" + + reuseport "github.com/libp2p/go-reuseport" +) + +const ( + envPreforkChildKey = "GIN_PREFORK_CHILD" + envPreforkChildVal = "1" +) + +// Holds process of childs +var children = map[int]*exec.Cmd{} + +// IsChild determines if the current process is a child of Prefork +func IsChild() bool { + return os.Getenv(envPreforkChildKey) == envPreforkChildVal +} + +// watchMaster watches child procs +func watchMaster() { + if runtime.GOOS == "windows" { + // finds parent process, + // and waits for it to exit + p, err := os.FindProcess(os.Getppid()) + if err == nil { + _, _ = p.Wait() + } + os.Exit(1) + } + // if it is equal to 1 (init process ID), + // it indicates that the master process has exited + for range time.NewTicker(time.Millisecond * 500).C { + if os.Getppid() == 1 { + os.Exit(1) + } + } +} + +// prefork manages child processes to make use of the OS REUSEPORT or REUSEADDR feature +func prefork(addr string, engine *Engine) (err error) { + // 👶 child process 👶 + if IsChild() { + // use 1 cpu core per child process + runtime.GOMAXPROCS(1) + + // kill current child proc when master exits + go watchMaster() + + // Run child + listener, err := reuseport.Listen("tcp", addr) + if err != nil { + return err + } + defer listener.Close() + + return http.Serve(listener, engine.Handler()) + } + + // child structure to be used in error returning + type child struct { + pid int + err error + } + // create variables + max := runtime.GOMAXPROCS(0) + channel := make(chan child, max) + + // kill child procs when master exits + defer func() { + for _, proc := range children { + _ = proc.Process.Kill() + } + }() + + // launch child procs + for i := 0; i < max; i++ { + cmd := exec.Command(os.Args[0], os.Args[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // Add gin prefork child flag into child proc env + cmd.Env = append(os.Environ(), + fmt.Sprintf("%s=%s", envPreforkChildKey, envPreforkChildVal), + ) + + if err = cmd.Start(); err != nil { + return fmt.Errorf("failed to start a child prefork process, error: %v", err) + } + + // Store child process ids + pid := cmd.Process.Pid + children[pid] = cmd + + // notify master if child crashes + go func() { + channel <- child{pid, cmd.Wait()} + }() + } + + // return error if child crashes + return (<-channel).err +}