gracehttp優雅重啓Go服務詳解

前言

grace是facebook公司爲golang服務開發的優雅重啓和零停機部署的開源庫。可以實現服務重啓時,舊有連接不斷,新服務啓動後,新連接連入新服務,如此客戶端無感知。

使用方法

(1)獲取

go get github.com/facebookgo/grace/gracehttp

mod可以使用如下方式引入:

require github.com/facebookgo/grace latest

(2)使用

gracehttp.Serve(
        &http.Server{Addr: *address0, Handler: newHandler("Zero  ")},
        &http.Server{Addr: *address1, Handler: newHandler("First ")},
        &http.Server{Addr: *address2, Handler: newHandler("Second")},
    )

(3)重啓命令

sudo kill -USR2 pidof yourservername
pidof yourservername 是指需要重啓的服務進程id

源碼解析

啓動服務

// Serve will serve the given http.Servers and will monitor for signals
// allowing for graceful termination (SIGTERM) or restart (SIGUSR2).
func Serve(servers ...*http.Server) error {
    a := newApp(servers)
    return a.run()
}

func newApp(servers []*http.Server) *app {
    return &app{
        servers:   servers,
        http:      &httpdown.HTTP{},
        net:       &gracenet.Net{},
        listeners: make([]net.Listener, 0, len(servers)),
        sds:       make([]httpdown.Server, 0, len(servers)),

        preStartProcess: func() error { return nil },
        // 2x num servers for possible Close or Stop errors + 1 for possible
        // StartProcess error.
        errors: make(chan error, 1+(len(servers)*2)),
    }
}

構造app,具體的處理邏輯均在app中進行。

注意:

此處註釋說明了Serve會啓動指定的http.Servers,並且會監聽系統信號以允許優雅結束(SIGTERM)或重啓服務(SIGUSR2),實際代碼中還支持SIGINT結束服務進程,我們可以根據需求指定信號來決定是結束還是重啓服務。

func (a *app) run() error {
    // Acquire Listeners
    //獲取所有http.Server服務地址的Listeners
    if err := a.listen(); err != nil {
        return err
    }

    // Some useful logging.
    // logger的處理
    if logger != nil {
        if didInherit {
            if ppid == 1 {
                logger.Printf("Listening on init activated %s", pprintAddr(a.listeners))
            } else {
                const msg = "Graceful handoff of %s with new pid %d and old pid %d"
                logger.Printf(msg, pprintAddr(a.listeners), os.Getpid(), ppid)
            }
        } else {
            const msg = "Serving %s with pid %d"
            logger.Printf(msg, pprintAddr(a.listeners), os.Getpid())
        }
    }

    // Start serving.
    // 啓動各http服務
    a.serve()

    // Close the parent if we inherited and it wasn't init that started us.
    // 如果已經繼承,並且不是從init啓動,就說明是重啓進程,需要將原進程關閉。
    if didInherit && ppid != 1 {
        if err := syscall.Kill(ppid, syscall.SIGTERM); err != nil {
            return fmt.Errorf("failed to close parent: %s", err)
        }
    }

    // 此處爲最核心處理部分
    waitdone := make(chan struct{})
    go func() {
        defer close(waitdone)
        a.wait()
    }()

    select {
    case err := <-a.errors://原有服務處理及關閉時發生錯誤
        if err == nil {
            panic("unexpected nil error")
        }
        return err
    case <-waitdone://waitdone close後即可取值,此時意味着原有服務已完成且關閉
        if logger != nil {
            logger.Printf("Exiting pid %d.", os.Getpid())
        }
        return nil
    }
}

//依次啓動指定的http.Servers
func (a *app) serve() {
    for i, s := range a.servers {
        a.sds = append(a.sds, a.http.Serve(s, a.listeners[i]))
    }
}

具體服務啓動過程

過程如下:

  • 構造server

  • 設置ConnState,監聽各連接的狀態變化

  • 啓動新協程,manage server的各chan信號

  • 在新協程中正式啓動服務

Serve總入口

