阅读 2389

gin框架实践连载八 | 如何优雅重启和停止

引言

  • 为什么要优雅重启和停止
  • 重启和停止有什么区别,怎么才能算是优雅的
  • 信号量是什么东西,有哪些
  • 如何实现优雅重启和停止
  • 代码github

1、Why?

在开发阶段,我们修改完代码或者配置,会直接ctrl + c,然后启动服务,那么生产环境如果也是如此会造成什么问题?

  • 请求丢失
  • 用户行为被打断
  • 你可能会失业

为了避免这种情况的发生,我们希望在应用更新或发布时,现有正在处理既有连接的应用不要中断,要先处理完既有连接后再退出。而新发布的应用在部署上去后再开始接收新的请求并进行处理,这样即可避免原来正在处理的连接被中断的问题

2、重启和停止的区别?

  • 重启是指在应用更新时,并不希望正在处理的连接断掉,同时要有新的进程采用新的应用,并接受新的请求
  • 停止是指在应用进程关闭时,可以处理完既有连接在关闭,并且停止接受新的连接
  • 所谓优雅,底线就是不能丢失连接,把该处理的连接都要处理完

3、信号量的定义

信号是 Unix 、类 Unix 以及其他 POSIX 兼容的操作系统中进程间通讯的一种有限制的方式

  • 常见信号量
命令信号描述
ctrl + cSIGINT强制进程结束
ctrl + zSIGTSTP任务中断,进程挂起
ctrl + \SIGQUIT进程结束 和 dump core
  • 全部信号量
$ kill -l
 1) SIGHUP     2) SIGINT     3) SIGQUIT     4) SIGILL     5) SIGTRAP
 6) SIGABRT     7) SIGBUS     8) SIGFPE     9) SIGKILL    10) SIGUSR1
11) SIGSEGV    12) SIGUSR2    13) SIGPIPE    14) SIGALRM    15) SIGTERM
16) SIGSTKFLT    17) SIGCHLD    18) SIGCONT    19) SIGSTOP    20) SIGTSTP
21) SIGTTIN    22) SIGTTOU    23) SIGURG    24) SIGXCPU    25) SIGXFSZ
26) SIGVTALRM    27) SIGPROF    28) SIGWINCH    29) SIGIO    30) SIGPWR
31) SIGSYS    34) SIGRTMIN    35) SIGRTMIN+1    36) SIGRTMIN+2    37) SIGRTMIN+3
38) SIGRTMIN+4    39) SIGRTMIN+5    40) SIGRTMIN+6    41) SIGRTMIN+7    42) SIGRTMIN+8
43) SIGRTMIN+9    44) SIGRTMIN+10    45) SIGRTMIN+11    46) SIGRTMIN+12    47) SIGRTMIN+13
48) SIGRTMIN+14    49) SIGRTMIN+15    50) SIGRTMAX-14    51) SIGRTMAX-13    52) SIGRTMAX-12
53) SIGRTMAX-11    54) SIGRTMAX-10    55) SIGRTMAX-9    56) SIGRTMAX-8    57) SIGRTMAX-7
58) SIGRTMAX-6    59) SIGRTMAX-5    60) SIGRTMAX-4    61) SIGRTMAX-3    62) SIGRTMAX-2
63) SIGRTMAX-1    64) SIGRTMAX
复制代码

4、如何实现

实现目的

  • 不关闭现有连接(正在运行中的程序)。
  • 新的进程启动并替代旧进程。
  • 新的进程接管新的连接。
  • 连接要随时响应用户的请求。当用户仍在请求旧进程时要保持连接,新用户应请求新进程,不可以出现拒绝请求的情况。

实现原理

  • 监听 SIGHUP 信号;
  • 收到信号后将服务监听的文件描述符传递给新的子进程;
  • 此时新老进程同时接收请求;
  • 父进程停止接收新请求, 等待旧请求完成(或超时);
  • 父进程退出.

采用第3方组件"github.com/fvbock/endless"

import ("github.com/fvbock/endless")

