共计 3211 个字符,预计需要花费 9 分钟才能阅读完成。
代码如下:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"sync"
"time"
"github.com/gin-gonic/gin"
)
var wg sync.WaitGroup
var 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.go
func (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