【Gin-API系列】守護程式和平滑重啟(八)

RunningPower發表於2020-09-08

生產環境的API服務我們都會部署在Linux伺服器上,為了不受終端狀態的影響,啟動服務的時候會讓服務在後臺執行。那麼如何讓服務在後臺執行呢,目前有2種常見的方法。

1、nohub 執行

表示忽略SIGHUP(結束通話)訊號,終端退出的時候所發起的結束通話訊號會被忽略。nohup一般會結合&引數執行程式,&表示將程式設定為後臺執行的程式。兩者結合就變成了啟動一個不受終端狀態影響的後臺服務。

nohup gin-ips >> gin-api.out 2>&1 &

2、守護程式

  • 理解守護程式

守護程式是一個在後臺執行並且不受任何終端控制的程式。使用守護程式的好處是該程式永遠以後臺方式啟動,生命週期一般都是和系統的啟動關閉狀態保持一致。

  • 守護程式和後臺程式的區別

守護程式和nohup + &啟動的後臺程式區別並不大,都是脫離終端的。但在程式組、檔案掩碼、工作目錄、標準/錯誤輸出輸入等會有不同。
對於Gin-IPs來說,用守護程式可以一鍵後臺啟動,並將日誌輸出到指定檔案,非常方便。

  • 建立守護程式

1、建立子程式,停止父程式
2、在子程式中建立新會話
3、改變工作目錄
4、重設檔案建立掩碼
5、重定向檔案描述符

Gin-API 建立守護程式

  • 實現函式
/*
Linux Mac 下執行
守護程式是生存期長的一種程式。它們獨立於控制終端並且週期性的執行某種任務或等待處理某些發生的事件。
守護程式必須與其執行前的環境隔離開來。這些環境包括未關閉的檔案描述符、控制終端、會話和程式組、工作目錄以及檔案建立掩碼等。這些環境通常是守護程式從執行它的父程式(特別是shell)中繼承下來的。
本程式只fork一次子程式,fork第二次主要目的是防止程式再次開啟一個控制終端(不是必要的)。因為開啟一個控制終端的前臺條件是該程式必須是會話組長,再fork一次,子程式ID != sid(sid是程式父程式的sid),所以也無法開啟新的控制終端
*/
package daemon

import (
	"fmt"
	"os"
	"os/exec"
	"syscall"
	"time"
)

//var daemon = flag.Bool("d", false, "run app as a daemon process with -d=true")

func InitProcess() {
	if syscall.Getppid() == 1 {
		if err := os.Chdir("./"); err != nil {
			panic(err)
		}
		syscall.Umask(0) // TODO TEST
		return
	}
	fmt.Println("go daemon!!!")
	fp, err := os.OpenFile("daemon.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		panic(err)
	}
	defer func() {
		_ = fp.Close()
	}()
	cmd := exec.Command(os.Args[0], os.Args[1:]...)
	cmd.Stdout = fp
	cmd.Stderr = fp
	cmd.Stdin = nil
	cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} // TODO TEST

	if err := cmd.Start(); err != nil {
		panic(err)
	}

	_, _ = fp.WriteString(fmt.Sprintf(
		"[PID] %d Start At %s\n", cmd.Process.Pid, time.Now().Format("2006-01-02 15:04:05")))
	os.Exit(0)
}

  • 初始化
func main() {
    daemon.InitProcess()   
    // ...
}

Gin-API 平滑重啟

建立守護程式之後,我們的程式已經能夠在後臺正常跑通了,但這樣還有個問題,那就是在重啟服務時候怎麼保證服務不中斷?

例如Nginx這種7*24小時接收請求的服務,在程式升級、配置檔案更新、或者外掛載入的時候就需要重啟,為保證重啟過程不中斷服務,我們會使用平滑重啟

  • 平滑重啟原理

gin-api服務作為協程啟動,做相應的處理並返回資料給客戶端;主程式負責監聽訊號,根據訊號進行關閉、重啟操作

  • 平滑重啟步驟