// 具體服務啓動管理
func (h HTTP) Serve(s *http.Server, l net.Listener) Server {
    stopTimeout := h.StopTimeout
    if stopTimeout == 0 {
        stopTimeout = defaultStopTimeout
    }
    killTimeout := h.KillTimeout
    if killTimeout == 0 {
        killTimeout = defaultKillTimeout
    }
    klock := h.Clock
    if klock == nil {
        klock = clock.New()
    }

    ss := &server{
        stopTimeout:  stopTimeout,//stop超時時間
        killTimeout:  killTimeout,//kill超時時間
        stats:        h.Stats,//數據統計
        clock:        klock,//數據統計的定時器
        oldConnState: s.ConnState,//原ConnState hook
        listener:     l,//服務listener
        server:       s,//具體服務
        serveDone:    make(chan struct{}),//服務啓動結束
        serveErr:     make(chan error, 1),//服務啓動錯誤
        new:          make(chan net.Conn),//新連接
        active:       make(chan net.Conn),//連接狀態Active
        idle:         make(chan net.Conn),//連接狀態Idle
        closed:       make(chan net.Conn),//連接狀態Closed
        stop:         make(chan chan struct{}),//stop通知
        kill:         make(chan chan struct{}),//kill通知
    }
    s.ConnState = ss.connState
    // 管理連接conns
    go ss.manage()
    // 啓動http服務
    go ss.serve()
    return ss
}

ConnState注入hook

設置ConnState,注入hook監聽各連接的狀態變化,發送至server的各統計狀態的chan中。

// 處理服務的連接狀態變化
func (s *server) connState(c net.Conn, cs http.ConnState) {
    if s.oldConnState != nil {
        s.oldConnState(c, cs)
    }

    switch cs {
    case http.StateNew:
        s.new <- c
    case http.StateActive:
        s.active <- c
    case http.StateIdle:
        s.idle <- c
    case http.StateHijacked, http.StateClosed:
        s.closed <- c
    }
}

manage管理各種chan信號的處理

manage處理了所有connState中監聽的連接變化,所有的連接會存儲在conns map中,隨着conn的變化進行增加、修改、刪除。

stop chan信號確認所有連接是否關閉,如存在未關閉的連接,則先關閉處理Idle狀態的連接,然後等待其他的狀態的連接繼續處理。

kill chan信號則強制關閉所有連接。

stop、kill chan信號及stopTimeout、killTimeout的使用均來自於Stop(結束服務)過程,後面會詳細分析。

stop chan信號在服務Stop時觸發,stop會優先關閉處於idle狀態的連接,然後等待其他狀態的連接繼續處理。當killTimeout觸發時,意味着
stop、kill chan信號對應stopTimeout、killTimeout參數。初始化中stopTimeout、killTimeout,如果有設置參數則使用設置的參數,沒有則使用默認參數(一分鐘)。stopTimeout是處理服務stop的超時時間,超時後,會觸發kill,killTimeout就是處理kill的超時時間。

特別說明:s.stats一直爲nil,其相關的代碼並不會執行,閱讀時可略過相關代碼。

// 管理各種chan信號的處理
// 說明:s.stats一直爲nil,此參數僅用來測試時使用,其相關的代碼可以略過
func (s *server) manage() {
    defer func() {
        close(s.new)
        close(s.active)
        close(s.idle)
        close(s.closed)
        close(s.stop)
        close(s.kill)
    }()
    var stopDone chan struct{}

    // 存儲服務的各連接的狀態
    conns := map[net.Conn]http.ConnState{}
    var countNew, countActive, countIdle float64

    // decConn decrements the count associated with the current state of the
    // given connection.
    // 更新計數
    decConn := func(c net.Conn) {
        switch conns[c] {
        default:
            panic(fmt.Errorf("unknown existing connection: %s", c))
        case http.StateNew:
            countNew--
        case http.StateActive:
            countActive--
        case http.StateIdle:
            countIdle--
        }
    }

    // setup a ticker to report various values every minute. if we don't have a
    // Stats implementation provided, we Stop it so it never ticks.
    // 僅在有使用Stats實現接口的情況開始定時器統計數據,實際代碼中並未使用,此處開啓後會被關閉
    statsTicker := s.clock.Ticker(time.Minute)
    if s.stats == nil {
        statsTicker.Stop()
    }
    // 等待所有連接處理
    for {
        select {
        case <-statsTicker.C: //每分鐘統計各狀態的連接數量,此項僅在測試源碼時生效,正式使用時,不生效
            // we'll only get here when s.stats is not nil
            s.stats.BumpAvg("http-state.new", countNew)
            s.stats.BumpAvg("http-state.active", countActive)
            s.stats.BumpAvg("http-state.idle", countIdle)
            s.stats.BumpAvg("http-state.total", countNew+countActive+countIdle)
        case c := <-s.new:
            conns[c] = http.StateNew //存入New狀態連接
            countNew++
        case c := <-s.active:
            decConn(c)
            countActive++

            conns[c] = http.StateActive //存入Active狀態連接
        case c := <-s.idle:
            decConn(c)
            countIdle++

            conns[c] = http.StateIdle //存入Idle狀態連接

            // if we're already stopping, close it
            if stopDone != nil { //已經完成stop,直接關閉連接
                c.Close()
            }
        case c := <-s.closed:
            stats.BumpSum(s.stats, "conn.closed", 1)
            decConn(c)
            delete(conns, c) //移除關閉的連接

            // if we're waiting to stop and are all empty, we just closed the last
            // connection and we're done.
            if stopDone != nil && len(conns) == 0 {
                close(stopDone)
                return
            }
        case stopDone = <-s.stop:
            // if we're already all empty, we're already done
            // 如果連接處理完畢,關閉chan,
            if len(conns) == 0 {
                close(stopDone)
                return
            }

            // close current idle connections right away
            // 先關閉idle狀態的連接
            for c, cs := range conns {
                if cs == http.StateIdle {
                    c.Close()
                }
            }

            // continue the loop and wait for all the ConnState updates which will
            // eventually close(stopDone) and return from this goroutine.

        case killDone := <-s.kill://強殺,超時時使用
            // force close all connections
            stats.BumpSum(s.stats, "kill.conn.count", float64(len(conns)))
            for c := range conns {//強制關閉所有連接
                c.Close()
            }

            // don't block the kill.
            close(killDone)

            // continue the loop and we wait for all the ConnState updates and will
            // return from this goroutine when we're all done. otherwise we'll try to
            // send those ConnState updates on closed channels.

        }
    }
}