r := InitRouter()
	s := &http.Server{
		Addr:         fmt.Sprintf("%s:%d", config.ServerSetting.HttpAddress, config.ServerSetting.HttpPort),
		Handler:      r,
		ReadTimeout:  config.ServerSetting.ReadTimeout,  //请求响应的超市时间
		WriteTimeout: config.ServerSetting.WriteTimeout, //返回响应的超时时间
		//MaxHeaderBytes: 1 << 20,//默认的1MB
	}
    endless.ListenAndServe()
复制代码

5、自研热重启组件

核心流程


func serve() {
    ListenAndServe() //监听并启动
    核心运行函数
    getNetListener()   // 1. 获取监听 listener
    Serve()         // 2. 用获取到的 listener 开启 server 服务
    handleSignals() // 3. 监听外部信号,用来控制程序 fork 还是 shutdown
}
复制代码

代码地址

package hotstart

import (
	"context"
	"crypto/tls"
	"fmt"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"sync"
	"syscall"
	"time"
)

const (
	LISTENER_FD           = 3
	DEFAULT_READ_TIMEOUT  = 60 * time.Second
	DEFAULT_WRITE_TIMEOUT = DEFAULT_READ_TIMEOUT
)

var (
	runMutex = sync.RWMutex{}
)

// HTTP server that supported hotstart shutdown or restart
type HotServer struct {
	*http.Server
	listener     net.Listener
	isChild      bool
	signalChan   chan os.Signal
	shutdownChan chan bool
	BeforeBegin  func(addr string)
}

func ListenAndServer(server *http.Server) error {
	return NewHotServer(server).ListenAndServe()
}

func ListenAndServe(addr string, handler http.Handler) error {
	return NewServer(addr, handler, DEFAULT_READ_TIMEOUT, DEFAULT_WRITE_TIMEOUT).ListenAndServe()
}

/*
new HotServer
*/
func NewHotServer(server *http.Server) (srv *HotServer) {
	runMutex.Lock()
	defer runMutex.Unlock()

	isChild := os.Getenv("HOT_CONTINUE") != ""

	srv = &HotServer{
		Server:       server,
		isChild:      isChild,
		signalChan:   make(chan os.Signal),
		shutdownChan: make(chan bool),
	}

	//服务启动之前钩子,命令行输出pid
	srv.BeforeBegin = func(addr string) {
		srv.logf(addr)
	}

	return
}

/*
new HotServer
*/
func NewServer(addr string, handler http.Handler, readTimeout, writeTimeout time.Duration) *HotServer {

	Server := &http.Server{
		Addr:         addr,
		Handler:      handler,
		ReadTimeout:  readTimeout,
		WriteTimeout: writeTimeout,
	}

	return NewHotServer(Server)
}

/*
Listen http server
*/
func (srv *HotServer) ListenAndServe() error {
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}

	ln, err := srv.getNetListener(addr)
	if err != nil {
		return err
	}

	srv.listener = ln

	if srv.isChild {
		//通知父进程不接受请求
		syscall.Kill(syscall.Getppid(), syscall.SIGTERM)
	}

	srv.BeforeBegin(srv.Addr)

	return srv.Serve()
}

/*
监听 https server
*/
func (srv *HotServer) ListenAndServeTLS(certFile, keyFile string) error {
	addr := srv.Addr
	if addr == "" {
		addr = ":https"
	}

	config := &tls.Config{}
	if srv.TLSConfig != nil {
		*config = *srv.TLSConfig
	}
	if config.NextProtos == nil {
		config.NextProtos = []string{"http/1.1"}
	}

	var err error
	config.Certificates = make([]tls.Certificate, 1)
	config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
	if err != nil {
		return err
	}

	ln, err := srv.getNetListener(addr)
	if err != nil {
		return err
	}

	srv.listener = tls.NewListener(ln, config)

	if srv.isChild {
		syscall.Kill(syscall.Getppid(), syscall.SIGTERM)
	}

	srv.BeforeBegin(srv.Addr)

	return srv.Serve()
}

/*
服务启动
*/
func (srv *HotServer) Serve() error {
	//监听信号
	go srv.handleSignals()
	err := srv.Server.Serve(srv.listener)

	srv.logf("waiting for connections closed.")
	//阻塞等待关闭
	<-srv.shutdownChan
	srv.logf("all connections closed.")

	return err
}

