[譯]Golang中的優雅重啟


原文 Graceful Restart in Golang

作者 grisha

聲明:本文目的僅僅作為個人mark,所以在翻譯的過程中參雜了自己的思想甚至改變了部分內容,其中有下划線的文字為譯者添加。但由於譯者水平有限,所寫文字或者代碼可能會誤導讀者,如發現文章有問題,請盡快告知,不勝感激。


前言

Update (Apr 2015): Florian von Bock已經根據本文實現了一個叫做endless的Go package

大家知道,當我們用Go寫的web服務器需要修改配置或者需要升級代碼的時候我們需要重啟服務器,普通的重啟會有一段宕機的時間,但優雅重啟則不然:

什么是優雅重啟

本文中的優雅重啟表現為兩點

  1. 進程在不關閉其所監聽的端口的情況下重啟
  2. 重啟過程中保證所有請求能被正確的處理

1.進程在不關閉其所監聽的端口的情況下重啟

  • fork一個子進程,該子進程繼承了父進程所監聽的socket
  • 子進程執行初始化等操作,並最終開始接收該socket的請求
  • 父進程停止接收請求並等待當前處理的請求終止
fork一個子進程

有不止一種方法fork一個子進程,但在這種情況下推薦exec.Command,因為Cmd結構提供了一個字段ExtraFiles,該字段(注意不支持windows)為子進程額外地指定了需要繼承的額外的文件描述符,不包含std_in, std_out, std_err
需要注意的是,ExtraFiles描述中有這樣一句話:

If non-nil, entry i becomes file descriptor 3+i

這句是說,索引位置為i的文件描述符傳過去,最終會變為值為i+3的文件描述符。ie: 索引為0的文件描述符565, 最終變為文件描述符3

file := netListener.File() // this returns a Dup()
path := "/path/to/executable"
args := []string{
    "-graceful"}

