优雅关闭的目的是让运行的Go应用程序停止接收新请求,同时在最终关闭之前完成正在进行的请求。这通常发生在滚动更新中。新的服务准备就绪后,旧服务才停止。
通常情况下,服务会在收到操作系统中断信号后终止。为了实现优雅的关闭,我们可以创建一个上下文,其中包含一个cancel回调和一个由http.Server接口提供的内置关闭方法。我们可以使用创建的上下文,通过一个通道侦听操作系统中断,并在接收到信号后立即调用cancel函数。
下面是一个http服务实现优雅关闭的示例。稍后我们将分析这个代码。
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"time"
)
// Server is an interface that will used by app to serve and shutdown http server
type Server interface {
ListenAndServe() error
Shutdown(context.Context) error
}
// Logger is an interface that used by app to log something
type Logger interface {
Fatalf(format string, v ...interface{})
Printf(format string, v ...interface{})
}
func handler() http.Handler {
mux := http.NewServeMux()
mux.Handle("/healthz", http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "ok")
},
))
return mux
}
func serve(ctx context.Context, server Server, logger Logger) error {
var err error
go func() {
if err = server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatalf("ListenAndServe()=%+s", err)
}
}()
<-ctx.Done()
logger.Printf("server stopped")
ctxShutDown, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer func() {
cancel()
}()
if err = server.Shutdown(ctxShutDown); err != nil {
logger.Fatalf("server.Shutdown(ctxShutdown)=%+s", err)
}
logger.Printf("server exited gracefully")
if err == http.ErrServerClosed {
err = nil
}
return err
}
func main() {
server := &http.Server{
Addr: ":8080",
Handler: handler(),
}
stdLog := log.New(os.Stderr, "", log.LstdFlags)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
ctx, cancel := context.WithCancel(context.Background())
go func() {
osCall := <-c
log.Printf("system call: %+v", osCall)
cancel()
}()
log.Printf("starting server at %s", server.Addr)
if err := serve(ctx, server, stdLog); err != nil {
log.Printf("serve(ctx, server, stdLog)=%+s", err)
}
}
要了解这种优雅的关闭是如何工作的,我们可以修改handler来模拟有请求时的处理过程。
func handler() http.Handler {
mux := http.NewServeMux()
mux.Handle("/healthz", http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "ok")
for i := 1; i <= 7; i++ {
log.Println(i)
time.Sleep(time.Second)
}
},
))
return mux
}
然后向对应接口发送一个请求。当程序处理请求时,按Ctrl+C发送中断信号。服务器将停止,但它仍然处理正在进行的请求。同时,如果在服务器停止后还有新的请求发起,它将被拒绝。
代码解析
首先,在main函数中,我们创建一个用于监听os.Interrupt的channel【也可以通过|逻辑操作符来同时监听其他信号如os.kill】。
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
然后我们用context.cancel函数创建一个context。context将阻塞进程(在本例中是防止服务器关闭),直到我们调用cancel函数。
ctx, cancel := context.WithCancel(context.Background())
然后,我们创建一个在单独的goroutine中运行的匿名函数,它等待一个中断信号,然后调用cancel函数。
go func() {
osCall := <-c
log.Printf("system call: %+v", osCall)
cancel()
}()
我们也创建一个&http.Server和logger实例,供后面使用。
server := &http.Server{
Addr: ":8080",
Handler: handler(),
}
stdLog := log.New(os.Stderr, "", log.LstdFlags)
最后,将context、server和logger实例传递给serve函数。
if err := serve(ctx, server, stdLog); err != nil {
log.Printf("serve(ctx, server, stdLog)=%+s", err)
}
在serve函数中,我们将在一个单独的goroutine中运行http服务。然后,用<-ctx.Done()阻塞进程,这样程序就会等待cancel函数的调用(在接收到中断信号后main函数会调用cancel)。
go func() {
if err = server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatalf("ListenAndServe()=%+s", err)
}
}()
<-ctx.Done()
在调用cancel函数之后,我们将通过服务实例调用shutdown函数来优雅地关闭服务。因为函数调用需要一个context,所以我们还创建了一个带有超时的上下文,以便为服务器关闭提供一个时间窗口。
ctxShutDown, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer func() {
cancel()
}()
if err = server.Shutdown(ctxShutDown); err != nil {
logger.Fatalf("server.Shutdown(ctxShutdown)=%+s", err)
}
您可能注意到我使用Server和Logger接口。这样做的目的是使serve函数更容易测试。
总结:
服务优雅停止的目的主要是对服务正在进行的请求或操作的正常结束以及停止接收新请求,在日常开发中可以作为一个脚手架直接应用到你的服务中。