Gong服務實現平滑重啟分析


平滑重啟是指能讓我們的程序在重啟的過程不中斷服務,新老進程無縫銜接,實現零停機時間(Zero-Downtime)部署;

平滑重啟是建立在優雅退出的基礎之上的,之前一篇文章介紹了相關實現:Golang中使用Shutdown特性對http服務進行優雅退出使用總結

目前實現平滑重啟的主要策略有兩種:

方案一:我們的服務如果是多機器部署,可以通過網關程序,將即將重啟服務的機器從網關下線,重啟完成后再重新上線,該方案適合多機器部署的企業級應用;

方案二:讓我們的程序實現自啟動,重啟子進程來實現平滑重啟,核心策略是通過拷貝文件描述符實現子進程和父進程切換,適合單機器部署應用;

 

今天我們就主要介紹方案二,讓我們的程序擁有平滑重啟的功能,相關實現參考一個開源庫:https://github.com/fvbock/endless

 

實現原理介紹

http 連接介紹:

我們知道,http 服務也是基於 tcp 連接,我們通過 golang http 包源碼也能看到底層是通過監聽 tcp 連接實現的;

func (srv *Server) ListenAndServe() error {
    if srv.shuttingDown() {
        return ErrServerClosed
    }
    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)})
}

 

復用 socket:

當程序開啟 tcp 連接監聽時會創建一個 socket 並返回一個文件描述符 handler 給我們的程序; 

通過拷貝文件描述符文件可以使 socket 不關閉繼續使用原有的端口,自然 http 連接也不會斷開,啟動一個相同的進程也不會出現端口被占用的問題;

通過如下代碼進行測試:

package main

import (
    "fmt"
    "net/http"
    "context"
    "time"
    "os"
    "os/signal"
    "syscall"
    "net"
    "flag"
    "os/exec"
)

var (
    graceful = flag.Bool("grace", false, "graceful restart flag")
    procType = ""
)

func main() {
    flag.Parse()
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

        fmt.Fprintln(w, fmt.Sprintf("Hello world! ===> %s", procType))
    })
    server := &http.Server{
        Addr:         ":8080",
        Handler:      mux,

    }

    var err error
    var listener net.Listener
    if *graceful {
        f := os.NewFile(3, "")
        listener, err = net.FileListener(f)
        procType = "fork process"
    } else {
        listener, _ = net.Listen("tcp", server.Addr)
        procType = "main process"

        //主程序開啟5s 后 fork 子進程
        go func() {
            time.Sleep(5*time.Second)
            forkSocket(listener.(*net.TCPListener))
        }()

    }

    err=server.Serve(listener.(*net.TCPListener))

    fmt.Println(fmt.Sprintf("proc exit %v", err))
}


func forkSocket(tcpListener *net.TCPListener) error {
    f, err := tcpListener.File()
    if err != nil {
        return err
    }

    args := []string{"-grace"}
    fmt.Println(os.Args[0], args)
    cmd := exec.Command(os.Args[0], args...)
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    // put socket FD at the first entry
    cmd.ExtraFiles = []*os.File{f}
    return cmd.Start()
}

該程序啟動后,等待 5s 會自動 fork 子進程,通過 ps 命令查看如圖可以看到有兩個進程同時共存:

然后我們可以通過瀏覽器訪問 http://127.0.0.1/ 可以看到會隨機顯示主進程或子進程的輸出;

寫一個測試代碼進行循環請求:

package main

import (
    "net/http"
    "io/ioutil"
    "fmt"
    "sync"
)

func main(){

    wg:=sync.WaitGroup{}
    wg.Add(100)
    for i:=0; i<100; i++ {
        go func(index int) {
            result:=getUrl(fmt.Sprintf("http://127.0.0.1:8080?%d", i))
            fmt.Println(fmt.Sprintf("loop:%d %s", index, result))
            wg.Done()
        }(i)
    }
    wg.Wait()
}

func getUrl(url string) string{
    resp, _ := http.Get(url)
    defer resp.Body.Close()
    body, _ := ioutil.ReadAll(resp.Body)
    return string(body)
}

 

 

能看到返回的數據也是有些是主進程有些是子進程。

 

切換過程:

在開啟新的進程和老進程退出的瞬間,會有一個短暫的瞬間是同時有兩個進程使用同一個文件描述符,此時這種狀態,通過http請求訪問,會隨機請求到新進程或老進程上,這樣也沒有問題,因為請求不是在新進程上就是在老進程上;當老進程結束后請求就會全部到新進程上進行處理,通過這種方式即可實現平滑重啟;

 

綜上,我們可以將核心的實現總結如下:

1.監聽退出信號;

2.監聽到信號后 fork 子進程,使用相同的命令啟動程序,將文件描述符傳遞給子進程;

3.子進程啟動后,父進程停止服務並處理正在執行的任務(或超時)退出;

4.此時只有一個新的進程在運行,實現平滑重啟。

 

一個完整的 demo 代碼,通過發送 USR1 信號,程序會自動創建子進程並關閉主進程,實現平滑重啟:

 

package main

import (
    "fmt"
    "net/http"
    "context"
    "os"
    "os/signal"
    "syscall"
    "net"
    "flag"
    "os/exec"
)

var (
    graceful = flag.Bool("grace", false, "graceful restart flag")
)

func main() {
    flag.Parse()
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

        fmt.Fprintln(w, "Hello world!")
    })
    server := &http.Server{
        Addr:         ":8080",
        Handler:      mux,

    }

    var err error
    var listener net.Listener
    if *graceful {
        f := os.NewFile(3, "")
        listener, err = net.FileListener(f)
    } else {
        listener, err = net.Listen("tcp", server.Addr)
    }
    if err != nil{
        fmt.Println(fmt.Sprintf("listener error %v", err))
        return
    }

    go listenSignal(context.Background(), server, listener)

    err=server.Serve(listener.(*net.TCPListener))
    fmt.Println(fmt.Sprintf("proc exit %v", err))
}


func forkSocket(tcpListener *net.TCPListener) error {
    f, err := tcpListener.File()
    if err != nil {
        return err
    }

    args := []string{"-grace"}
    fmt.Println(os.Args[0], args)
    cmd := exec.Command(os.Args[0], args...)
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    // put socket FD at the first entry
    cmd.ExtraFiles = []*os.File{f}
    return cmd.Start()
}




func listenSignal(ctx context.Context, httpSrv *http.Server, listener net.Listener) {
    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, syscall.USR1)


    select {
    case <-sigs:
        forkSocket(listener.(*net.TCPListener))
        httpSrv.Shutdown(ctx)
        fmt.Println("http shutdown")
    }
}

使用 apache 的 ab 壓測工具進行驗證一下,執行 ab -c 50 -t 20 http://127.0.0.1:8080/ 持續 50 的並發 20s,在壓測的期間向程序運行的pid發送 USR1 信號,可以看到壓測結果,沒有失敗的請求,由此可知,該方案實現平滑重啟是木有問題的。

 

最后給大家安利一個 Web 開發框架,該框架已經將平滑重啟進行的封裝,開箱即用,快速構建一個帶平滑重啟的 Web 服務。

框架源碼:https://gitee.com/zhucheer/orange

文檔:https://www.kancloud.cn/chase688/orange_framework/1448035


免責聲明!

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



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