原文 Graceful Restart in Golanghtml
做者 grishagit
聲明:本文目的僅僅做爲我的mark,因此在翻譯的過程當中參雜了本身的思想甚至改變了部份內容,其中有下劃線的文字爲譯者添加。但因爲譯者水平有限,所寫文字或者代碼可能會誤導讀者,如發現文章有問題,請儘快告知,不勝感激。github
Update (Apr 2015): Florian von Bock已經根據本文實現了一個叫作endless的Go packagegolang
你們知道,當咱們用Go寫的web服務器須要修改配置或者須要升級代碼的時候咱們須要重啓服務器,普通的重啓會有一段宕機的時間,但優雅重啓則否則:web
本文中的優雅重啓表現爲兩點windows
有不止一種方法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}
refer^{[1]}
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通常來講有兩個方法:
另外,也能夠參考:Go中如何優雅地關閉net.Listener