代码如下:
package mainimport ( "context" "log" "net/http" "os" "os/signal" "sync" "time" "github.com/gin-gonic/gin")var wg sync.WaitGroupvar apiQuit = make(chan bool)func apiQuitSignal() { log.Println("quit signal") apiQuit <- true}func main() { router := gin.New() router.GET("/quit", func(c *gin.Context) { log.Println("GET /quit") apiQuitSignal() //time.AfterFunc(5*time.Second, apiQuitSignal) c.String(200, "quit") //c.String(200, "quit in 5 seconds") }) router.GET("/hello", func(c *gin.Context) { log.Println("GET /hello") c.String(200, "hello") }) srv := &http.Server{ Addr: ":8888", Handler: router, } register := func(f func()) { wg.Add(1) srv.RegisterOnShutdown(func() { defer wg.Done() f() }) } register(func() { time.Sleep(10 * time.Second) }) go func() { // 服务连贯 if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("listen: %s\n", err) } }() quit := make(chan os.Signal) signal.Notify(quit, os.Interrupt) select { case <-quit: log.Println("quit from os.Signal") case <-apiQuit: log.Println("quit from api") } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) //ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Fatal("Server Shutdown:", err) } wg.Wait() log.Println("Server Exit")}
下面的服务应用apiQuit从申请quit时取得shutdown的信号,而后进行graceful shutdown操作。
而后在申请的过程中,呈现了一些奇怪的景象,表述如下:
一,在服务器启动后,仅申请quit接口,会报
Server Shutdown:context deadline exceeded
这个显著是srv.Shutdown(ctx)的时候context超时了。
二,在服务器启动后,先申请hello接口,再申请quit接口,能够失常退出。
三,在服务器启动后,先申请quit接口,再立即申请hello接口,能够失常退出。
四,减少context的超时工夫到10秒,能够失常退出。
这显著是Shutdown的代码有些奇怪的feature(或者bug)。
看代码:
// file: net/http/server.gofunc (srv *Server) Shutdown(ctx context.Context) error { srv.inShutdown.setTrue() srv.mu.Lock() lnerr := srv.closeListenersLocked() srv.closeDoneChanLocked() for _, f := range srv.onShutdown { go f() } srv.mu.Unlock() pollIntervalBase := time.Millisecond nextPollInterval := func() time.Duration { // Add 10% jitter. interval := pollIntervalBase + time.Duration(rand.Intn(int(pollIntervalBase/10))) // Double and clamp for next time. pollIntervalBase *= 2 if pollIntervalBase > shutdownPollIntervalMax { pollIntervalBase = shutdownPollIntervalMax } return interval } timer := time.NewTimer(nextPollInterval()) defer timer.Stop() for { if srv.closeIdleConns() && srv.numListeners() == 0 { return lnerr } select { case <-ctx.Done(): return ctx.Err() case <-timer.C: timer.Reset(nextPollInterval()) } }}func (s *Server) closeIdleConns() bool { s.mu.Lock() defer s.mu.Unlock() quiescent := true for c := range s.activeConn { st, unixSec := c.getState() // Issue 22682: treat StateNew connections as if // they're idle if we haven't read the first request's // header in over 5 seconds. if st == StateNew && unixSec < time.Now().Unix()-5 { st = StateIdle } if st != StateIdle || unixSec == 0 { // Assume unixSec == 0 means it's a very new // connection, without state set yet. quiescent = false continue } c.rwc.Close() delete(s.activeConn, c) } return quiescent}
先找到Shutdown函数,看到ctx.Done(),超时报错在这里,而后定位srv.closeIdleConns()
再找到closeIdleConns办法,能够看到次要代码就是一个
for c := range s.activeConn
而后留神上面的代码
// Issue 22682: treat StateNew connections as if// they're idle if we haven't read the first request's// header in over 5 seconds.if st == StateNew && unixSec < time.Now().Unix()-5 { st = StateIdle}
如果连贯的状态是StateNew的时候,会提早到5秒(实际上并不是严格的5秒,参考Shutdown函数里的timer机制)能力转为StateIdle。
并且这个问题曾经有人提到了,参考Issue 22682
https://github.com/golang/go/issues/22682
解决的办法也就很简略:
办法一、在quit申请完结5秒后再发信号之后再调用Shutdown
办法二、在发送了quit信号后阻塞5秒再调用Shutdown