正式的服務,則在新協程中調用http.Server的Serve啓動,啓動的錯誤發送至serveErr供啓動協程處理,如發生錯誤,則panic。若無錯誤,通知服務啓動結束。若存在Stop,則必須等server啓動結束後才能進行,這是必然的邏輯,因爲不可能stop一個未啓動的server。

// 正式啓動服務
func (s *server) serve() {
    stats.BumpSum(s.stats, "serve", 1)
    s.serveErr <- s.server.Serve(s.listener)
    close(s.serveDone)
    close(s.serveErr)
}

系統信號監聽

這部分是核心處理部分:

  • 啓動信號監聽協程

  • 監聽當前服務啓動及stop狀態,如有服務啓動及stop發生錯誤,則通過a.errors,通知啓動協程。

wait

// 等待原有http.Servers的連接處理完後,結束服務
func (a *app) wait() {
    var wg sync.WaitGroup
    wg.Add(len(a.sds) * 2) // Wait & Stop
    // 監聽信號
    go a.signalHandler(&wg)
    for _, s := range a.sds {
        go func(s httpdown.Server) {
            defer wg.Done()
            if err := s.Wait(); err != nil {
                a.errors <- err
            }
        }(s)
    }
    wg.Wait()
}

wait內的WaitGroup包含了Wait & Stop的處理,Wait用以等待各服務的啓動狀態結果監聽,Stop則是用以結束進程(包含重啓)狀態監聽,只有Wait & Stop均完成後,wait才能真正結束。結合之前的代碼,此時啓動協程才能算處理結束。

signalHandler

// 系統信號監聽服務
func (a *app) signalHandler(wg *sync.WaitGroup) {
    ch := make(chan os.Signal, 10)
    signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2)
    for {
        sig := <-ch
        switch sig {
        case syscall.SIGINT, syscall.SIGTERM:
            // this ensures a subsequent INT/TERM will trigger standard go behaviour of
            // terminating.
            // 停止接收信號
            signal.Stop(ch)
            // 結束原有服務
            a.term(wg)
            return
        case syscall.SIGUSR2:
            err := a.preStartProcess()
            if err != nil {
                a.errors <- err
            }
            // we only return here if there's an error, otherwise the new process
            // will send us a TERM when it's ready to trigger the actual shutdown.
            if _, err := a.net.StartProcess(); err != nil {
                a.errors <- err
            }
        }
    }
}

gracehttp默認監聽的信號爲:SIGINT、SIGTERM、SIGUSR2。其中SIGINT、SIGTERM用以優雅結束服務,SIGUSR2用以優雅重啓服務。

關閉服務

關閉服務,目前由兩種形式:

  • 我們通過kill命令操作

  • 進程重啓時發現父進程時,主動調用syscall.Kill命令

這兩種形式實際結果是一致的,只是調用者不同。