cmd := exec.Command(path, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.ExtraFiles = []*os.File{file}

err := cmd.Start()
if err != nil {
    log.Fatalf("gracefulRestart: Failed to launch, error: %v", err)
}

上面的代碼中,netListener是一個net.Listener類型的指針,path變量則是我們要更新的新的可執行文件的路徑。

需要注意的是:上面netListener.File()dup函數類似,返回的是一個拷貝的文件描述符。另外,該文件描述符不應該設置FD_CLOEXEC標識,這將會導致出現我們不想要的結果:子進程的該文件描述符被關閉。

你可能會想到可以使用命令行參數把該文件描述符的值傳遞給子進程,但相對來說,我使用的這種方式更為簡單

最終,args數組包含了一個-graceful選項,你的進程需要以某種方式通知子進程要復用父進程的描述符而不是新打開一個。

子進程初始化
server := &http.Server{Addr: "0.0.0.0:8888"}

var gracefulChild bool
var l net.Listever
var err error

flag.BoolVar(&gracefulChild, "graceful", false, "listen on fd open 3 (internal use only)")

if gracefulChild {
    log.Print("main: Listening to existing file descriptor 3.")
    f := os.NewFile(3, "")
    l, err = net.FileListener(f)
} else {
    log.Print("main: Listening on a new file descriptor.")
    l, err = net.Listen("tcp", server.Addr)
}
通知父進程停止
if gracefulChild {
    parent := syscall.Getppid()
    log.Printf("main: Killing parent pid: %v", parent)
    syscall.Kill(parent, syscall.SIGTERM)
}

server.Serve(l)
父進程停止接收請求並等待當前所處理的所有請求結束

為了做到這一點我們需要使用sync.WaitGroup來保證對當前打開的連接的追蹤,基本上就是:每當接收一個新的請求時,給wait group做原子性加法,當請求結束時給wait group做原子性減法。也就是說wait group存儲了當前正在處理的請求的數量

var httpWg sync.WaitGroup

匆匆一瞥,我發現go中的http標准庫並沒有為Accept()和Close()提供鈎子函數,但這就到了interface展現其魔力的時候了(非常感謝 Jeff R. Allen的這篇文章)

下面是一個例子,該例子實現了每當執行Accept()的時候會原子性增加wait group。首先我們先繼承net.Listener實現一個結構體

type gracefulListener struct {
    net.Listener
    stop    chan error
    stopped bool
}

func (gl *gracefulListener) File() *os.File {
    tl := gl.Listener.(*net.TCPListener)
    fl, _ := tl.File()
    return fl
}

接下來我們覆蓋Accept方法(暫時先忽略gracefulConn)

func (gl *gracefulListener) Accept() (c net.Conn, err error) {
    c, err = gl.Listener.Accept()
    if err != nil {
        return
    }

    c = gracefulConn{Conn: c}

    httpWg.Add(1)
    return
}

我們還需要一個構造函數以及一個Close方法,構造函數中另起一個goroutine關閉,為什么要另起一個goroutine關閉,請看refer^{[1]}

func newGracefulListener(l net.Listener) (gl *gracefulListener) {
    gl = &gracefulListener{Listener: l, stop: make(chan error)}
    // 這里為什么使用go 另起一個goroutine關閉請看文章末尾
    go func() {
        _ = <-gl.stop
        gl.stopped = true
        gl.stop <- gl.Listener.Close()
    }()
    return
}

func (gl *gracefulListener) Close() error {
    if gl.stopped {
        return syscall.EINVAL
    }
    gl.stop <- nil
    return <-gl.stop
}

我們的Close方法簡單的向stop chan中發送了一個nil,讓構造函數中的goroutine解除阻塞狀態並執行Close操作。最終,goroutine執行的函數釋放了net.TCPListener文件描述符。

接下來,我們還需要一個net.Conn的變種來原子性的對wait group做減法

type gracefulConn struct {
    net.Conn
}

func (w gracefulConn) Close() error {
    httpWg.Done()
    return w.Conn.Close()
}

為了讓我們上面所寫的優雅啟動方案生效,我們需要替換server.Serve(l)行為:

netListener = newGracefulListener(l)
server.Serve(netListener)

最后補充:我們還需要避免客戶端長時間不關閉連接的情況,所以我們創建server的時候可以指定超時時間:

server := &http.Server{
        Addr:           "0.0.0.0:8888",
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 16}

譯者總結

譯者注:

  1. refer^{[1]}
    在上面的代碼中使用goroutine的原因作者寫了一部分,但我並沒有讀懂,但幸好在評論中,jokingus問道:如果用下面的方式,是否就不需要在newGracefulListener中使用那個goroutine函數了
func (gl *gracefulListener) Close() error { 
    // some code
    gl.Listener.Close()
}

作者回復道:

Honestly, I cannot fathom why there would need to be a goroutine for this, and simply doing gl.Listener.Close() like you suggest wouldn't work.... May be there is some reason that is escaping me presently, or perhaps I just didn't know what I was doing? If you get to the bottom of it, would you post here, so I can correct the post if this goroutine business is wrong?

作者自己也較為疑惑,但表示像jokingus所提到的這種方式是行不通的

譯者的個人理解:在絕大多數情況下,需要一個goroutine(可以稱之為主goroutine)來創建socket,監聽該socket,並accept直到有請求到達,當請求到來之后再另起goroutine進行處理。首先因為accept一般處於主goroutine中,且其是一個阻塞操作,如果我們想在accept執行后關閉socket一般來說有兩個方法:

  • 為accept設置一個超時時間,到達超時時間后,檢測是否需要close socket,如果需要就關閉。但這樣的話我們的超時時間可定不能設置太大,這樣結束就不夠靈敏,但設置的太小,就會對性能影響很大,總之來說不夠優雅。
  • accept方法可以一直阻塞,當我們需要close socket的時候,在另一個goroutine執行流中關閉socket,這樣相對來說就比較優雅了,作者所使用的方法就是這種

另外,也可以參考:Go中如何優雅地關閉net.Listener


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM