【Gin-API系列】守护进程和平滑重启(八)

生产环境的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