天天看點

Golang 如何實作熱重新開機

作者:寒笛過霜天

熱重新開機

熱重新開機(Zero Downtime), 指新老程序無縫切換, 在替換過程中可保持對 client 的服務。

原理

父程序監聽重新開機信号

在收到重新開機信号後, 父程序調用 fork, 同時傳遞 socket 描述符給子程序

子程序接收并監聽父程序傳遞的 socket 描述符

在子程序啟動成功之後, 父程序停止接收新連接配接, 同時等待舊連接配接處理完成(或逾時)

父程序退出, 熱重新開機完成

實作

> # mkdir hotRestart && cd hotRestart

初始化項目

> # go mod init server

注意: 項目的名稱不一定與子產品名稱(module) 一緻

> # vim main.go

package main
import (
"context"
"errors"
"flag"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
)
var (
server *http.Server
listener net.Listener = nil
graceful = flag.Bool("graceful", false, "listen on fd open 3 (internal use only)")
message = flag.String("message", "Hello World", "message to send")
)
func handler(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second)
w.Write([]byte(*message))
}
func main() {
var err error
// 解析參數
flag.Parse()
http.HandleFunc("/test", handler)
server = &http.Server{Addr: ":3000"}
// 設定監聽器的監聽對象(建立的或已存在的 socket 描述符)
if *graceful {
// 子程序監聽父程序傳遞的 socket 描述符
log.Println("listening on the existing file descriptor 3")
// 子程序的 0, 1, 2 是預留給标準輸入、标準輸出、錯誤輸出,故傳遞的 socket 描述符
// 應放在子程序的 3
f := os.NewFile(3, "")
listener, err = net.FileListener(f)
} else {
// 父程序監聽建立的 socket 描述符
log.Println("listening on a new file descriptor")
listener, err = net.Listen("tcp", server.Addr)
}
if err != nil {
log.Fatalf("listener error: %v", err)
}
go func() {
err = server.Serve(listener)
log.Printf("server.Serve err: %v\n", err)
}()
// 監聽信号
handleSignal()
log.Println("signal end")
}
func handleSignal() {
ch := make(chan os.Signal, 1)
// 監聽信号
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2)
for {
sig := <-ch
log.Printf("signal receive: %v\n", sig)
ctx, _ := context.WithTimeout(context.Background(), 20*time.Second)
switch sig {
case syscall.SIGINT, syscall.SIGTERM: // 終止程序執行
log.Println("shutdown")
signal.Stop(ch)
server.Shutdown(ctx)
log.Println("graceful shutdown")
return
case syscall.SIGUSR2: // 程序熱重新開機
log.Println("reload")
err := reload() // 執行熱重新開機函數
if err != nil {
log.Fatalf("graceful reload error: %v", err)
}
server.Shutdown(ctx)
log.Println("graceful reload")
return
}
}
}
func reload() error {
tl, ok := listener.(*net.TCPListener)
if !ok {
return errors.New("listener is not tcp listener")
}
// 擷取 socket 描述符
f, err := tl.File()
if err != nil {
return err
}
// 設定傳遞給子程序的參數(包含 socket 描述符)
args := []string{"-graceful"}
cmd := exec.Command(os.Args[0], args...)
// 程序轉入daemon的方法
cmd.Stdout = os.Stdout // 标準輸出
cmd.Stderr = os.Stderr // 錯誤輸出
cmd.ExtraFiles = []*os.File{f} // 檔案描述符
// 建立并執行子程序
return cmd.Start()
}           

編譯

> # go build

> # tree

.

├── go.mod

├── main.go

└── server

注意: 此時編譯的二進制執行檔案的名稱 由 [module] 決定

go mod init [module]

當然, 我們編譯時也可以指定名稱

go build [-o output] [build flags] [packages]

> # go version

go version go1.16.6 linux/amd64

測試

編譯上述程式為 server , 執行 ./server -message "Graceful Reload" , 通路 http://localhost:3000/test, 等待 5 秒後, 我們可以看到 Graceful Reload 的響應。

通過執行 kill -USR2 [PID] , 我們即可進行 Graceful Reload 的測試。

通過執行 kill -INT [PID] , 我們即可進行 Graceful Shutdown 的測試。

具體步驟:

> # ./server -message "Graceful Reload"

2021/09/13 18:06:40 listening on a new file descriptor

> # curl http://localhost:3000/test

Graceful Reload

> # ps -ef | grep server

root 6991 12565 0 18:06 pts/14 00:00:00 ./server -message Graceful Reload

> # kill -USR2 6991

> # ps -ef | grep server

root 30783 1 0 18:10 pts/14 00:00:00 ./server -graceful

再次通路

> # curl http://localhost:3000/test

Graceful Reload

我們此時發現 server 程序服務已經實作平滑重新開機

kill掉程序

> # kill -INT 30783