优雅停止

与php语言处理一次请求的生命周期不同,go编译后的二进制可执行文件直接实现了php里需nginx、fpm、apache管控的功能,基于php的http服务只需要处理请求进来和请求结束的部分(当然也有例外的如swoole的实现),基于go的http服务器生命周期则贯穿了整个可执行程序的启动到停止的整个过程。当go服务需要停止、重启时就涉及到当前尚未完成的请求怎样优雅结束的问题;在传统LNMP的php结构中这些问题由nginx帮助完成,但到了go这些就是不得不考虑的问题了。

net/httphttp.Server

信号:os.Signal

ctrlccontrolcSIGINT
os.Signalsignal.Notify(c chan<- os.Signal, sig ...os.Signal)os.Signal

优雅退出

一个go的http-server的核心代码如下:

// 初始化http-sever
server := &http.Server{
     Addr:           ":9080",
     Handler:        xxx, // http-handler,此处伪代码
     ReadTimeout:    30 * time.Second,
     WriteTimeout:   30* time.Second,
     MaxHeaderBytes: 1 << 20, //1MB
}

// 开始监听服务
server.ListenAndServe()
ListenAndServefor

一个带监听系统信号优雅退出的伪代码就产生了

// 监听系统信号:即将系统信号抽象成os.Signal通道
signalChan := make(chan os.Signal, 2) // 注意自1.17开始这里的chan必须是带缓冲的,见参考资料③
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)

// 启动一个协程,协程内部读取(接收)系统信号转换的 chan os.Signal signalChan本身这个通道的自主关闭
// 即当系统传递给进程一个信号时 signalChan 这个变量的 channel将会可读,否则一直阻塞
go func() {
    <-signalChan // 此处没有系统信号时阻塞,后续代码不执行,有信号时后续代码执行

    signal.Stop(signalChan)  // 显式停止监听系统信号
    close(signalChan) // 显式关闭监听信号的通道
}()

// 初始化http-sever
server := &http.Server{
     Addr:           ":9080",
     Handler:        xxx, // http-handler,此处伪代码
     ReadTimeout:    30 * time.Second,
     WriteTimeout:   30* time.Second,
     MaxHeaderBytes: 1 << 20, //1MB
}

// 主进程(主协程)阻塞channel,以便控制http-server优雅退出后才退出主进程(主协程)
// 就是一个简单的空结构体channel
idleCloser := make(chan struct{})


// 启动一个监听系统信号控制的channel
go func() {
    <-signalChan

    // 超时context
    timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer timeoutCancel()

    // 系统包提供的优雅退出方法,见参考资料①
    if err := server.Shutdown(timeoutCtx); err != nil {
        // Error from closing listeners, or context timeout:
        fmt.Println("Http服务暴力停止,一般是达到超时context的时间当前还有尚未完结的http请求:" + err.Error())
    } else {
        fmt.Println("Http服务优雅停止")
    }

    // 关闭 主进程(主协程)阻塞channel,本子协程安全退出然后下方57行的阻塞终止主进程安全退出
    close(idleCloser)
}()


// 启动进入for循环的http-server,启动返回了不为 http.ErrServerClosed 的 error 时则表示启动有异常,例如端口号被占用
// http.ErrServerClosed 错误则是当前server正在关闭中,当多个协程控制启动关闭通信不当时可能会出现这种情况
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
    fmt.Println("Http服务异常:" + err.Error())
    close(idleCloser)
}

// 通过空结构体channel阻塞主进程(主协程)达到持续运行的目的
<-idleCloser
fmt.Println("进程已退出:服务已关闭")
os.Signalcontext.Contextchannelnet/httpServer.Shutdownhttp.atomicBool

----

参考资料:

① https://github.com/golang/go/commit/53fc330e2d154443acf3d01e0d68bae22b2b7804

② https://www.cnblogs.com/yougewe/p/14321413.html

③ https://golang.google.cn/doc/go1.17#vet