//此處的term使用的WaitGroup就是wait中的wg
func (a *app) term(wg *sync.WaitGroup) {
    for _, s := range a.sds {
        go func(s httpdown.Server) {
            defer wg.Done()
            if err := s.Stop(); err != nil {
                a.errors <- err
            }
        }(s)
    }
}
func (s *server) Stop() error {
    s.stopOnce.Do(func() {
        defer stats.BumpTime(s.stats, "stop.time").End()
        stats.BumpSum(s.stats, "stop", 1)

        // first disable keep-alive for new connections
        // 關閉keep-alive,讓原conn處理完關閉
        s.server.SetKeepAlivesEnabled(false)

        // then close the listener so new connections can't connect come thru
        // 關閉listener,讓新連接無法連入
        closeErr := s.listener.Close()
        <-s.serveDone//等待服務啓動完畢,避免未啓動完成調用Stop

        // then trigger the background goroutine to stop and wait for it
        stopDone := make(chan struct{})
        s.stop <- stopDone//通知stop下的連接處理

        // wait for stop
        select {
        case <-stopDone://conn全部處理完畢
        case <-s.clock.After(s.stopTimeout)://觸發超時
            defer stats.BumpTime(s.stats, "kill.time").End()
            stats.BumpSum(s.stats, "kill", 1)

            // stop timed out, wait for kill
            killDone := make(chan struct{})
            s.kill <- killDone
            select {
            case <-killDone:
            case <-s.clock.After(s.killTimeout):
                // kill timed out, give up
                stats.BumpSum(s.stats, "kill.timeout", 1)
            }
        }

        if closeErr != nil && !isUseOfClosedError(closeErr) {
            stats.BumpSum(s.stats, "listener.close.error", 1)
            s.stopErr = closeErr
        }
    })
    return s.stopErr
}

term會關閉所有的服務,並將關閉服務發生的錯誤發送至s.erros供啓動協程處理。

Stop的處理流程如下:

  • 關閉keep-alive,新連接不再保持長連接

  • 關閉listener,不再接受新連接

  • 等待新服務進程啓動完畢

  • 發送stop信號,確定連接是否處理完畢,未處理完則先關閉處理IDLE的連接

  • 全部連接處理完,處理closeErr後返回

  • stopTimeout超時,則發送kill信號,強制關閉全部連接

  • 全部連接強制關閉後,處理closeErr後返回

  • killTimeout超時,不再繼續處理,處理closeErr後返回

  • term會在關閉發生錯誤則通知啓動協程,所有服務全部正常退出後,wait結束,原進程結束。

啓動新進程

重啓的邏輯整體上就是,新服務進程啓動後,會主動發送TERM信號結束原進程,原進程會在處理完原有連接或超時後退出。

func (n *Net) StartProcess() (int, error) {
    // 獲取http.Servers的Listeners
    listeners, err := n.activeListeners()
    if err != nil {
        return 0, err
    }

    // Extract the fds from the listeners.
    // 提取文件描述fds
    files := make([]*os.File, len(listeners))
    for i, l := range listeners {
        files[i], err = l.(filer).File()
        if err != nil {
            return 0, err
        }
        defer files[i].Close()
    }

    // Use the original binary location. This works with symlinks such that if
    // the file it points to has been changed we will use the updated symlink.
    // 使用原執行文件路徑
    argv0, err := exec.LookPath(os.Args[0])
    if err != nil {
        return 0, err
    }

    // Pass on the environment and replace the old count key with the new one.
    // env添加listeners的數量
    var env []string
    for _, v := range os.Environ() {
        if !strings.HasPrefix(v, envCountKeyPrefix) {
            env = append(env, v)
        }
    }
    env = append(env, fmt.Sprintf("%s%d", envCountKeyPrefix, len(listeners)))

    allFiles := append([]*os.File{os.Stdin, os.Stdout, os.Stderr}, files...)
    // 利用原參數(原執行文件路徑、參數、打開的文件描述等)啓動新進程
    process, err := os.StartProcess(argv0, os.Args, &os.ProcAttr{
        Dir:   originalWD,
        Env:   env,
        Files: allFiles,
    })
    if err != nil {
        return 0, err
    }
    return process.Pid, nil
}

總結

正常啓動程序(非重啓)到重啓:

  • 所有服務啓動,進程監聽系統信號,啓動協程通過wait監聽服務協程啓動及stop狀態。

  • 監聽到USR2信號,標識環境變量LISTEN_FDS,獲取服務執行文件路徑、參數、打開的文件描述及新增加的環境變量標識LISTEN_FDS,調用StartProcess啓動新進程

  • 新進程啓動,處理新連接。新進程檢測到環境變量LISTEN_FDS及進程的父進程id,調用syscall.Kill結束原進程,新進程等待父進程(原服務進程)的退出。

  • 父進程檢測到TERM信號,先停止接收系統信號,開始準備結束進程。若父進程存在未關閉的連接,則先關閉keep-alive,再關閉listener以阻止新連接連入。全部連接處理完關閉或超時後強制關閉所有連接後,wait內wg全部done。

  • wait處理結束,協程結束,父進程結束,僅留下新啓動的子進程服務。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章