跳到主要内容

如何优雅重启和停止

引言

为什么要优雅重启和停止 重启和停止有什么区别,怎么才能算是优雅的 信号量是什么东西,有哪些 如何实现优雅重启和停止 代码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使用,支持多环境编译,地址

作者:Rocket 链接:https://juejin.cn/post/6867074626427502600 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。