Go中的微服务:干净地退出进程的方式

449 阅读3分钟

在构建任何长期运行的进程时,如网络服务器或导入数据的程序,我们应该考虑提供一种优雅关闭的方式,这背后的想法是提供一种干净地退出进程的方式,清理资源并适当取消该运行进程。

在Go中支持Graceful Shutdown 的步骤包括两个步骤。

  1. 听取操作系统的信号,以及
  2. 处理这些信号。

在Go中,这些信号是由os/signal 包提供的。从Go 1.16开始,我喜欢通过使用os/signal.NotifyContext 来实现优雅关机,这个函数提供了一种习惯性的方式来传播使用goroutines时的取消,这在处理长期运行的进程时通常是这样的。

请记住,根据我们的main 包的实现方式,你可能需要对其进行重构,有main 函数来达到以下目的。

  1. 如果需要的话,调用一个Parse 的函数,就像flag.Parse() ,并且
  2. 调用一个类似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

总结

实现优雅关机的目的是在处理一个长期运行的进程时,允许定义一些清理步骤,在这种情况下,也许我们需要提交一些数据库事务,删除一些使用过的文件,或者也许触发一个事件,表明其他进程应该接管后续的事件。