Merge afce7f1a923dbdea9888aa3bedb5fc328f5758fd into 8659ab573cf7d26b2fa2a41e90075d84606188f1

This commit is contained in:
Mahmood 2022-12-28 16:37:14 +07:00 committed by GitHub
commit 717bd30a3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 219 additions and 1 deletions

View File

@ -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

View File

@ -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.

37
gin.go
View File

@ -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) {

1
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

135
prefork.go Normal file
View File

@ -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
}