[譯]Golang中的優雅重啓

原文 Graceful Restart in Golanghtml

做者 grishagit

聲明:本文目的僅僅做爲我的mark,因此在翻譯的過程當中參雜了本身的思想甚至改變了部份內容,其中有下劃線的文字爲譯者添加。但因爲譯者水平有限,所寫文字或者代碼可能會誤導讀者,如發現文章有問題,請儘快告知,不勝感激。github


前言

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

你們知道,當咱們用Go寫的web服務器須要修改配置或者須要升級代碼的時候咱們須要重啓服務器,普通的重啓會有一段宕機的時間,但優雅重啓則否則:web

什麼是優雅重啓

本文中的優雅重啓表現爲兩點windows

  1. 進程在不關閉其所監聽的端口的狀況下重啓
  2. 重啓過程當中保證全部請求能被正確的處理

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

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

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

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變量則是咱們要更新的新的可執行文件的路徑。less

須要注意的是:上面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

相關文章
相關標籤/搜索