1、主程式(原程式中的主程式)啟動協程處理http請求,主程式開始監聽終端訊號
2、使用 kill -USR2 $pid 發起停止主程式的動作
3、主程式接收到訊號量 12 (SIGUSR2) 後, 啟動新的子程式,子程式接管父程式的標準輸出、錯誤輸出和socket描述符
4、子程式同樣啟動協程處理請求,子程式中的主程式繼續監聽終端訊號
5、父程式中的主程式發起關閉協程的動作,該協程處理完所有請求後自動關閉(平滑關閉)
6、父程式中的主程式退出

  • 使用 http.Server

由於gin庫函式缺少上下文管理功能,所以我們需要使用http.Server來包裹gin服務,支援對服務的平滑關閉功能

  • 實現方式
func (server *Server) Listen(graceful bool) error {
	addr := fmt.Sprintf("%s:%d", server.Host, server.Port)
	httpServer := &http.Server{
		Addr:    addr,
		Handler: server.Router,
	}
	// 判斷是否為 reload
	var err error
	if graceful {
		server.Logger.Info("listening on the existing file descriptor 3")
		//子程式的 0 1 2 是預留給 標準輸入 標準輸出 錯誤輸出
		//因此傳遞的socket 描述符應該放在子程式的 3
		f := os.NewFile(3, "")
		// 獲取 上個服務程式的 socket 的描述符
		server.Listener, err = net.FileListener(f)
	} else {
		server.Logger.Info("listening on a new file descriptor")
		server.Listener, err = net.Listen("tcp", httpServer.Addr)
		server.Logger.Infof("Actual pid is %d\n", syscall.Getpid())
	}
	if err != nil {
		server.Logger.Error(err)
		return err
	}

	go func() {
		// 開啟服務
		if err := httpServer.Serve(server.Listener); err != nil && err != http.ErrServerClosed {
			err = errors.New(fmt.Sprintf("listen error:%v\n", err))
			server.Logger.Fatal(err) // 報錯退出
		}
	}()
	return server.HandlerSignal(httpServer)
}

func (server *Server) HandlerSignal(httpServer *http.Server) error {
	sign := make(chan os.Signal)
	signal.Notify(sign, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2)
	for {
		// 接收訊號量
		sig := <-sign
		server.Logger.Infof("Signal receive: %v\n", sig)
		ctx, _ := context.WithTimeout(context.Background(), time.Second*10)
		switch sig {
		case syscall.SIGINT, syscall.SIGTERM:
			// 關閉服務
			server.Logger.Info("Shutdown Api Server")
			signal.Stop(sign) // 停止通道
			if err := httpServer.Shutdown(ctx); err != nil {
				err = errors.New(fmt.Sprintf("Shutdown Api Server Error: %s", err))
				return err
			}
			return nil
		case syscall.SIGUSR2:
			// 重啟服務
			server.Logger.Info("Reload Api Server")
			// 先啟動新服務
			if err := server.Reload(); err != nil {
				server.Logger.Errorf("Reload Api Server Error: %s", err)
				continue
			}
			// 關閉舊服務
			if err := httpServer.Shutdown(ctx); err != nil {
				err = errors.New(fmt.Sprintf("Shutdown Api Server Error: %s", err))
				return err
			}
			if err := destroyMgoPool(); err != nil {
				return err
			}
			server.Logger.Info("Reload Api Server Successful")
			return nil
		}
	}
}

func (server *Server) Reload() error {
	tl, ok := server.Listener.(*net.TCPListener)
	if !ok {
		return errors.New("listener is not tcp listener")
	}

	f, err := tl.File()
	if err != nil {
		return err
	}

	// 命令列啟動新程式
	args := []string{"-graceful"}
	cmd := exec.Command(os.Args[0], args...)
	cmd.Stdout = os.Stdout         //  1
	cmd.Stderr = os.Stderr         //  2
	cmd.ExtraFiles = []*os.File{f} //  3
	if err := cmd.Start(); err != nil {
		return err
	}
	server.Logger.Infof("Forked New Pid %v: \n", cmd.Process.Pid)
	return nil
}

守護程式和平滑重啟的功能在生產環境上經常被使用,但要注意的是隻能執行在Unix環境下。使用了這2個功能之後,程式在部署架構的時候就能發揮高可用的功能。
下一章,我們將介紹如何在生產環境部署服務。

Github 程式碼

請訪問 Gin-IPs 或者搜尋 Gin-IPs

相關文章