【Gin-API系列】守護程序和平滑重啟(八)
阿新 • • 發佈:2020-09-08
生產環境的API服務我們都會部署在Linux伺服器上,為了不受終端狀態的影響,啟動服務的時候會讓服務在後臺執行。那麼如何讓服務在後臺執行呢,目前有2種常見的方法。
### 1、nohub 執行
表示忽略`SIGHUP`(結束通話)訊號,終端退出的時候所發起的結束通話訊號會被忽略。`nohup`一般會結合`&`引數執行程式,`&`表示將程式設定為後臺執行的程式。兩者結合就變成了啟動一個不受終端狀態影響的後臺服務。
```bash
nohup gin-ips >> gin-api.out 2>&1 &
```
### 2、守護程序
* 理解守護程序
> 守護程序是一個在後臺執行並且不受任何終端控制的程序。使用守護程序的好處是該程序永遠以後臺方式啟動,生命週期一般都是和系統的啟動關閉狀態保持一致。
* 守護程序和後臺程序的區別
> 守護程序和`nohup` + `&`啟動的後臺程序區別並不大,都是脫離終端的。但在程序組、檔案掩碼、工作目錄、標準/錯誤輸出輸入等會有不同。
> 對於Gin-IPs來說,用守護程序可以一鍵後臺啟動,並將日誌輸出到指定檔案,非常方便。
* 建立守護程序
> 1、建立子程序,停止父程序
2、在子程序中建立新會話
3、改變工作目錄
4、重設檔案建立掩碼
5、重定向檔案描述符
# Gin-API 建立守護程序
* 實現函式
```golang
/*
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)
}
```
* 初始化
```golang
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`服務,支援對服務的平滑關閉功能
* 實現方式
```golang
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](https://github.com/AutoBingo/Gin-IPs.git) 或者搜尋