Golang 熱重啓

熱重啓

熱重啓(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, "") 來獲取該描述符。值得注意的是,子進程的 012 分別預留給標準輸入、標準輸出和錯誤輸出,因此父進程傳遞的 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

參考資料

相關文章
相關標籤/搜索