生产环境的API服务我们都会部署在Linux服务器上,为了不受终端状态的影响,启动服务的时候会让服务在后台运行。那么如何让服务在后台运行呢,目前有2种常见的方法。

1、nohub 运行

表示忽略SIGHUP(挂断)信号,终端退出的时候所发起的挂断信号会被忽略。nohup一般会结合&参数运行程序,&表示将程序设置为后台运行的程序。两者结合就变成了启动一个不受终端状态影响的后台服务。

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

2、守护进程

  • 理解守护进程
  • 守护进程和后台进程的区别
  • 创建守护进程

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 平滑重启

创建守护进程之后,我们的程序已经能够在后台正常跑通了,但这样还有个问题,那就是在重启服务时候怎么保证服务不中断?

  • 平滑重启原理
  • 平滑重启步骤
  • 使用 http.Server
  • 实现方式
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 代码

09-09 01:42