平滑重啓是指能讓咱們的程序在重啓的過程不中斷服務,新老進程無縫銜接,實現零停機時間(Zero-Downtime)部署;html
平滑重啓是創建在優雅退出的基礎之上的,以前一篇文章介紹了相關實現:Golang中使用Shutdown特性對http服務進行優雅退出使用總結git
目前實現平滑重啓的主要策略有兩種:github
方案一:咱們的服務若是是多機器部署,能夠經過網關程序,將即將重啓服務的機器從網關下線,重啓完成後再從新上線,該方案適合多機器部署的企業級應用;golang
方案二:讓咱們的程序實現自啓動,重啓子進程來實現平滑重啓,核心策略是經過拷貝文件描述符實現子進程和父進程切換,適合單機器部署應用;apache
今天咱們就主要介紹方案二,讓咱們的程序擁有平滑重啓的功能,相關實現參考一個開源庫:https://github.com/fvbock/endless瀏覽器
實現原理介紹併發
http 鏈接介紹:框架
咱們知道,http 服務也是基於 tcp 鏈接,咱們經過 golang http 包源碼也能看到底層是經過監聽 tcp 鏈接實現的;less
func (srv *Server) ListenAndServe() error { if srv.shuttingDown() { return ErrServerClosed } addr := srv.Addr if addr == "" { addr = ":http" } ln, err := net.Listen("tcp", addr) if err != nil { return err } return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}) }
複用 socket:socket
當程序開啓 tcp 鏈接監聽時會建立一個 socket 並返回一個文件描述符 handler 給咱們的程序;
經過拷貝文件描述符文件可使 socket 不關閉繼續使用原有的端口,天然 http 鏈接也不會斷開,啓動一個相同的進程也不會出現端口被佔用的問題;
經過以下代碼進行測試:
package main import ( "fmt" "net/http" "context" "time" "os" "os/signal" "syscall" "net" "flag" "os/exec" ) var ( graceful = flag.Bool("grace", false, "graceful restart flag") procType = "" ) func main() { flag.Parse() mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, fmt.Sprintf("Hello world! ===> %s", procType)) }) server := &http.Server{ Addr: ":8080", Handler: mux, } var err error var listener net.Listener if *graceful { f := os.NewFile(3, "") listener, err = net.FileListener(f) procType = "fork process" } else { listener, _ = net.Listen("tcp", server.Addr) procType = "main process" //主程序開啓5s 後 fork 子進程 go func() { time.Sleep(5*time.Second) forkSocket(listener.(*net.TCPListener)) }() } err=server.Serve(listener.(*net.TCPListener)) fmt.Println(fmt.Sprintf("proc exit %v", err)) } func forkSocket(tcpListener *net.TCPListener) error { f, err := tcpListener.File() if err != nil { return err } args := []string{"-grace"} fmt.Println(os.Args[0], args) cmd := exec.Command(os.Args[0], args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr // put socket FD at the first entry cmd.ExtraFiles = []*os.File{f} return cmd.Start() }
該程序啓動後,等待 5s 會自動 fork 子進程,經過 ps 命令查看如圖能夠看到有兩個進程同時共存:
而後咱們能夠經過瀏覽器訪問 http://127.0.0.1/ 能夠看到會隨機顯示主進程或子進程的輸出;
寫一個測試代碼進行循環請求:
package main import ( "net/http" "io/ioutil" "fmt" "sync" ) func main(){ wg:=sync.WaitGroup{} wg.Add(100) for i:=0; i<100; i++ { go func(index int) { result:=getUrl(fmt.Sprintf("http://127.0.0.1:8080?%d", i)) fmt.Println(fmt.Sprintf("loop:%d %s", index, result)) wg.Done() }(i) } wg.Wait() } func getUrl(url string) string{ resp, _ := http.Get(url) defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) return string(body) }
能看到返回的數據也是有些是主進程有些是子進程。
切換過程:
在開啓新的進程和老進程退出的瞬間,會有一個短暫的瞬間是同時有兩個進程使用同一個文件描述符,此時這種狀態,經過http請求訪問,會隨機請求到新進程或老進程上,這樣也沒有問題,由於請求不是在新進程上就是在老進程上;當老進程結束後請求就會所有到新進程上進行處理,經過這種方式便可實現平滑重啓;
綜上,咱們能夠將核心的實現總結以下:
1.監聽退出信號;
2.監聽到信號後 fork 子進程,使用相關的命令啓動程序,將文件描述符傳遞給子進程;
3.子進程啓動後,父進程中止服務並處理正在執行的任務(或超時)退出;
4.此時只有一個新的進程在運行,實現平滑重啓。
一個完整的 demo 代碼,經過發送 USR1 信號,程序會自動建立子進程並關閉主進程,實現平滑重啓:
package main import ( "fmt" "net/http" "context" "os" "os/signal" "syscall" "net" "flag" "os/exec" ) var ( graceful = flag.Bool("grace", false, "graceful restart flag") ) func main() { flag.Parse() mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello world!") }) server := &http.Server{ Addr: ":8080", Handler: mux, } var err error var listener net.Listener if *graceful { f := os.NewFile(3, "") listener, err = net.FileListener(f) } else { listener, err = net.Listen("tcp", server.Addr) } if err != nil{ fmt.Println(fmt.Sprintf("listener error %v", err)) return } go listenSignal(context.Background(), server, listener) err=server.Serve(listener.(*net.TCPListener)) fmt.Println(fmt.Sprintf("proc exit %v", err)) } func forkSocket(tcpListener *net.TCPListener) error { f, err := tcpListener.File() if err != nil { return err } args := []string{"-grace"} fmt.Println(os.Args[0], args) cmd := exec.Command(os.Args[0], args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr // put socket FD at the first entry cmd.ExtraFiles = []*os.File{f} return cmd.Start() } func listenSignal(ctx context.Context, httpSrv *http.Server, listener net.Listener) { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.USR1) select { case <-sigs: forkSocket(listener.(*net.TCPListener)) httpSrv.Shutdown(ctx) fmt.Println("http shutdown") } }
使用 apache 的 ab 壓測工具進行驗證一下,執行 ab -c 50 -t 20 http://127.0.0.1:8080/ 持續 50 的併發 20s,在壓測的期間向程序運行的pid發送 USR1 信號,能夠看到壓測結果,沒有失敗的請求,由此可知,該方案實現平滑重啓是木有問題的。
最後給你們安利一個 Web 開發框架,該框架已經將平滑重啓進行的封裝,開箱即用,快速構建一個帶平滑重啓的 Web 服務。
框架源碼:https://gitee.com/zhucheer/orange
文檔:https://www.kancloud.cn/chase688/orange_framework/1448035