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
-
https://blog.cloudflare.com/graceful-upgrades-in-go/
-
https://github.com/fvbock/endless
-
https://pkg.go.dev/github.com/astaxie/beego/grace
-
https://grisha.org/blog/2014/06/03/graceful-restart-in-golang/
還想了解更多嗎?
更多請查看:https://github.com/cloudflare/tableflip
歡迎加入我們 GOLANG 中國社區:https://gocn.vip/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/9KrdW1k_lAJTN0l_8i-a-A