Go 使用 tableflip 實現應用的優雅熱升級

在日常研發過程中,我們負責的 web 應用常常會因發佈過程中的服務重啓而出現短時間的服務不可用或大量請求報錯。隨着互聯網行業研發模式的逐漸敏捷和迭代週期的不斷縮短,應用升級導致的服務抖動對系統穩定性的影響已不可忽視。在應用中集成 tableflip 或許可以緩解大家在新功能上線時的擔憂。

tableflip 是 Cloudflare 針對 golang 進程實現優雅重啓而設計的一套開源類庫,集成 tableflip 可以讓我們的 go 應用獲得與 nginx reload 一樣強大的熱更新能力。如果你的應用尚未接入負載均衡與滾動發佈,或者你的應用本身就是需要特殊處理的有狀態應用,趕快試試 tableflip 吧!

tableflip 簡介

tableflip 的設計宗旨就是實現類似 nginx 的優雅熱更新能力,包括:

tableflip 中的核心類型是 Upgrader,調用 Upgrader.Upgrade 會產生一個繼承必要的 net.Listeners 的新進程,並等待新進程發出表明其已成功完成初始化、退出或超時的信號。如果當前已有升級的任務在執行,則直接返回相應的錯誤。

當新進程啓動成功後,調用 Upgrader.Ready 會清除無效的 fd 並向父進程發出初始化成功完成的信號,然後父進程就可以安心退出。至此,我們就完成了一次優雅的進程重啓。

tableflip 狀態流轉圖

注:tableflip 目前只適用於 Linux 和 macOS

tableflip 應用舉例

接下來我們設計一個集成 tableflip 的簡單 http server,完整代碼如下:

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/cloudflare/tableflip"
)

// 當前程序的版本
const version = "v0.0.1"

func main() {
    upg, err := tableflip.New(tableflip.Options{})
    if err != nil {
        panic(err)
    }
    defer upg.Stop()

    // 爲了演示方便,爲程序啓動強行加入 1s 的延時,並在日誌中附上進程 pid
    time.Sleep(time.Second)
    log.SetPrefix(fmt.Sprintf("[PID: %d] ", os.Getpid()))

    // 監聽系統的 SIGHUP 信號,以此信號觸發進程重啓
    go func() {
        sig := make(chan os.Signal, 1)
        signal.Notify(sig, syscall.SIGHUP)
        for range sig {
            // 核心的 Upgrade 調用
            err := upg.Upgrade()
            if err != nil {
                log.Println("Upgrade failed:", err)
            }
        }
    }()

    // 注意必須使用 upg.Listen 對端口進行監聽
    ln, err := upg.Listen("tcp"":8080")
    if err != nil {
        log.Fatalln("Can't listen:", err)
    }

    // 創建一個簡單的 http server,/version 返回當前的程序版本
    mux := http.NewServeMux()
    mux.HandleFunc("/version", func(rw http.ResponseWriter, r *http.Request) {
        log.Println(version)
        rw.Write([]byte(version + "\n"))
    })
    server := http.Server{
        Handler: mux,
    }

    // 照常啓動 http server
    go func() {
        err := server.Serve(ln)
        if err != http.ErrServerClosed {
            log.Println("HTTP server:", err)
        }
    }()

    if err := upg.Ready(); err != nil {
        panic(err)
    }
    <-upg.Exit()

    // 給老進程的退出設置一個 30s 的超時時間,保證老進程的退出
    time.AfterFunc(30*time.Second, func() {
        log.Println("Graceful shutdown timed out")
        os.Exit(1)
    })

    // 等待 http server 的優雅退出
    server.Shutdown(context.Background())
}

上面的代碼實現了一個返回當前 version 的 http server,我們還在啓動過程中插入了 1s 的延時來拉長進程的初始化時間,以觀察升級過程中服務是否依舊可用。

編譯並運行之:

go build -o demo main.go
./demo

使用 curl 模擬一些客戶端請求(10 qps):

while true; do curl http://localhost:8080/version; sleep 0.1; done
...
[PID: 18939] 2021/07/04 15:02:47 v0.0.1
[PID: 18939] 2021/07/04 15:02:47 v0.0.1
[PID: 18939] 2021/07/04 15:02:47 v0.0.1
[PID: 18939] 2021/07/04 15:02:48 v0.0.1
...

然後,我們對應用進行了一些升級,將版本號修改爲 v0.0.2,並重新編譯程序:

go build -o demo main.go

最後,來試試優雅的熱重啓是否奏效吧!

kill -s HUP 18939
...
[PID: 19306] 2021/07/04 15:04:57 v0.0.2
[PID: 19306] 2021/07/04 15:04:57 v0.0.2
[PID: 19306] 2021/07/04 15:04:57 v0.0.2
[PID: 19306] 2021/07/04 15:04:57 v0.0.2
...

可見,客戶端完全不會受服務端的升級和重啓的影響,我們的應用實現了優雅升級!

...
v0.0.1
v0.0.1
v0.0.2
v0.0.2
v0.0.2
...

總結

tableflip 是實現 go 進程優雅重啓的優秀工具。因爲其支持對連接層進行保持和綁定,所以幾乎適用於所有的 web 框架(HTTP、gRPC 等)。通過簡單的配置,集成 tableflip 的程序也可以非常方便地被 systemd 等工具進行管控。

參考資料

還想了解更多嗎?

更多請查看:https://github.com/cloudflare/tableflip

歡迎加入我們 GOLANG 中國社區:https://gocn.vip/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/9KrdW1k_lAJTN0l_8i-a-A