在构建任何长期运行的进程时,如网络服务器或导入数据的程序,我们应该考虑提供一种优雅关闭的方式,这背后的想法是提供一种干净地退出进程的方式,清理资源并适当取消该运行进程。
在Go中支持Graceful Shutdown 的步骤包括两个步骤。
- 听取操作系统的信号,以及
- 处理这些信号。
在Go中,这些信号是由os/signal 包提供的。从Go 1.16开始,我喜欢通过使用os/signal.NotifyContext 来实现优雅关机,这个函数提供了一种习惯性的方式来传播使用goroutines时的取消,这在处理长期运行的进程时通常是这样的。
请记住,根据我们的main 包的实现方式,你可能需要对其进行重构,有main 函数来达到以下目的。
- 如果需要的话,调用一个
Parse的函数,就像flag.Parse(),并且 - 调用一个类似
run的函数。
run 函数是协调所有不同类型、初始化所有东西、连接所有点的函数,也许还使用了显式的依赖注入,更重要的是它可能会运行一些goroutines来实现对signal.NotifyContext 的调用,最终要处理实现优雅关闭的逻辑。
让我们来看看一些具体的例子。
使用signal.NotifyContext
从Go 1.16开始,signal.NotifyContext 是我在处理信号时喜欢推荐的方式,这取代了以前需要一个通道的方式。
例如有同样的main() 。
func main() {
errC := run()
if err := <-errC; err != nil {
fmt.Println("error", err)
}
fmt.Println("exiting...")
}
当使用signal.Notify 。
func run() <-chan error {
errC := make(chan error, 1)
sc := make(chan os.Signal, 1)
signal.Notify(sc,
os.Interrupt,
syscall.SIGTERM,
syscall.SIGQUIT)
go func() {
defer close(errC)
fmt.Println("waiting for signal...")
<-sc
fmt.Println("signal received")
}()
return errC
}
而当使用signal.NotifyContext 。
func run() <-chan error {
errC := make(chan error, 1)
ctx, stop := signal.NotifyContext(context.Background(),
os.Interrupt,
syscall.SIGTERM,
syscall.SIGQUIT)
go func() {
defer func() {
stop()
close(errC)
}()
fmt.Println("waiting for signal...")
<-ctx.Done()
fmt.Println("signal received")
}()
return errC
}
在实践中,这两种方式都能达到相同的目的,因为它们都是为了监听信号,然而最大的区别是,signal.NotifyContext 提供了一个上下文ctx ,可以用来创建更复杂的传播规则(例如超时),我们可以用它来取消其他的goroutine,而不是手动做更多的工作。
在HTTP服务器中实现优雅关机
包括在标准库中,在net/http ,Go包括自己的HTTP服务器在 net/http.Server,这个服务器定义了一个名为 Shutdown的方法,目的是在服务器应该退出时被调用,它可以优雅地关闭服务器。
如果我们使用上面定义的代码段,我们可以用下面的方式编写代码来处理HTTP服务器的优雅关机。
// ... other code initializing things used by this HTTP server
go func() {
<-ctx.Done()
fmt.Println("Shutdown signal received")
ctxTimeout, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer func() {
stop()
cancel()
close(errC)
}()
srv.SetKeepAlivesEnabled(false)
if err := srv.Shutdown(ctxTimeout); err != nil {
errC <- err
}
fmt.Println("Shutdown completed")
}()
go func() {
fmt.Println("Listening and serving")
// "ListenAndServe always returns a non-nil error. After Shutdown or Close, the returned error is
// ErrServerClosed."
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errC <- err
}
}()
return errC
总结
实现优雅关机的目的是在处理一个长期运行的进程时,允许定义一些清理步骤,在这种情况下,也许我们需要提交一些数据库事务,删除一些使用过的文件,或者也许触发一个事件,表明其他进程应该接管后续的事件。