golang優雅關閉與重啟
來源:互聯網
上載者:User
# golang程式優雅關閉與重啟# 何謂優雅當線上代碼有更新時,我們要首先關閉服務,然後再啟動服務,如果訪問量比較大,當關閉服務的時候,當前伺服器很有可能有很多串連,那麼如果此時直接關閉服務,這些串連將全部斷掉,影響使用者體驗,絕對稱不上優雅所以我們要想出一種可以平滑關閉或者重啟程式的方式是謂優雅。## 思路1. 服務端啟動時多開啟一個協程用來監聽關閉訊號2. 當協程接收到關閉訊號時,將拒絕接收新的串連,並處理好當前所有串連後斷開3. 啟動一個新的服務端進程來接管新的串連4. 關閉當前進程## 實現以 [siluser/bingo](https://github.com/silsuer/bingo)架構為例> 關於這個架構的系列文章: - [使用Go寫一個簡易的MVC的Web架構](https://studygolang.com/articles/12818) - [使用Go封裝一個便捷的ORM](https://studygolang.com/articles/12825) - [改造httprouter使其支援中介軟體](改造httprouter使其支援中介軟體) - [仿照laravel-artisan實現簡易go開發腳手架](https://studygolang.com/articles/14148) 我使用了[tim1020/godaemon](https://github.com/tim1020/godaemon)這個包來實現平滑重啟的功能(對於大部分項目來說,直接使用可以滿足大部分需求,無需改造)期望效果:在控制台輸入 `bingo run daemon [start|restart|stop]` 可以令伺服器 `啟動|重啟|停止`1. 先看如何開啟一個伺服器 (`bingo run dev`)關於 `bingo` 命令的實現可以看我以前的部落格: [仿照laravel-artisan實現簡易go開發腳手架](https://studygolang.com/articles/14148)因為是開發環境嘛,大體的思路就是吧 `bingo run`命令轉換成令 `go run start.go` 這種 `shell`命令所以 `bingo run dev`就等於 `go run start.go dev````go//處理http.Server,使支援graceful stop/restartfunc Graceful(s http.Server) error {// 設定一個環境變數os.Setenv("__GRACEFUL", "true")// 建立一個自訂的serversrv = &server{cm: newConnectionManager(),Server: s,}// 設定server的狀態srv.ConnState = func(conn net.Conn, state http.ConnState) {switch state {case http.StateNew:srv.cm.add(1)case http.StateActive:srv.cm.rmIdleConns(conn.LocalAddr().String())case http.StateIdle:srv.cm.addIdleConns(conn.LocalAddr().String(), conn)case http.StateHijacked, http.StateClosed:srv.cm.done()}}l, err := srv.getListener()if err == nil {err = srv.Server.Serve(l)} else {fmt.Println(err)}return err}```這樣就可以啟動一個伺服器,並且在串連狀態變化的時候可以監聽到2. 以守護進程啟動伺服器當使用 `bingo run daemon`或者 `bingo run daemon start`的時候,會觸發 `DaemonInit()`函數,內容如下:```gofunc DaemonInit() {// 得到存放pid檔案的路徑dir, _ := os.Getwd()pidFile = dir + "/" + Env.Get("PID_FILE")if os.Getenv("__Daemon") != "true" { //mastercmd := "start" //預設為startif l := len(os.Args); l > 2 {cmd = os.Args[l-1]}switch cmd {case "start":if isRunning() {fmt.Printf("\n %c[0;48;34m%s%c[0m", 0x1B, "["+strconv.Itoa(pidVal)+"] Bingo is running", 0x1B)} else { //fork daemon進程if err := forkDaemon(); err != nil {fmt.Println(err)}}case "restart": //重啟:if !isRunning() {fmt.Printf("\n %c[0;48;31m%s%c[0m", 0x1B, "[Warning]bingo not running", 0x1B)restart(pidVal)} else {fmt.Printf("\n %c[0;48;34m%s%c[0m", 0x1B, "["+strconv.Itoa(pidVal)+"] Bingo restart now", 0x1B)restart(pidVal)}case "stop": //停止if !isRunning() {fmt.Printf("\n %c[0;48;31m%s%c[0m", 0x1B, "[Warning]bingo not running", 0x1B)} else {syscall.Kill(pidVal, syscall.SIGTERM) //kill}case "-h":fmt.Println("Usage: " + appName + " start|restart|stop")default: //其它不識別的參數return //返回至調用方}//主進程退出os.Exit(0)}go handleSignals()}```首先要擷取`pidFile` 這個檔案主要是儲存令程式運行時候的進程`pid`,為什麼要持久化`pid`呢?是為了讓多次程式運行過程中,判定是否有相同程式啟動等操作之後要擷取對應的操作 (start|restart|stop),一個一個說> case `start`:首先使用 `isRunning()`方法判斷當前程式是否在運行,如何判斷?就是從上面提到的 `pidFile` 中取出進程號然後判斷當前系統是否運行令這個進程,如果有,證明正在運行,返回 `true`,反之返回 `false`如果沒有啟動並執行話,調用 `forkDaemon()` 函數啟動程式,這個函數是整個功能的核心```gofunc forkDaemon() error {args := os.Argsos.Setenv("__Daemon", "true")procAttr := &syscall.ProcAttr{Env: os.Environ(),Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},}pid, err := syscall.ForkExec(args[0], []string{args[0], "dev"}, procAttr)if err != nil {panic(err)}savePid(pid)fmt.Printf("\n %c[0;48;32m%s%c[0m", 0x1B, "["+strconv.Itoa(pid)+"] Bingo running...", 0x1B)fmt.Println()return nil}````syscall`包不支援win系統,也就意味著如果想在 `windows`上做開發的話,只能使用虛擬機器或者 `docker`啦這裡的主要功能就是,使用 `syscall.ForkExec()`,`fork` 一個進程出來運行這個進程所執行的命令就是這裡的參數(因為我們的原始命令是 `go run start.go dev`,所以這裡的`args[0]`實際上是 `start.go`編譯之後的二進位檔案)然後再把 `fork`出來的進程號儲存在 `pidFile`裡所以最終啟動並執行效果就是我們第一步時候說到的 `bingo run dev` 達到的效果> case `restart`:這個比較簡單,通過 `pidFile`判定程式是否正在運行,如果正在運行,才會繼續向下執行函數體也比較簡單,只有兩行```gosyscall.Kill(pid, syscall.SIGHUP) //kill -HUP, daemon only時,會直接退出forkDaemon()```第一行殺死這個進程第二行開啟一個新進程> case `stop`:這裡就一行代碼,就是殺死這個進程## 額外的想法在開發過程中,每當有一丁點變動(比如更改來一丁點控制器),就需要再次執行一次 `bingo run daemon restart` 命令,讓新的改動生效,十分麻煩所以我又開發了 `bingo run watch` 命令,監聽改動,自動重啟server伺服器我使用了[github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify)包來實現監聽```gofunc startWatchServer(port string, handler http.Handler) {// 監聽目錄變化,如果有變化,重啟服務// 守護進程開啟服務,主進程阻塞不斷掃描目前的目錄,有任何更新,向守護進程傳遞訊號,守護進程重啟服務// 開啟一個協程運行服務// 監聽目錄變化,有變化運行 bingo run daemon restartf, err := fsnotify.NewWatcher()if err != nil {panic(err)}defer f.Close()dir, _ := os.Getwd()wdDir = dirfileWatcher = ff.Add(dir)done := make(chan bool)go func() {procAttr := &syscall.ProcAttr{Env: os.Environ(),Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},}_, err := syscall.ForkExec(os.Args[0], []string{os.Args[0], "daemon", "start"}, procAttr)if err != nil {fmt.Println(err)}}()go func() {for {select {case ev := <-f.Events:if ev.Op&fsnotify.Create == fsnotify.Create {fmt.Printf("\n %c[0;48;33m%s%c[0m", 0x1B, "["+time.Now().Format("2006-01-02 15:04:05")+"]created file:"+ev.Name, 0x1B)}if ev.Op&fsnotify.Remove == fsnotify.Remove {fmt.Printf("\n %c[0;48;31m%s%c[0m", 0x1B, "["+time.Now().Format("2006-01-02 15:04:05")+"]deleted file:"+ev.Name, 0x1B)}if ev.Op&fsnotify.Rename == fsnotify.Rename {fmt.Printf("\n %c[0;48;34m%s%c[0m", 0x1B, "["+time.Now().Format("2006-01-02 15:04:05")+"]renamed file:"+ev.Name, 0x1B)} else {fmt.Printf("\n %c[0;48;32m%s%c[0m", 0x1B, "["+time.Now().Format("2006-01-02 15:04:05")+"]modified file:"+ev.Name, 0x1B)}// 有變化,放入重啟數組中restartSlice = append(restartSlice, 1)case err := <-f.Errors:fmt.Println("error:", err)}}}()// 準備重啟守護進程go restartDaemonServer()<-done}```首先按照 `fsnotify`的文檔,建立一個 `watcher`,然後添加監聽目錄(這裡只是監聽目錄下的檔案,不能監聽子目錄)然後開啟兩個協程:1. 監聽檔案變化,如果有檔案變化,把變化的個數寫入一個 `slice` 裡,這是一個阻塞的 `for`迴圈2. 每隔1s中查看一次記錄檔案變化的 `slice`, 如果有的話,就重啟伺服器,並重新設定監聽目錄,然後清空 `slice` ,否則跳過 遞迴遍曆子目錄,達到監聽整個工程目錄的效果:```gofunc listeningWatcherDir(dir string) {filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {dir, _ := os.Getwd()pidFile = dir + "/" + Env.Get("PID_FILE")fileWatcher.Add(path)// 這裡不能監聽 pidFile,否則每次重啟都會導致pidFile有更新,會不斷的觸發重啟功能fileWatcher.Remove(pidFile)return nil})}```這裡這個 `slice` 的作用也就是為了避免當一次儲存更新了多個檔案的時候,也重啟了多次伺服器下面看看重啟伺服器的代碼:```gogo func() {// 執行重啟命令cmd := exec.Command("bingo", "run", "daemon", "restart")stdout, err := cmd.StdoutPipe()if err != nil {fmt.Println(err)}defer stdout.Close()if err := cmd.Start(); err != nil {panic(err)}reader := bufio.NewReader(stdout)//即時迴圈讀取輸出資料流中的一行內容for {line, err2 := reader.ReadString('\n')if err2 != nil || io.EOF == err2 {break}fmt.Print(line)}if err := cmd.Wait(); err != nil {fmt.Println(err)}opBytes, _ := ioutil.ReadAll(stdout)fmt.Print(string(opBytes))}()```使用 `exec.Command()` 方法得到一個 `cmd`調用 `cmd.Stdoutput()` 得到一個輸出管道,命令列印出來的資料都會從這個管道流出來然後使用 `reader := bufio.NewReader(stdout)` 從管道中讀出資料用一個阻塞的`for`迴圈,不斷的從管道中讀出資料,以 `\n` 為一行,一行一行的讀並列印在控制台裡,達到輸出的效果,如果這幾行不寫的話,在新的進程裡的 `fmt.Println()`方法列印出來的資料將無法顯示在控制台上.就醬,最後貼下項目連結 [silsuer/bingo](https://github.com/silsuer/bingo) ,歡迎star,歡迎PR,歡迎提意見152 次點擊