主要介紹服務優雅重啓的基本概念。git
查閱相關資料後,大概猜想出作法github
服務重啓時,舊進程並不直接中止,而是用舊進程fork一個新進程,同時舊進程的全部句柄都dup到新進程。這時新的請求都由新的進程處理,舊進程在處理完本身的任務後,自行退出。app
這只是大概流程,裏面還有許多細節須要考慮socket
https://github.com/facebookar...源碼分析
// facebookgo/grace/gracenet/net.go:206(省略非核心代碼) func (n *Net) StartProcess() (int, error) { listeners, err := n.activeListeners() // 複製socket句柄 files := make([]*os.File, len(listeners)) for i, l := range listeners { files[i], err = l.(filer).File() defer files[i].Close() } // 複製標準IO句柄 allFiles := append([]*os.File{os.Stdin, os.Stdout, os.Stderr}, files...) // 啓動新進程,並傳遞句柄 process, err := os.StartProcess(argv0, os.Args, &os.ProcAttr{ Dir: originalWD, Env: env, Files: allFiles, }) return process.Pid, nil }
這段代碼是啓動新進程的過程。學習
files
保存listeners
句柄(即socket句柄)allFiles
保存files
+stdout、stdin、stderr
句柄os.StartProcess
啓動新進程,並傳遞父進程句柄注:這裏傳遞的句柄只包括socket句柄與標準IO句柄。
舊進程退出須要確保當前的請求所有處理完成。同時再也不接收新的請求。命令行
回答這個問題須要提到socket流程
。code
一般創建socket須要經歷如下四步:server
一般,accept處於一個循環中,這樣就能持續處理請求。因此若不想接收新請求,只需退出循環,再也不accept便可。進程
回答這個問題,咱們須要給每個鏈接賦予一系列狀態。剛好,net/http
包幫咱們作好了這件事。
// GOROOT/net/http/server.go:2743 type ConnState int const ( // 新鏈接剛創建時 StateNew ConnState = iota // 鏈接處於活躍狀態,即正在處理的請求 StateActive // 鏈接處於空閒狀態,通常用於keep-alive StateIdle // 劫持狀態,能夠理解爲關閉狀態 StateHijacked // 關閉狀態 StateClosed )
經過狀態,咱們就能精確判斷全部請求是否處理完成。只要全部活躍(StateActive)的鏈接都成爲空閒(StateIdle)或者關閉(StateClosed)狀態。就能夠保證請求所有處理完成。
具體代碼
// facebookgo/httpdown/httpdown.go:347 func ListenAndServe(s *http.Server, hd *HTTP) error { // 監聽端口,提供服務 hs, err := hd.ListenAndServe(s) signals := make(chan os.Signal, 10) signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT) // 監聽信號量2和15(即kill -2 -15) select { case <-signals: signal.Stop(signals) // hs.Stop() 開始中止服務 if err := hs.Stop(); err != nil { return err } } }
這段代碼是啓動服務的入口代碼
syscall.SIGTERM
和syscall.SIGINT
,即通常終止進程的信號量能夠看出,服務退出的邏輯都在hs.Stop()
// facebookgo/httpdown/httpdown.go:293 func (s *server) Stop() error { s.stopOnce.Do(func() { // 禁止keep-alive s.server.SetKeepAlivesEnabled(false) // 關閉listener,再也不接收請求 closeErr := s.listener.Close() <-s.serveDone // 經過stop(一個chan),傳遞關閉信號 stopDone := make(chan struct{}) s.stop <- stopDone // 若在s.stopTimeout之內沒有結束,則強行kill全部鏈接。默認s.stopTimeout爲1min select { case <-stopDone: case <-s.clock.After(s.stopTimeout): // stop timed out, wait for kill killDone := make(chan struct{}) s.kill <- killDone } })}
Stop方法
那麼,等待全部請求處理完畢的邏輯,應該處於消費s.stop的地方。
這裏咱們注意到,最核心的結構體有這樣幾個屬性
// facebookgo/httpdown/httpdown.go:126 type server struct { ... new chan net.Conn active chan net.Conn idle chan net.Conn closed chan net.Conn stop chan chan struct{} kill chan chan struct{} ... }
stop和kill說過了,是用來傳遞中止和強行終止信號的。
其他new
、active
、idle
、closed
是用來記錄處於不一樣狀態的鏈接的。
咱們記錄了不一樣狀態的鏈接,那麼在關閉時,就能等鏈接處於「空閒「或」關閉「時再關閉它。
// facebookgo/httpdown/httpdown.go:233 case c := <-s.idle: conns[c] = http.StateIdle // 那些處於「活躍」的鏈接,會等到它轉爲「空閒」時,將其關閉 if stopDone != nil { c.Close() } case c := <-s.closed: // 全部鏈接關閉後,退出 if stopDone != nil && len(conns) == 0 { close(stopDone) return } case stopDone = <-s.stop: // 全部鏈接關閉後,退出 if len(conns) == 0 { close(stopDone) return } // 關閉全部「空閒」鏈接 for c, cs := range conns { if cs == http.StateIdle { c.Close() } }
這裏能夠看出,當接收到關閉信號時(stopDone = <-s.stop)
進程重啓主要就是如何退出、如何啓動。grace代碼量很少,以上敘述了核心的邏輯,有興趣的同窗能夠fork github源碼研讀。