原文連接:http://tabalt.net/blog/gracef...
Golang支持平滑升級(優雅重啓)的包已開源到Github:https://github.com/tabalt/gracehttp,歡迎使用和貢獻代碼。git
前段時間用Golang在作一個HTTP的接口,因編譯型語言的特性,修改了代碼須要從新編譯可執行文件,關閉正在運行的老程序,並啓動新程序。對於訪問量較大的面向用戶的產品,關閉、重啓的過程當中勢必會出現沒法訪問的狀況,從而影響用戶體驗。程序員
使用Golang的系統包開發HTTP服務,是沒法支持平滑升級(優雅重啓)的,本文將探討如何解決該問題。github
通常狀況下,要實現平滑升級,須要如下幾個步驟:golang
用新的可執行文件替換老的可執行文件(如只需優雅重啓,能夠跳過這一步)編程
經過pid給正在運行的老進程發送 特定的信號(kill -SIGUSR2 $pid)網絡
正在運行的老進程,接收到指定的信號後,以子進程的方式啓動新的可執行文件並開始處理新請求app
老進程再也不接受新的請求,等待未完成的服務處理完畢,而後正常結束curl
新進程在父進程退出後,會被init進程領養,並繼續提供服務socket
Socket是程序員層面上對傳輸層協議TCP/IP的封裝和應用。Golang中Socket相關的函數與結構體定義在net包中,咱們從一個簡單的例子來學習一下Golang Socket 網絡編程,關鍵說明直接寫在註釋中。tcp
package main import ( "fmt" "log" "net" "time" ) func main() { // 監聽8086端口 listener, err := net.Listen("tcp", ":8086") if err != nil { log.Fatal(err) } defer listener.Close() for { // 循環接收客戶端的鏈接,沒有鏈接時會阻塞,出錯則跳出循環 conn, err := listener.Accept() if err != nil { fmt.Println(err) break } fmt.Println("[server] accept new connection.") // 啓動一個goroutine 處理鏈接 go handler(conn) } } func handler(conn net.Conn) { defer conn.Close() for { // 循環從鏈接中 讀取請求內容,沒有請求時會阻塞,出錯則跳出循環 request := make([]byte, 128) readLength, err := conn.Read(request) if err != nil { fmt.Println(err) break } if readLength == 0 { fmt.Println(err) break } // 控制檯輸出讀取到的請求內容,並在請求內容前加上hello和時間後向客戶端輸出 fmt.Println("[server] request from ", string(request)) conn.Write([]byte("hello " + string(request) + ", time: " + time.Now().Format("2006-01-02 15:04:05"))) } }
package main import ( "fmt" "log" "net" "os" "time" ) func main() { // 從命令行中讀取第二個參數做爲名字,若是不存在第二個參數則報錯退出 if len(os.Args) != 2 { fmt.Fprintf(os.Stderr, "Usage: %s name ", os.Args[0]) os.Exit(1) } name := os.Args[1] // 鏈接到服務端的8086端口 conn, err := net.Dial("tcp", "127.0.0.1:8086") checkError(err) for { // 循環往鏈接中 寫入名字 _, err = conn.Write([]byte(name)) checkError(err) // 循環從鏈接中 讀取響應內容,沒有響應時會阻塞 response := make([]byte, 256) readLength, err := conn.Read(response) checkError(err) // 將讀取響應內容輸出到控制檯,並sleep一秒 if readLength > 0 { fmt.Println("[client] server response:", string(response)) time.Sleep(1 * time.Second) } } } func checkError(err error) { if err != nil { log.Fatal("fatal error: " + err.Error()) } }
# 運行服務端程序 go run server.go # 在另外一個命令行窗口運行客戶端程序 go run client.go "tabalt"
HTTP是基於傳輸層協議TCP/IP的應用層協議。Golang中HTTP相關的實如今net/http包中,直接用到了net包中Socket相關的函數和結構體。
咱們再從一個簡單的例子來學習一下Golang HTTP 編程,關鍵說明直接寫在註釋中。
package main import ( "log" "net/http" "os" ) // 定義http請求的處理方法 func handlerHello(w http.ResponseWriter, r *http.Request) { w.Write([]byte("http hello on golang\n")) } func main() { // 註冊http請求的處理方法 http.HandleFunc("/hello", handlerHello) // 在8086端口啓動http服務,會一直阻塞執行 err := http.ListenAndServe("localhost:8086", nil) if err != nil { log.Println(err) } // http服務因故中止後 纔會輸出以下內容 log.Println("Server on 8086 stopped") os.Exit(0) }
# 運行HTTP服務程序 go run http.go # 在另外一個命令行窗口curl請求測試頁面 curl http://localhost:8086/hello/ # 輸出以下內容: http hello on golang
從上面的簡單示例中,咱們看到在Golang中要啓動一個http服務,只須要簡單的三步:
定義http請求的處理方法
註冊http請求的處理方法
在某個端口啓動HTTP服務
而最關鍵的啓動http服務,是調用http.ListenAndServe()函數實現的。下面咱們找到該函數的實現:
func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() }
這裏建立了一個Server的對象,並調用它的ListenAndServe()方法,咱們再找到結構體Server的ListenAndServe()方法的實現:
func (srv *Server) ListenAndServe() error { addr := srv.Addr if addr == "" { addr = ":http" } ln, err := net.Listen("tcp", addr) if err != nil { return err } return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}) }
從代碼上看到,這裏監聽了tcp端口,並將監聽者包裝成了一個結構體 tcpKeepAliveListener,再調用srv.Serve()方法;咱們繼續跟蹤Serve()方法的實現:
func (srv *Server) Serve(l net.Listener) error { defer l.Close() var tempDelay time.Duration // how long to sleep on accept failure for { rw, e := l.Accept() if e != nil { if ne, ok := e.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } return e } tempDelay = 0 c, err := srv.newConn(rw) if err != nil { continue } c.setState(c.rwc, StateNew) // before Serve can return go c.serve() } }
能夠看到,和咱們前面Socket編程的示例代碼同樣,循環從監聽的端口上Accept鏈接,若是返回了一個net.Error而且這個錯誤是臨時性的,則會sleep一個時間再繼續。 若是返回了其餘錯誤則會終止循環。成功Accept到一個鏈接後,調用了方法srv.newConn()對鏈接作了一層包裝,最後啓了一個goroutine處理http請求。
我建立了一個新的包gracehttp來實現支持平滑升級(優雅重啓)的HTTP服務,爲了少寫代碼和下降使用成本,新的包儘量多地利用net/http
包的實現,並和net/http
包保持一致的對外方法。如今開始咱們來看gracehttp
包支持平滑升級 (優雅重啓)Golang HTTP服務涉及到的細節如何實現。
Golang的os/signal
包封裝了對信號的處理。簡單用法請看示例:
package main import ( "fmt" "os" "os/signal" "syscall" ) func main() { signalChan := make(chan os.Signal) // 監聽指定信號 signal.Notify( signalChan, syscall.SIGHUP, syscall.SIGUSR2, ) // 輸出當前進程的pid fmt.Println("pid is: ", os.Getpid()) // 處理信號 for { sig := <-signalChan fmt.Println("get signal: ", sig) } }
在第四部分的ListenAndServe()方法的實現代碼中能夠看到,net/http包中使用net.Listen
函數來監聽了某個端口,但若是某個運行中的程序已經監聽某個端口,其餘程序是沒法再去監聽這個端口的。解決的辦法是使用子進程的方式啓動,並將監聽端口的文件描述符傳遞給子進程,子進程裏從這個文件描述符實現對端口的監聽。
具體實現須要藉助一個環境變量來區分進程是正常啓動,仍是以子進程方式啓動的,相關代碼摘抄以下:
// 啓動子進程執行新程序 func (this *Server) startNewProcess() error { listenerFd, err := this.listener.(*Listener).GetFd() if err != nil { return fmt.Errorf("failed to get socket file descriptor: %v", err) } path := os.Args[0] // 設置標識優雅重啓的環境變量 environList := []string{} for _, value := range os.Environ() { if value != GRACEFUL_ENVIRON_STRING { environList = append(environList, value) } } environList = append(environList, GRACEFUL_ENVIRON_STRING) execSpec := &syscall.ProcAttr{ Env: environList, Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), listenerFd}, } fork, err := syscall.ForkExec(path, os.Args, execSpec) if err != nil { return fmt.Errorf("failed to forkexec: %v", err) } this.logf("start new process success, pid %d.", fork) return nil } func (this *Server) getNetTCPListener(addr string) (*net.TCPListener, error) { var ln net.Listener var err error if this.isGraceful { file := os.NewFile(3, "") ln, err = net.FileListener(file) if err != nil { err = fmt.Errorf("net.FileListener error: %v", err) return nil, err } } else { ln, err = net.Listen("tcp", addr) if err != nil { err = fmt.Errorf("net.Listen error: %v", err) return nil, err } } return ln.(*net.TCPListener), nil }
這一塊是最複雜的;首先咱們須要一個計數器,在成功Accept一個鏈接時,計數器加1,在鏈接關閉時計數減1,計數器爲0時則父進程能夠正常退出了。Golang的sync的包裏的WaitGroup能夠很好地實現這個功能。
而後要控制鏈接的創建和關閉,咱們須要深刻到net/http包中Server結構體的Serve()方法。重溫第四部分Serve()方法的實現,會發現若是要從新寫一個Serve()方法幾乎是不可能的,由於這個方法裏調用了好多個不可導出的內部方法,重寫Serve()方法幾乎要重寫整個net/http
包。
幸運的是,咱們還發如今 ListenAndServe()方法裏傳遞了一個listener給Serve()方法,並最終調用了這個listener的Accept()方法,這個方法返回了一個Conn的示例,最終在鏈接斷開的時候會調用Conn的Close()方法,這些結構體和方法都是可導出的!
咱們能夠定義本身的Listener結構體和Conn結構體,組合net/http
包中對應的結構體,並重寫Accept()和Close()方法,實現對鏈接的計數,相關代碼摘抄以下:
type Listener struct { *net.TCPListener waitGroup *sync.WaitGroup } func (this *Listener) Accept() (net.Conn, error) { tc, err := this.AcceptTCP() if err != nil { return nil, err } tc.SetKeepAlive(true) tc.SetKeepAlivePeriod(3 * time.Minute) this.waitGroup.Add(1) conn := &Connection{ Conn: tc, listener: this, } return conn, nil } func (this *Listener) Wait() { this.waitGroup.Wait() } type Connection struct { net.Conn listener *Listener closed bool } func (this *Connection) Close() error { if !this.closed { this.closed = true this.listener.waitGroup.Done() } return this.Conn.Close() }
gracehttp包已經應用到天天幾億PV的項目中,也開源到了github上:github.com/tabalt/gracehttp,使用起來很是簡單。
如如下示例代碼,引入包後只需修改一個關鍵字,將http.ListenAndServe 改成 gracehttp.ListenAndServe便可。
package main import ( "fmt" "net/http" "github.com/tabalt/gracehttp" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "hello world") }) err := gracehttp.ListenAndServe(":8080", nil) if err != nil { fmt.Println(err) } }
測試平滑升級(優雅重啓)的效果,能夠參考下面這個頁面的說明:
https://github.com/tabalt/gracehttp#demo
使用過程當中有任何問題和建議,歡迎提交issue反饋,也能夠Fork到本身名下修改以後提交pull request。
若是文章對您有幫助,歡迎打賞, 您的支持是我碼字的動力!