go程序不停機重啟

  • 2021 年 11 月 23 日
  • 筆記

讓我們給http服務寫一個版本更新接口,讓它自動更新版本並重啟服務吧。

 

初步例子

  註:為了精簡,文中代碼都去除了err處理  

main.go

var Version = "1.0"

/* 打印版本 */
func version(w http.ResponseWriter, r *http.Request) {
	msg := fmt.Sprintf("version %v\n", Version)
	w.Write([]byte(msg))
}

/* 版本升級 */
func upgrade(w http.ResponseWriter, r *http.Request) {
	// 1. 把新版本文件放置到服務主目錄(簡化)
	os.Remove("test_restart")
	os.Rename("new_test_restart", "test_restart")

	// 2. go的可執行文件加權限(省略)

	// 3. 重啟服務
	cmd := exec.Command("/bin/bash", "-c", "./restart.sh")
	cmd.Output()

	w.Write([]byte("restart ok\n"))
}

func main() {
	// 記錄pid
	f,_ := os.Create("s.pid")
	pid := os.Getpid()
	f.WriteString(fmt.Sprintf("%v", pid))
	fmt.Printf("System running:%v\n", pid)

	// 監聽連接
	mux := http.NewServeMux()
	mux.HandleFunc("/version", version)
	mux.HandleFunc("/upgrade", upgrade)
	http.ListenAndServe("127.0.0.1:9527", mux)
}

restart.sh(重啟腳本)

kill -9 $(cat s.pid)
nohup ./test_restart > nohup.log 2>&1 &

 

 測試

    1. 編譯後開始運行

     

    2. 請求一下版本信息接口

   

    3. 將代碼中Version改為「1.1」, 生成一個新文件「new_test_restart」

    4. 請求版本更新接口

   

   

發現問題

    重啟腳本是stop後start的,stop時直接殺死了進程,程序直接中斷了當前所有的連接,此時接口函數還未return,導致調用方接收不到響應。

 

使用Endless

    看來得不中斷已有連接的情況下進行重啟才行,不能簡單的stop後start,得平滑重啟。大致就是讓父進程啟動一個子進程去監聽新的連接,自己不再監聽新的連接,而是在處理完已有連接後終止,之後子進程獨挑大樑。

    隨後發現github上的endless挺滿足需求,它是一個不停機重啟的服務器實現,實現流程為:

  1. 監聽 SIGHUP 信號
  2. 收到信號後 fork 子進程(使用相同的啟動命令),將服務監聽的 socket 文件描述符傳遞給子進程
  3. 子進程啟動成功後開始監聽新的連接,並發送 SIGTERM 信號給父進程
  4. 父進程收到 SIGTERM 信號後停止接收新的連接,等待舊連接處理完成後終止
  5. 父進程終止,重啟完成

    關於 SIGHUP 信號,我們可以用「kill -1」命令發送給endless。

    使用endless改動很小,在main函數中只需要把 http.ListenAndServe 修改為 endless.ListenAndServe即可: 

main.go

func main() {
	// 記錄pid(省略)

	// 監聽連接
	mux := http.NewServeMux()
	mux.HandleFunc("/version", version)
	mux.HandleFunc("/upgrade", upgrade)
        // http.ListenAndServe("127.0.0.1:9527", mux)
	endless.ListenAndServe("127.0.0.1:9527", mux)
}

restart.sh

kill -1 $(cat s.pid)

 

再測試

   1. 編譯後開始運行(帶時間前綴的是endless打印的日誌)

   

    2. 請求一下版本信息接口

   

    3. 將代碼中Version改為「1.1」, 生成一個新文件「new_test_restart」

    4. 請求版本更新接口

   

   

        請求沒有被中斷,成功接收了響應。

        endless的日誌比較清晰,77625的父進程接收 SIGHUP 後,fork了子進程77625,接收到子進程傳遞的 SIGHUP 後,等待已有連接處理完成後終止,完全符合上述介紹的流程。

    5. 驗證版本

   

    至此不停機版本更新成功 ٩(◕‿◕。)۶