commit 4519b7ee563433f32615248c305a1602d13a224a Author: 会PS的小码农 <747357766@qq.com> Date: Fri May 6 18:49:41 2022 +0800 测试通过,并写了中文自述文件 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48498c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/go-graceful-restart-example \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..75dd772 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2022, EdisonLiu +Copyright (c) 2014, Scalingo +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.cn.md b/README.cn.md new file mode 100644 index 0000000..d25810f --- /dev/null +++ b/README.cn.md @@ -0,0 +1,37 @@ +# Server graceful restart with Go + +## 安装并运行服务器(打开终端运行一下命令) + +``` +$ git clone +$ go mod tidy +$ go run build //不打包第二次以后无法正常重启,会报 Fail to fork1 no such file or directory +$ go-graceful-restart-example +2014/12/14 20:26:42 [Server - 4301] Listen on [::]:12345 +[...] +``` + +## 测试客户端(新开一个终端) + +``` +$ cd /client +$ go run pong.go +``` + +## 优雅重启服务(再开一个终端) + +``` +# 服务器pid包含在其日志中,例如:[Server - 1204933] , 1204933即是当前pid +$ go run build //修改服务端代码后重新打包,再运行才能看到效果 +$ kill -HUP +``` + +## 延时停止服务 +让当前请求等待10秒后强制完成,并停止服务。 +``` +$ kill -TERM +``` + +## Gist of output + +https://gist.github.com/Soulou/7ca6a2d4f475f8e2345e diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..480ea30 --- /dev/null +++ b/README.en.md @@ -0,0 +1,37 @@ +# Server graceful restart with Go + +## Install and run the server + +``` +$ go get github.com/Scalingo/go-graceful-restart-example +$ go-graceful-restart-example +2014/12/14 20:26:42 [Server - 4301] Listen on [::]:12345 +[...] +``` + +## Connect with the client + +``` +$ cd $GOPATH/src/github.com/Scalingo/go-graceful-restart-example/client +$ go run pong.go +``` + +## Graceful restart + +``` +# The server pid is included in its log, in the example: 4301 + +$ kill -HUP +``` + +## Stop with timeout + +Let 10 seconds for the current requests to finish. + +``` +$ kill -TERM +``` + +## Gist of output + +https://gist.github.com/Soulou/7ca6a2d4f475f8e2345e diff --git a/client/pong.go b/client/pong.go new file mode 100644 index 0000000..034d526 --- /dev/null +++ b/client/pong.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "log" + "net" +) + +func main() { + sock, err := net.Dial("tcp", ":12345") + if err != nil { + log.Fatalln("fail to dial ':12345':", err) + } + + buffer := make([]byte, 64) + for { + n, err := sock.Read(buffer) + if err != nil { + log.Fatalln("fail to read socket:", err) + } + fmt.Printf(string(buffer)) + fmt.Printf("[Client] Received %d bytes, '%s'\n", n, string(buffer[:n])) + _, err = sock.Write([]byte("pong")) + if err != nil { + log.Fatalln("fail to write 'pong' to the socket:", err) + } + + fmt.Printf("[Client] Sent 'ping'\n") + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9e117e7 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/Scalingo/go-graceful-restart-example + +go 1.17 diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..6e4186a --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,32 @@ +package logger + +import ( + "fmt" + "log" + "os" +) + +type Logger struct { + logger *log.Logger + prefix string + pid int +} + +func New(prefix string) *Logger { + l := &Logger{logger: log.New(os.Stdout, "", log.LstdFlags)} + l.prefix = fmt.Sprintf("[%s - %d]", prefix, os.Getpid()) + return l +} + +func (l *Logger) Println(args ...interface{}) { + l.logger.Println(append([]interface{}{l.prefix}, args...)...) +} + +func (l *Logger) Printf(format string, args ...interface{}) { + l.logger.Printf(l.prefix+" "+format, args...) +} + +func (l *Logger) Fatalln(args ...interface{}) { + l.Println(args...) + os.Exit(-1) +} diff --git a/ping.go b/ping.go new file mode 100644 index 0000000..6676019 --- /dev/null +++ b/ping.go @@ -0,0 +1,73 @@ +package main + +import ( + "os" + "os/signal" + "syscall" + "time" + + "github.com/Scalingo/go-graceful-restart-example/logger" + "github.com/Scalingo/go-graceful-restart-example/server" +) + +func main() { + log := logger.New("Server") + + var s *server.Server + var err error + if os.Getenv("_GRACEFUL_RESTART") == "true" { + s, err = server.NewFromFD(log, 4) + } else { + s, err = server.New(log, 12345) + } + if err != nil { + log.Fatalln("fail to init server:", err) + } + log.Println("Listen on", s.Addr()) + + go s.StartAcceptLoop() + + signals := make(chan os.Signal) + signal.Notify(signals, syscall.SIGHUP, syscall.SIGTERM) + for sig := range signals { + if sig == syscall.SIGTERM { + // Stop accepting new connections + s.Stop() + // Wait a maximum of 10 seconds for existing connections to finish + err := s.WaitWithTimeout(10 * time.Second) + if err == server.WaitTimeoutError { + log.Printf("Timeout when stopping server, %d active connections will be cut.\n", s.ConnectionsCounter()) + os.Exit(-127) + } + // Then the program exists + log.Println("Server shutdown successful") + os.Exit(0) + } else if sig == syscall.SIGHUP { + // Stop accepting requests + s.Stop() + // Get socket file descriptor to pass it to fork + listenerFD, err := s.ListenerFD() + if err != nil { + log.Fatalln("Fail to get socket file descriptor:", err) + } + // Set a flag for the new process start process + os.Setenv("_GRACEFUL_RESTART", "true") + execSpec := &syscall.ProcAttr{ + Env: os.Environ(), + Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), listenerFD}, + } + // Fork exec the new version of your server + fork, err := syscall.ForkExec(os.Args[0], os.Args, execSpec) + if err != nil { + log.Fatalln("Fail to fork1", err) + } + log.Println("SIGHUP received: fork-exec to", fork) + // Wait for all conections to be finished + s.Wait() + log.Println(os.Getpid(), "Server gracefully shutdown") + + // Stop the old server, all the connections have been closed and the new one is running + os.Exit(0) + } + } +} diff --git a/server/connections.go b/server/connections.go new file mode 100644 index 0000000..e85aec6 --- /dev/null +++ b/server/connections.go @@ -0,0 +1,24 @@ +package server + +import "sync" + +type ConnectionManager struct { + *sync.WaitGroup + Counter int +} + +func NewConnectionManager() *ConnectionManager { + cm := &ConnectionManager{} + cm.WaitGroup = &sync.WaitGroup{} + return cm +} + +func (cm *ConnectionManager) Add(delta int) { + cm.Counter += delta + cm.WaitGroup.Add(delta) +} + +func (cm *ConnectionManager) Done() { + cm.Counter-- + cm.WaitGroup.Done() +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..3b8b928 --- /dev/null +++ b/server/server.go @@ -0,0 +1,137 @@ +package server + +import ( + "errors" + "fmt" + "net" + "os" + "time" + + "github.com/Scalingo/go-graceful-restart-example/logger" +) + +type Server struct { + cm *ConnectionManager + socket *net.TCPListener + logger *logger.Logger +} + +func New(logger *logger.Logger, port int) (*Server, error) { + s := &Server{cm: NewConnectionManager(), logger: logger} + + addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + return nil, fmt.Errorf("fail to resolve addr: %v", err) + } + sock, err := net.ListenTCP("tcp", addr) + if err != nil { + return nil, fmt.Errorf("fail to listen tcp: %v", err) + } + + s.socket = sock + return s, nil +} + +func NewFromFD(logger *logger.Logger, fd uintptr) (*Server, error) { + s := &Server{cm: NewConnectionManager(), logger: logger} + file := os.NewFile(3, "") + // file := os.NewFile(fd, "/tmp/sock-go-graceful-restart") + listener, err := net.FileListener(file) + if err != nil { + return nil, errors.New("File to recover socket from file descriptor: " + err.Error()) + } + listenerTCP, ok := listener.(*net.TCPListener) + if !ok { + return nil, fmt.Errorf("File descriptor %d is not a valid TCP socket", fd) + } + s.socket = listenerTCP + + return s, nil +} + +func (s *Server) Stop() { + // Accept will instantly return a timeout error + s.socket.SetDeadline(time.Now()) +} + +func (s *Server) ListenerFD() (uintptr, error) { + file, err := s.socket.File() + if err != nil { + return 0, err + } + return file.Fd(), nil +} + +func (s *Server) Wait() { + s.cm.Wait() +} + +var WaitTimeoutError = errors.New("timeout") + +func (s *Server) WaitWithTimeout(duration time.Duration) error { + timeout := time.NewTimer(duration) + wait := make(chan struct{}) + go func() { + s.Wait() + wait <- struct{}{} + }() + + select { + case <-timeout.C: + return WaitTimeoutError + case <-wait: + return nil + } +} + +func (s *Server) StartAcceptLoop() { + for { + conn, err := s.socket.Accept() + if err != nil { + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + s.logger.Println("Stop accepting connections") + return + } + s.logger.Println("[Error] fail to accept:", err) + } + go func() { + s.cm.Add(1) + s.handleConn(conn) + s.cm.Done() + }() + } +} + +func (s *Server) handleConn(conn net.Conn) { + tick := time.NewTicker(time.Second) + buffer := make([]byte, 64) + for { + select { + case <-tick.C: + _, err := conn.Write([]byte("ping6")) + if err != nil { + s.logger.Println("[Error] fail to write 'ping':", err) + conn.Close() + return + } + s.logger.Printf("[Server] Sent 'ping'\n") + + n, err := conn.Read(buffer) + if err != nil { + s.logger.Println("[Error] fail to read from socket:", err) + conn.Close() + return + } + + s.logger.Printf("[Server] OK: read %d bytes: '%s'\n", n, string(buffer[:n])) + } + } +} + +func (s *Server) Addr() net.Addr { + return s.socket.Addr() +} + +func (s *Server) ConnectionsCounter() int { + return s.cm.Counter +}