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

bupt_xingxin發表於2021-07-04

推薦 tableflip 的背景

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

tableflipCloudflare 針對 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 等工具進行管控。

參考資料


歡迎加入 GOLANG 中國社群:https://gocn.vip

更多原創文章乾貨分享,請關注公眾號
  • 使用 tableflip 實現應用的優雅熱升級
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章