/*
get lister
*/
func (srv *HotServer) getNetListener(addr string) (ln net.Listener, err error) {
	if srv.isChild {
		file := os.NewFile(LISTENER_FD, "")
		ln, err = net.FileListener(file)
		if err != nil {
			err = fmt.Errorf("net.FileListener error: %v", err)
			return nil, err
		}
	} else {
		ln, err = net.Listen("tcp", addr)
		if err != nil {
			err = fmt.Errorf("net.Listen error: %v", err)
			return nil, err
		}
	}
	return ln, nil
}

/*
监听信号
*/

func (srv *HotServer) handleSignals() {
	var sig os.Signal

	signal.Notify(
		srv.signalChan,
		syscall.SIGTERM,
		syscall.SIGUSR2,
	)

	for {
		sig = <-srv.signalChan
		switch sig {
		case syscall.SIGTERM:
			srv.logf("received SIGTERM, hotstart shutting down HTTP server.")
			srv.shutdown()
		case syscall.SIGUSR2:
			srv.logf("received SIGUSR2, hotstart restarting HTTP server.")
			if err := srv.fork(); err != nil {
				log.Println("Fork err:", err)
			}
		default:
		}
	}
}

/*
优雅关闭后台
*/
func (srv *HotServer) shutdown() {
	if err := srv.Shutdown(context.Background()); err != nil {
		srv.logf("HTTP server shutdown error: %v", err)
	} else {
		srv.logf("HTTP server shutdown success.")
		srv.shutdownChan <- true
	}
}

// start new process to handle HTTP Connection
func (srv *HotServer) fork() (err error) {
	listener, err := srv.getTCPListenerFile()
	if err != nil {
		return fmt.Errorf("failed to get socket file descriptor: %v", err)
	}

	// set hotstart restart env flag
	env := append(
		os.Environ(),
		"HOT_CONTINUE=1",
	)

	execSpec := &syscall.ProcAttr{
		Env:   env,
		Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), listener.Fd()},
	}

	_, err = syscall.ForkExec(os.Args[0], os.Args, execSpec)
	if err != nil {
		return fmt.Errorf("Restart: Failed to launch, error: %v", err)
	}

	return
}

/*
获取TCP监听文件
*/
func (srv *HotServer) getTCPListenerFile() (*os.File, error) {
	file, err := srv.listener.(*net.TCPListener).File()
	if err != nil {
		return file, err
	}
	return file, nil
}

/*
格式化输出Log
*/

func (srv *HotServer) logf(format string, args ...interface{}) {
	pids := strconv.Itoa(os.Getpid())
	format = "[pid " + pids + "] " + format
	log.Printf(format, args...)
}

复制代码

demo 地址

func hello(w http.ResponseWriter, r *http.Request) {
	time.Sleep(20 * time.Second)
	w.Write([]byte("hello world233333!!!!"))
}

func test(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("test 22222"))
}

func main() {
	http.HandleFunc("/hello", hello)
	http.HandleFunc("/test", test)
	pid := os.Getpid()
	address := ":9999"
	s := &http.Server{
		Addr:    address,
		Handler: nil,
	}
	err := Hot.ListenAndServer(s)
	log.Printf("process with pid %d stoped, error: %s.\n", pid, err)
}
复制代码

6、脚本

restart(根据项目启动脚本)

ps aux | grep "gintest" | grep -v grep | awk '{print $2}' | xargs -i kill -SIGUSR2 {}
复制代码

stop

ps aux | grep "gintest" | grep -v grep | awk '{print $2}' | xargs -i kill -SIGTERM {}
复制代码

端口restart

ps aux | lsof -i:8080 | grep -v grep | awk '{print $1}' | xargs -i kill -1 {}
复制代码

端口stop

ps aux | lsof -i:8080 | grep -v grep | awk '{print $1}' | xargs -i kill {}
复制代码

7、总结

在日常的http服务中,优雅的重启(热更新)是非常重要的一环,目前Golang有不少方案,我们可以视情况选择

8、推荐案例

我自己的另外一个框架中已加入hotstart使用,支持多环境编译,地址

9、系列文章

文章分类
后端
文章标签