golang中使用Shutdown特性对http服务优雅退出

golang 程序启动一个 http 服务时,若服务被意外终止或中断,会让现有请求连接突然中断,未处理完成的任务也会出现不可预知的错误,这样即会造成服务硬终止;为了解决硬终止问题我们希望服务中断或退出时将正在处理的请求正常返回并且等待服务停止前作的一些必要的处理工作。

我们可以看一个硬终止的例子:

复制代码
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second)
fmt.Fprintln(w, "Hello world!")
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
server.ListenAndServe()
复制代码
启动服务后,我们可以访问 http://127.0.0.1:8080 页面等待 5s 会输出一个 “Hello world!”, 我们可以尝试 Ctrl+C 终止程序,可以看到浏览器立刻就显示无法连接,这表示连接立刻就中断了,退出前的请求也未正常返回。

在 Golang1.8 以后 http 服务有个新特性 Shutdown 方法可以优雅的关闭一个 http 服务, 该方法需要传入一个 Context 参数,当程序终止时其中不会中断活跃的连接,会等待活跃连接闲置或 Context 终止(手动 cancle 或超时)最后才终止程序,官方文档详见:https://godoc.org/net/http#Server.Shutdown

在具体用应用中我们可以配合 signal.Notify 函数来监听系统退出信号来完成程序优雅退出;

特别注意:server.ListenAndServe() 方法在 Shutdown 时会立刻返回,Shutdown 方法会阻塞至所有连接闲置或 context 完成,所以 Shutdown 的方法要写在主 goroutine 中。

优雅退出实验1:

复制代码
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

  time.Sleep(5 * time.Second)
  fmt.Fprintln(w, "Hello world!")

})
server := &http.Server{

  Addr:         ":8080",
  Handler:      mux,

}
go server.ListenAndServe()

listenSignal(context.Background(), server)
}

func listenSignal(ctx context.Context, httpSrv *http.Server) {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)

select {
case <-sigs:

  fmt.Println("notify sigs")
  httpSrv.Shutdown(ctx)
  fmt.Println("http shutdown")

}
}
复制代码
我们创建了一个 listenSignal 函数来监听程序退出信号 listenSignal 函数中的 select 会一直阻塞直到收到退出信号,然后执行 Shutdown(ctx) 。

可以看到,我们是重新开启了一个 goroutine 来启动 http 服务监听,而 Shutdown(ctx) 在主 goroutine 中,这样才能等待所有连接闲置后再退出程序。

启动上述程序,我们访问 http://127.0.0.1:8080 页面等待 5s 会输出一个 “Hello world!” 在等待期间,我们可以尝试 Ctrl+C 关闭程序,可以看程序控制台会等待输出后才打印 http shutdown 同时浏览器会显示输出内容;而关闭程序之后再新开一个浏览器窗口访问 http://127.0.0.1:8080 则新开的窗口直接断开无法访问。(这些操作需要在 5s 内完成,可以适当调整处理时间方便我们观察实验结果)

通过该实验我们能看到,Shutdown(ctx) 会阻止新的连接进入并等待活跃连接处理完成后再终止程序,达到优雅退出的目的。

当然我们还可以进一步证明 Shutdown(ctx) 除了等待活跃连接的同时也会监听 Context 完成事件,二者有一个触发都会触发程序终止;

我们将代码稍作修改如下:

复制代码
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

  time.Sleep(10 * time.Second)
  fmt.Fprintln(w, "Hello world!")

})
server := &http.Server{

  Addr:         ":8080",
  Handler:      mux,

}
go server.ListenAndServe()

listenSignal(context.Background(), server)
}

func listenSignal(ctx context.Context, httpSrv *http.Server) {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)

select {
case <-sigs:

  timeoutCtx,_ := context.WithTimeout(ctx, 3*time.Second)
  fmt.Println("notify sigs")
  httpSrv.Shutdown(timeoutCtx)
  fmt.Println("http shutdown")

}
}
复制代码
我们将 http 服务处理修改成等待 10s, 监听到退出事件后 ctx 修改成 3s 超时的 Context,运行上述程序,然后 Ctrl+C 发送结束信号,我们可以直观的看到,程序在等待 3s 后就终止了,此时即使 http 服务中的处理还没完成,程序也终止了,浏览器中也直接中断连接了。

需要注意的问题:我们在 HandleFunc 中编写的处理逻辑都是在主 goroutine 中完成的和 Shotdown 方法是一个同步操作,因此 Shutdown(ctx) 会等待完成,如果我们的处理逻辑是在新的 goroutine 中或是一个像 Websock 这样的长连接,则Shutdown(ctx) 不会等待处理完成,如果需要解决这类问题还是需要利用 sync.WaitGroup 来进行同步等待。

技术总结:

  1. Shutdown 方法要写在主 goroutine 中;

2.在主 goroutine 中的处理逻辑才会阻塞等待处理;

3.带超时的 Context 是在创建时就开始计时了,因此需要在接收到结束信号后再创建带超时的 Context。

给大家推荐一个框架来快速构建带优雅退出功能的 http 服务,详见: https://www.cnblogs.com/zhucheer/p/12341595.html

https://www.cnblogs.com/zhucheer/p/12337277.html

您可以自由的转载和修改,但请务必注明文章来源并且不可用于商业目的。
本站大部分内容收集于互联网,如果有侵权内容、不妥之处,请联系删除。敬请谅解!

添加新评论

  关于博主【WANG-FEiHU】

Duplicate
-----------Complicate
--------------------------Appreciate
-----------------------------------------Fate

闻先后,术专攻
三人有师
习得文武艺,货与帝王家
人性不曾变,资本永无眠

-----------花有重开日,人无再少年-----------

  分类目录

  monitor(TD)

往前一步是黄昏,退后一步是人生

渡口边最后一面洒下了句点,与你若只如初见 何须感伤离别

生活远没有咖啡那么苦涩,关键是喝它的人怎么品味!每个人都喜欢和向往随心所欲的生活,殊不知随心所欲根本不是生活。

如果错过了太阳时你流泪了,那么你也要错过群星了。

不如意的时候不要尽往悲伤里钻,想想有笑声的日子吧。

我不明白为什么要那么在意别人的看法,评头论足只是无聊人的消遣,何必看得如临大敌。如果你不吃别人家的饭,就别太把别人的话放在心上。