优雅退出
graceful shutdown,优雅退出。
指HTTP服务接受到用户的退出指令后停止接收新请求,在处理和回复当前正在处理的这批请求后主动退出服务。
区别于SIGKILL(kill -9 or CTRL + C),安全退出可以最小化程序在滚动更新时的服务抖动
用户的退出指令一般是
SIGTERM
(k8s的实现)或SIGINT
(常常对应bash的Ctrl + C
)
一、 涉及模块
1、 监听信号
使用标准库os/exec.go中Signal
即可完成信息监听
go
// 至少设置数量为1的缓存区
quitSignal := make(chan os.Signal, 1)
signal.Notify(quitSignal, []os.Signal{syscall.SIGINT, syscall.SIGTERM}...)
// 阻塞直至有信号写入
<-quitSignal
- SIGINT:当你在终端按下ctrl+c时,则会触发这个信号
- SIGTERM:当我们给程序发送kill或者killall指令时,则会触发这个信号
值得注意的是,在没有使用signal.Notify()
时,Go默认有一套信号处理规则,比如 SIGHUP
, SIGINT
或SIGTERM
会让程序直接退出。
2、 停止HTTP服务
调用运行中的Server实例的Shutdown()方法可以让服务安全退出:
go
// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// If srv.Addr is blank, ":http" is used.
//
// ListenAndServe always returns a non-nil error. After Shutdown or Close,
// the returned error is ErrServerClosed.
func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(ln)
}
// When Shutdown is called, Serve, ListenAndServe, and
// ListenAndServeTLS immediately return ErrServerClosed. Make sure the
// program doesn't exit and waits instead for Shutdown to return.
// Once Shutdown has been called on a server, it may not be reused;
// future calls to methods such as Serve will return ErrServerClosed.
func (srv *Server) Shutdown(ctx context.Context) error {
xxx
}
这里能看到标准库的注释
ListenAndServe
会在Shutdown
、Close
后立即 返回 ErrServerClosed- 在
Shutdown
执行完成时,确保程序不会被退出而是等待以返回
3、 超时处理
server的Shutdown方法需要接收一个context对象,因此我们可以定义一个设置超时的context,如果超过这个时间请求还没完成处理,则会强制退出,避免程序长时间等待无法退出。
当然也可以传入一个没有超时的context(context.Background())
二、 代码实现
handler方法,通过num参数进行短暂休眠,并打印休眠持续时间。
简单Demo,未加数据校验。
go
func handler(w http.ResponseWriter, r *http.Request) {
numStr := r.URL.Query().Get("num")
num, err := strconv.Atoi(numStr)
if err != nil {
return
}
delay := time.Duration(num) * time.Second
startAt := time.Now()
fmt.Println("req received, delay", delay)
defer func() {
fmt.Println("req completed, latency", time.Since(startAt))
}()
time.Sleep(delay)
w.WriteHeader(http.StatusOK)
_, _ = io.WriteString(w, numStr)
}
main函数,创建http服务、启动服务、监听系统退出信号、超时处理。
go
func main() {
// 创建一个新的HTTP服务器
mux := http.NewServeMux()
mux.HandleFunc("/process", handler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// 在一个新的goroutine中启动服务器
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Printf("listen: %s\n", err)
}
}()
// 监听系统退出信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, []os.Signal{syscall.SIGINT, syscall.SIGTERM}...)
fmt.Println(fmt.Sprintf("\n exit: %v", <-quit))
// 创建一个带有超时的context,以便在服务器关闭时有一个限制时间
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
fmt.Printf("Server Shutdown: %s, time out\n", err)
}
fmt.Println("Server exiting")
}
- 启动服务ListenAndServe()会阻塞程序,为了避免后续的信号监测被阻塞,因此需要把服务启动放到协程执行。
- 根据设置超时时间和handler传参的大小,体验服务是否超时的返回结果有什么不同
三、 业务场景
代码实现只是一个简单的demo,在实际应用场景下,并不会简单地、草率地开启这样一个http服务,一方便程序要监听系统的退出信号,另一方面在程序拉起时创建routerGroup路由组、LoadConfig加载配置等出现错误时也应中断程序并给出对应的错误信息。
go
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
...
errs := make(chan error, 2)
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
errs <- fmt.Errorf("%s", <-c)
}()
hostAPI(errs)
...
fmt.Println(fmt.Sprintf("exit: %v", <-errs))
}
func hostAPI(errs chan error) {
server := GetServer() // 表示获取Server对象,伪代码
router := gin.Default()
if svr == nil {
log.Warnf(" [%s] server config is nil", name)
return
}
log.Infof("host [%s] server [%s,%d]", svr.Name, svr.Host, svr.Port)
go func() {
strPort := strconv.Itoa(svr.Port)
listenAddr := svr.Host + ":" + strPort
fmt.Println("hosts:", listenAddr)
errs <- http.ListenAndServe(listenAddr, router) //打开监听端口
}()
}
- 在main函数中创建error管道,缓冲区大小设置为2(ERROR+SIGTERM)
- 监听系统退出+监听API路由组创建是否异常
- 错误均写入errs, 在main函数最后,阻塞等待errs管道后退出程序