Golang開發支援平滑升級(優雅重啟)的HTTP服務

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

原文連結:http://tabalt.net/blog/gracef...
Golang支援平滑升級(優雅重啟)的包已開源到Github:https://github.com/tabalt/gracehttp,歡迎使用和貢獻代碼。

前段時間用Golang在做一個HTTP的介面,因編譯型語言的特性,修改了代碼需要重新編譯可執行檔,關閉正在啟動並執行老程式,並啟動新程式。對於訪問量較大的面向使用者的產品,關閉、重啟的過程中勢必會出現無法訪問的情況,從而影響使用者體驗。

使用Golang的系統包開發HTTP服務,是無法支援平滑升級(優雅重啟)的,本文將探討如何解決該問題。

一、平滑升級(優雅重啟)的一般思路

一般情況下,要實現平滑升級,需要以下幾個步驟:

  1. 用新的可執行檔替換老的可執行檔(如只需優雅重啟,可以跳過這一步)

  2. 通過pid給正在啟動並執行老進程發送 特定的訊號(kill -SIGUSR2 $pid)

  3. 正在啟動並執行老進程,接收到指定的訊號後,以子進程的方式啟動新的可執行檔並開始處理新請求

  4. 老進程不再接受新的請求,等待未完成的服務處理完畢,然後正常結束

  5. 新進程在父進程退出後,會被init進程領養,並繼續提供服務

二、Golang Socket 網路編程

Socket是程式員層面上對傳輸層協議TCP/IP的封裝和應用。Golang中Socket相關的函數與結構體定義在net包中,我們從一個簡單的例子來學習一下Golang Socket 網路編程,關鍵說明直接寫在注釋中。

1、服務端程式 server.go

package mainimport (    "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")))    }}

2、用戶端程式 client.go

package mainimport (    "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())    }}

3、運行樣本程式

# 運行服務端程式go run server.go# 在另一個命令列視窗運行用戶端程式go run client.go "tabalt"

三、Golang HTTP 編程

HTTP是基於傳輸層協議TCP/IP的應用程式層協議。Golang中HTTP相關的實現在net/http包中,直接用到了net包中Socket相關的函數和結構體。

我們再從一個簡單的例子來學習一下Golang HTTP 編程,關鍵說明直接寫在注釋中。

1、http服務程式 http.go

package mainimport (    "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)}

2、運行樣本程式

# 運行HTTP服務程式go run http.go# 在另一個命令列視窗curl請求測試頁面curl http://localhost:8086/hello/# 輸出如下內容:http hello on golang

四、Golang net/http包中 Socket操作的實現

從上面的簡單樣本中,我們看到在Golang中要啟動一個http服務,只需要簡單的三步:

  1. 定義http請求的處理方法

  2. 註冊http請求的處理方法

  3. 在某個連接埠啟動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請求。

五、Golang 平滑升級(優雅重啟)HTTP服務的實現

我建立了一個新的包gracehttp來實現支援平滑升級(優雅重啟)的HTTP服務,為了少寫代碼和降低使用成本,新的包儘可能多地利用net/http包的實現,並和net/http包保持一致的對外方法。現在開始我們來看gracehttp包支援平滑升級 (優雅重啟)Golang HTTP服務涉及到的細節如何?。

1、Golang處理訊號

Golang的os/signal包封裝了對訊號的處理。簡單用法請看樣本:

package mainimport (    "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)    }}

2、子進程啟動新程式,監聽相同的連接埠

在第四部分的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}

3、父進程等待已有串連中未完成的請求處理完畢

這一塊是最複雜的;首先我們需要一個計數器,在成功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()}

4、gracehttp包的用法

gracehttp包已經應用到每天幾億PV的項目中,也開源到了github上:github.com/tabalt/gracehttp,使用起來非常簡單。

如以下範例程式碼,引入包後只需修改一個關鍵字,將http.ListenAndServe 改為 gracehttp.ListenAndServe即可。

package mainimport (    "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。

如果文章對您有協助,歡迎打賞, 您的支援是我碼字的動力!

原文連結:http://tabalt.net/blog/gracef...

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.