熱重啓(Zero Downtime),指新老進程無縫切換,在替換過程當中可保持對 client 的服務。html
fork
,同時傳遞 socket
描述符給子進程socket
描述符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...)
cmd.Stdout = os.Stdout // 標準輸出
cmd.Stderr = os.Stderr // 錯誤輸出
cmd.ExtraFiles = []*os.File{f} // 文件描述符
// 新建並執行子進程
return cmd.Start()
}
複製代碼
咱們在父進程執行 cmd.ExtraFiles = []*os.File{f}
來傳遞 socket
描述符給子進程,子進程經過執行 f := os.NewFile(3, "")
來獲取該描述符。值得注意的是,子進程的 0
、1
和 2
分別預留給標準輸入、標準輸出和錯誤輸出,因此父進程傳遞的 socket
描述符在子進程的順序是從 3
開始。segmentfault
編譯上述程序爲 main
,執行 ./main -message "Graceful Reload"
,訪問 http://localhost:3000/test
,等待 5 秒後,咱們能夠看到 Graceful Reload
的響應。服務器
經過執行 kill -USR2 [PID]
,咱們便可進行 Graceful Reload
的測試。socket
經過執行 kill -INT [PID]
,咱們便可進行 Graceful Shutdown
的測試。tcp