熱重啓
熱重啓(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