关于go:Go-httpServer-graceful-shutdown遇到的奇怪问题

5次阅读

共计 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

正文完
 0