Go 服務優雅重啓 - facebook-grace 學習

【導讀】線上服務在發佈時有減少對當前業務影響的需求,會採用優雅重啓的方案。本文詳細介紹了 facebook 開源庫 grace 的使用。

逐步分析

猜測

查閱相關資料後,大概猜測出做法

服務重啓時,舊進程並不直接停止,而是用舊進程 fork 一個新進程,同時舊進程的所有句柄都 dup 到新進程。這時新的請求都由新的進程處理,舊進程在處理完自己的任務後,自行退出。

這只是大概流程,裏面還有許多細節需要考慮

分析 grace

github

https://github.com/facebookarchive/grace

流程簡述

  1. 利用啓動時的參數(包括命令行參數、環境變量等),重新啓動新進程。同時將當前 socket 句柄給新進程。

  2. 舊進程不再 Accept,待當前任務結束後,進程退出

源碼分析

如何啓動新進程
// facebookgo/grace/gracenet/net.go:206(省略非核心代碼)

func (n *Net) StartProcess() (int, error) {
    listeners, err := n.activeListeners()

  // 複製 socket 句柄
    files := make([]*os.File, len(listeners))
    for i, l := range listeners {
        files[i]err = l.(filer).File()
        defer files[i].Close()
    }

  // 複製標準 IO 句柄
    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,
    })
    return process.Pid, nil
}

這段代碼是啓動新進程的過程。

注:這裏傳遞的句柄只包括 socket 句柄與標準 IO 句柄。

舊進程如何退出

舊進程退出需要確保當前的請求全部處理完成。同時不再接收新的請求。

  1. 如何不接收新的請求

回答這個問題需要提到socket 流程

通常建立 socket 需要經歷以下四步:

通常,accept 處於一個循環中,這樣就能持續處理請求。所以若不想接收新請求,只需退出循環,不再 accept 即可。

  1. 如何確保當前請求全部處理完成

回答這個問題,我們需要給每一個連接賦予一系列狀態。恰好,net/http包幫我們做好了這件事。

// GOROOT/net/http/server.go:2743

type ConnState int

const (
  // 新連接剛建立時
    StateNew ConnState = iota

  // 連接處於活躍狀態,即正在處理的請求
    StateActive

  // 連接處於空閒狀態,一般用於 keep-alive
    StateIdle

  // 劫持狀態,可以理解爲關閉狀態
    StateHijacked

  // 關閉狀態
    StateClosed
)

通過狀態,我們就能精確判斷所有請求是否處理完成。只要所有活躍(StateActive)的連接都成爲空閒(StateIdle)或者關閉(StateClosed)狀態。就可以保證請求全部處理完成。

具體代碼

// facebookgo/httpdown/httpdown.go:347

func ListenAndServe(s *http.Server, hd *HTTP) error {
  // 監聽端口,提供服務
    hs, err := hd.ListenAndServe(s)

    signals := make(chan os.Signal, 10)
    signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)

  // 監聽信號量 2 和 15(即 kill -2 -15)
    select {
    case <-signals:
        signal.Stop(signals)
    // hs.Stop() 開始停止服務
        if err := hs.Stop(); err != nil {
            return err
        }
    }
}

這段代碼是啓動服務的入口代碼

可以看出,服務退出的邏輯都在hs.Stop()

// facebookgo/httpdown/httpdown.go:293

func (s *server) Stop() error {
    s.stopOnce.Do(func() {
    // 禁止 keep-alive
        s.server.SetKeepAlivesEnabled(false)

    // 關閉 listener, 不再接收請求
        closeErr := s.listener.Close()
        <-s.serveDone

    // 通過 stop(一個 chan),傳遞關閉信號
        stopDone := make(chan struct{})
        s.stop <- stopDone

        // 若在 s.stopTimeout 以內沒有結束,則強行 kill 所有連接。默認 s.stopTimeout 爲 1min
        select {
        case <-stopDone:
        case <-s.clock.After(s.stopTimeout):
            // stop timed out, wait for kill
            killDone := make(chan struct{})
            s.kill <- killDone
        }
    })}

Stop 方法

那麼,等待所有請求處理完畢的邏輯,應該處於消費 s.stop 的地方。

這裏我們注意到,最核心的結構體有這樣幾個屬性

// facebookgo/httpdown/httpdown.go:126

type server struct {
  ...
  
    new    chan net.Conn
    active chan net.Conn
    idle   chan net.Conn
    closed chan net.Conn
    stop   chan chan struct{}
    kill   chan chan struct{}

  ...
}

stop 和 kill 說過了,是用來傳遞停止和強行終止信號的。

其餘newactiveidleclosed是用來記錄處於不同狀態的連接的。

我們記錄了不同狀態的連接,那麼在關閉時,就能等連接處於 “空閒“或” 關閉“時再關閉它。

// facebookgo/httpdown/httpdown.go:233

case c := <-s.idle:
    conns[c] = http.StateIdle

    // 那些處於“活躍”的連接,會等到它轉爲“空閒”時,將其關閉
    if stopDone != nil {
        c.Close()
    }
case c := <-s.closed:
    // 所有連接關閉後,退出
    if stopDone != nil && len(conns) == 0 {
        close(stopDone)
        return
    }
case stopDone = <-s.stop:
  // 所有連接關閉後,退出
    if len(conns) == 0 {
        close(stopDone)
        return
    }

// 關閉所有“空閒”連接
    for c, cs := range conns {
        if cs == http.StateIdle {
            c.Close()
        }
    }

這裏可以看出,當接收到關閉信號時(stopDone = <-s.stop)

總結

進程重啓主要就是如何退出、如何啓動。grace 代碼量不多,以上敘述了核心的邏輯,有興趣的同學可以 fork github 源碼研讀。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Ct2VE1FgZ_bqhbFki38z5A