golang程序優雅關閉與重啓

golang程序優雅關閉與重啓

何謂優雅

當線上代碼有更新時,咱們要首先關閉服務,而後再啓動服務,若是訪問量比較大,當關閉服務的時候,當前服務器頗有可能有不少 鏈接,那麼若是此時直接關閉服務,這些鏈接將所有斷掉,影響用戶體驗,絕對稱不上優雅laravel

因此咱們要想出一種能夠平滑關閉或者重啓程序的方式git

是謂優雅。github

思路

  1. 服務端啓動時多開啓一個協程用來監聽關閉信號
  2. 當協程接收到關閉信號時,將拒絕接收新的鏈接,並處理好當前全部鏈接後斷開
  3. 啓動一個新的服務端進程來接管新的鏈接
  4. 關閉當前進程

實現

siluser/bingo框架爲例golang

關於這個框架的系列文章:docker

我使用了tim1020/godaemon這個包來實現平滑重啓的功能(對於大部分項目來講,直接使用能夠知足大部分需求,無需改造)shell

指望效果:windows

在控制檯輸入 bingo run daemon [start|restart|stop] 能夠令服務器 啓動|重啓|中止數組

  1. 先看如何開啓一個服務器 (bingo run dev)

關於 bingo 命令的實現能夠看我之前的博客: 仿照laravel-artisan實現簡易go開發腳手架服務器

由於是開發環境嘛,大致的思路就是吧 bingo run命令轉換成令 go run start.go 這種 shell命令app

因此 bingo run dev就等於 go run start.go dev

//處理http.Server,使支持graceful stop/restart
func Graceful(s http.Server) error {
	// 設置一個環境變量
	os.Setenv("__GRACEFUL", "true")
	// 建立一個自定義的server
	srv = &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
}
複製代碼

這樣就能夠啓動一個服務器,而且在鏈接狀態變化的時候能夠監聽到

  1. 以守護進程啓動服務器

當使用 bingo run daemon或者 bingo run daemon start的時候,會觸發 DaemonInit()函數,內容以下:

func DaemonInit() {
	// 獲得存放pid文件的路徑
	dir, _ := os.Getwd()
	pidFile = dir + "/" + Env.Get("PID_FILE")
	if os.Getenv("__Daemon") != "true" { //master
		cmd := "start" //缺省爲start
		if 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() 函數啓動程序,這個函數是整個功能的核心

func forkDaemon() error {
	args := os.Args
	os.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斷定程序是否正在運行,若是正在運行,纔會繼續向下執行

函數體也比較簡單,只有兩行

syscall.Kill(pid, syscall.SIGHUP) //kill -HUP, daemon only時,會直接退出
forkDaemon()
複製代碼

第一行殺死這個進程 第二行開啓一個新進程

case stop:

這裏就一行代碼,就是殺死這個進程

額外的想法

在開發過程當中,每當有一丁點變更(好比更改來一丁點控制器),就須要再次執行一次 bingo run daemon restart 命令,讓新的改動生效,十分麻煩

因此我又開發了 bingo run watch 命令,監聽改動,自動重啓server服務器

我使用了github.com/fsnotify/fs…包來實現監聽

func startWatchServer(port string, handler http.Handler) {
	// 監聽目錄變化,若是有變化,重啓服務
	// 守護進程開啓服務,主進程阻塞不斷掃描當前目錄,有任何更新,向守護進程傳遞信號,守護進程重啓服務
	// 開啓一個協程運行服務
	// 監聽目錄變化,有變化運行 bingo run daemon restart
	f, err := fsnotify.NewWatcher()
	if err != nil {
		panic(err)
	}
	defer f.Close()
	dir, _ := os.Getwd()
	wdDir = dir
	fileWatcher = f
	f.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 ,不然跳過

    遞歸遍歷子目錄,達到監聽整個工程目錄的效果:

func 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 的做用也就是爲了不當一次保存更新了多個文件的時候,也重啓了屢次服務器

下面看看重啓服務器的代碼:

go 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 ,歡迎star,歡迎PR,歡迎提意見

相關文章
相關標籤/搜索