Golang HTTP 服務平滑重啟及升級的思路
Golang HTTP服務在上線時,需要重新編譯可執(zhí)行文件,關(guān)閉正在運行的進程,然后再啟動新的運行進程。對于訪問頻率比較高的面向終端用戶的產(chǎn)品,關(guān)閉、重啟的過程中會出現(xiàn)無法訪問(nginx表現(xiàn)為502)的情況,影響終端用戶的使用體驗。
實現(xiàn)的一般思路
- 一般情況下,要實現(xiàn)平滑重啟或升級,需要執(zhí)行以下幾個步驟:
- 發(fā)布新的bin文件覆蓋老的bin文件
- 發(fā)送一個信號量(USR2),告訴正在運行的進程,進行重啟
- 正在運行的進程接受到信號后,以子進程的方式啟動新的bin文件
- 新進程接收并處理新的請求
- 老進程不再接收新請求,等待所有正在處理的請求處理完成后自動退出
- 新進程在老進程退出后,繼續(xù)提供服務
選型與實踐
重復造平滑重啟及升級的輪子比較簡單,但測試覆蓋無法控制,比較耗時耗力。所以秉著不重復造輪子的思路,使用github中的三方庫進行選擇:
- facebookgo/grace
- fvbock/endless
- jpillora/overseer
endless與grace的實現(xiàn)方式原理都比較類似,所以在選型初期我們以facebookgo/grace
庫為例集成到項目中進行測試:
func (h *Server) ListenAndServe(listenAddress string) error { // .... return gracehttp.Serve(&http.Server{ Addr: listenAddress, Handler: h.httpServerMux, }) }
使用ab
工具壓測 api-publish
服務進行測試,服務啟動后,執(zhí)行以下命令:
ab -c 10 -n 2000 http://127.0.0.1:38272/api/list
然后給進程發(fā)送USR2
信號 kill -USR2 api-server-pid
,可看到以下結(jié)果:
結(jié)果中 Failed requests
表示在整個壓測請求中沒有錯誤的請求,這可以說明服務重啟時沒有中斷請求的接收和處理。如果使用sleep的方式測試,可以明顯的看到新進程替代老進程的過程。
supervisor的問題
實際項目中,線上服務是被supervisor啟動的。如上所說的我們?nèi)绻ㄟ^grace或者endless的子進程啟動后退出父進程這種方式的話,存在的問題就是子進程會被1號進程接管,導致supervisor認為服務掛掉重啟服務,為了避免這種問題我們需要使用master-worker的方式。
overseer
這個備選庫實現(xiàn)了master-worker的方式。簡單集成方式:
return overseer.RunErr(overseer.Config{ Address: address, Program: func(state overseer.State) { // ... http.Serve(state.Listener, nil) }, })
另外:在更新supervisor時,配置不需要更新,但重啟服務的命令不能使用supervisor restart
,需要使用supervisor signal sigusr2 api
的命令。
還是使用上面的測試方式:
可以明顯的看到,supervisor發(fā)送了USR2信號后,主進程的pid沒有變化,重新啟動了一個新的子進程來處理線上請求。
其他的問題
在使用overseer集成到項目中測試時,子進程的運行函數(shù)中僅僅加入了http服務的啟動,這樣導致一個問題。
main函數(shù)中任務會被執(zhí)行兩次,如果是cron的初始化,那么cron就會初始化兩次,導致有兩個cron在執(zhí)行,這樣的方式是不符合預期的。
導致這樣的原因是:overseer在啟動子進程時是使用和主進程一樣的啟動命令。所以main函數(shù)會執(zhí)行兩次。
func (mp *master) fork() error { mp.debugf("starting %s", mp.binPath) cmd := exec.Command(mp.binPath) //mark this new process as the "active" slave process. //this process is assumed to be holding the socket files. mp.slaveCmd = cmd mp.slaveID++ //provide the slave process with some state e := os.Environ() e = append(e, envBinID+"="+hex.EncodeToString(mp.binHash)) e = append(e, envBinPath+"="+mp.binPath) e = append(e, envSlaveID+"="+strconv.Itoa(mp.slaveID)) e = append(e, envIsSlave+"=1") e = append(e, envNumFDs+"="+strconv.Itoa(len(mp.slaveExtraFiles))) cmd.Env = e //inherit master args/stdfiles cmd.Args = os.Args cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr //include socket files cmd.ExtraFiles = mp.slaveExtraFiles if err := cmd.Start(); err != nil { return fmt.Errorf("Failed to start slave process: %s", err) } // ... }
我們通過調(diào)整main函數(shù)的內(nèi)容來解決這個問題:
- 將之前所有的初始化內(nèi)容集成在initialization函數(shù)中
- 將http初始化的內(nèi)容集成在httpServer函數(shù)中,返回一個http.Server
func main() { // 配置初始化 if err := config.Init(appConf); err != nil { fmt.Println(err) return } cfg := config.GetConfig() // 初始化graceful http服務 gracefulHTTPServer := microsvr.GracefulHTTPServer{ Address: cfg.HTTPListenAddress, Conf: cfg, Initialization: initialization, HttpServer: httpServer, } // 啟動 if err := gracefulHTTPServer.Run(); err != nil { fmt.Println(err) return } } // 初始化日志、數(shù)據(jù)庫鏈接、定時任務等 func initialization(cfg *config.Conf) { if err := microsvr.Init(cfg); err != nil { fmt.Println(err) return } if err := server.AddConnect(cfg.Databases.String()); err != nil { fmt.Println(err) return } logger.Info("數(shù)據(jù)庫鏈接成功:" + cfg.Databases.Address) // cron cron.Cron.Init() } // 初始化http服務,但不啟動 func httpServer() *http.Server { server := microsvr.NewHTTPServer() server.SetAllowOrginBack() Routers(server) return server }
實踐對比結(jié)果:
- grace與endless:舊的api都不會斷掉,會執(zhí)行原來的邏輯,但pid會變化;不支持supervisor管理
- overseer:舊api不會斷掉,會執(zhí)行原來的邏輯,主進程pid也不會變化,支持supervisor、systemd等管理
grace與endless的原理比較相像,都是類似上述的一般思路的實現(xiàn)原理。overseer的不同,主要有兩點:
- 添加了fetcher:用來支持自動升級bin文件,fetcher運行在一個goroutine中,通過預先設置好的間隔時間來檢查bin文件;支持File、Github、S3的方式
- 添加了主進程管理平滑重啟:子進程處理鏈接,能夠保持主進程pid不變
我們使用了overseer作為最終的選型結(jié)果。
總結(jié)
到此這篇關(guān)于Golang HTTP 服務平滑重啟及升級的思路的文章就介紹到這了,更多相關(guān)golang http 平滑重啟內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang跳轉(zhuǎn)語句continue與goto使用語法詳解
這篇文章主要介紹了Golang跳轉(zhuǎn)語句continue與goto使用語法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習吧2023-01-01關(guān)于Golang中range指針數(shù)據(jù)的坑詳解
這篇文章主要給大家介紹了關(guān)于Golang中range指針數(shù)據(jù)的坑的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-02-02