优雅停止
:顾名思义,即优雅的停止正在运行的程序,对一个go实现的http服务优雅停止主要指标是停止的时候需要尽量完成正在服务的http请求并且不再接收新请求。
与php语言处理一次请求的生命周期不同,go编译后的二进制可执行文件直接实现了php里需nginx、fpm、apache管控的功能,基于php的http服务只需要处理请求进来和请求结束的部分(当然也有例外的如swoole的实现),基于go的http服务器生命周期则贯穿了整个可执行程序的启动到停止的整个过程。当go服务需要停止、重启时就涉及到当前尚未完成的请求怎样优雅结束的问题;在传统LNMP的php结构中这些问题由nginx帮助完成,但到了go这些就是不得不考虑的问题了。
得益于go语言原生便捷支持http服务开发,日常开发的http服务基本都是围绕net/http
包做一些开发任务,更深一点儿则是围绕http.Server
这个结构体发散。
信号:os.Signal
二进制可执行程序的停止、重启一般是通过系统信号进行控制,典型的信号如在当前进程同时按下ctrl
+c
键(mac的话control
+c
键)就是一个编号为2的SIGINT
信号,通过捕获这个信号可以实现对当前进程的一些操作比如优雅退出当前进程。
go抽象了各个平台的信号为os.Signal
,然后提供了signal.Notify(c chan<- os.Signal, sig ...os.Signal)
捕获第二个不定长参数指定的信号传递给第一个参数通道,不定长参数也是可选参数,如果不传则捕获所有信号传递给通道。通道的特性可以在多个协程之间安全通信,这样通过go抽象的os.Signal
配合协程即可实现系统信号捕获转换为通道chan,利用chan的一些特性即可完成协程间通信和控制。
优雅退出
一个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()
正常情况下,ListenAndServe
会进入到for
循环阻塞,即进程进入服务http请求状态。来一个客户端http请求for里会开一个goroutine协程进行服务。此段核心代码里并没有处理系统中断信号,在终端命令行里启动的进程退出将会直接中断当前尚未退出的goroutine协程即当前尚未完成的http请求会被粗暴的终止。
一个带监听系统信号优雅退出的伪代码就产生了
// 监听系统信号:即将系统信号抽象成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("进程已退出:服务已关闭")
一个可优雅退出的http-server实现涉及到os.Signal
、context.Context
、协程间通信channel
、以及还需要http包本身的优雅退出支持。查看net/http
包的Server.Shutdown
方法的源码会发现里面的检查当前尚在服务的http请求控制更为复杂,核心是在超时上下文指定的超时时间内不接受新进请求并尽量服务完当前尚未完成的http请求。会涉及到互斥锁、定时器、原子状态设置(http.atomicBool
这个内部结构体)等一系列go知识点和官方使用方法,多看看官方库代码有助于写出高质量go代码。
----
参考资料:
① https://github.com/golang/go/commit/53fc330e2d154443acf3d01e0d68bae22b2b7804
② https://www.cnblogs.com/yougewe/p/14321413.html
③ https://golang.google.cn/doc/go1.17#vet
哟嚯,本文评论功能关闭啦~