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

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

梗概

主要介紹服務優雅重啓的基本概念。git

逐步分析

猜想

查閱相關資料後,大概猜想出作法github

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

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

分析grace

github

https://github.com/facebookar...源碼分析

流程簡述

  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
}

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

  • 變量files保存listeners句柄(即socket句柄)
  • 變量allFiles保存files+stdout、stdin、stderr句柄
  • os.StartProcess啓動新進程,並傳遞父進程句柄
注:這裏傳遞的句柄只包括socket句柄與標準IO句柄。
舊進程如何退出

舊進程退出須要確保當前的請求所有處理完成。同時再也不接收新的請求。命令行

  1. 如何不接收新的請求

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

一般創建socket須要經歷如下四步:server

  • socket
  • bind
  • listen
  • accept

一般,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
        }
    }
}

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

  • ListenAndServe 監聽端口,提供http服務
  • signal.Notify 註冊要監聽的信號量,這裏監聽syscall.SIGTERMsyscall.SIGINT,即通常終止進程的信號量
  • hs.Stop() 中止服務,結束當前進程

能夠看出,服務退出的邏輯都在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方法

  • 禁止keep-alive
  • 關閉listener,即再也不accept新請求
  • 想s.stop(一個chan)傳遞關閉的信號
  • 若s.stopTimeout時間內,沒有退出,則強行kill全部鏈接。

那麼,等待全部請求處理完畢的邏輯,應該處於消費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源碼研讀。

相關文章
相關標籤/搜索