目录

go 平滑重启endless方案

信号处理

Go 信号通知通过在 Channel 上发送 os.Signal 值来工作。如我们如果使用 Ctrl+C,那么会触发 SIGINT 信号,操作系统会中断该进程的正常流程,并进入相应的信号处理函数执行操作,完成后再回到中断的地方继续执行。

func main() {
    sigs := make(chan os.Signal, 1)
    done := make(chan bool, 1)
    // 监听信号
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 
    go func() {
        // 接收到信号返回
        sig := <-sigs
        fmt.Println()
        fmt.Println(sig)
        done <- true
    }() 
    fmt.Println("awaiting signal")
    // 等待信号的接收
    <-done
    fmt.Println("exiting")
}

我们就可以监听 SIGINT 和 SIGTERM 信号。当 Go 接收到操作系统发送过来的信号,那么会将信号值放入到 sigs 管道中进行处理。

fork子进程

file := netListener.File() // this returns a Dup()
path := "/path/to/executable"
args := []string{
    "-graceful"}
// 产生 Cmd 实例
cmd := exec.Command(path, args...)
// 标准输出
cmd.Stdout = os.Stdout
// 标准错误输出
cmd.Stderr = os.Stderr
cmd.ExtraFiles = []*os.File{file}
// 启动命令
err := cmd.Start()
if err != nil {
    log.Fatalf("gracefulRestart: Failed to launch, error: %v", err)
}

通过调用 exec 包的 Command 命令传入 path(将要执行的命令路径)、args (命令的参数)即可返回 Cmd 实例,通过 ExtraFiles 字段指定额外被新进程继承的已打开文件,最后调用 Start 方法创建子进程。

这里的 netListener.File会通过系统调用 dup 复制一份 file descriptor 文件描述符。

func Dup(oldfd int) (fd int, err error) {
    r0, _, e1 := Syscall(SYS_DUP, uintptr(oldfd), 0, 0)
    fd = int(r0)
    if e1 != 0 {
        err = errnoErr(e1)
    }
    return
}

endless 不停机重启示例

func initRouter2() {
	engine := gin.Default()

	engine.GET("/bbb/aa", func(context *gin.Context) {
		time.Sleep(time.Second * 40)
		context.JSON(http.StatusOK, "11")
	})

	err := endless.ListenAndServe("localhost:8080", engine)
	if err != nil {
		log.Println(err)
	}
	log.Println("Server on 8080 stopped")
}
go build -o hello
./hello
修改文件,context.JSON(http.StatusOK, "22")
go build -o hello
curl http://localhost:8080/bbb/aa
kill -1 hello_pid
curl http://localhost:8080/bbb/aa
...
请求返回11
请求返回22

我们要做的不停机重启,监听 SIGHUP 信号;

  1. 收到信号时 fork 子进程(使用相同的启动命令),将服务监听的 socket 文件描述符传递给子进程;
  2. 子进程监听父进程的 socket,这个时候父进程和子进程都可以接收请求;
  3. 子进程启动成功之后发送 SIGTERM 信号给父进程,父进程停止接收新的连接,等待旧连接处理完成(或超时);
  4. 父进程退出,升级完成;

endless实现

func (srv *endlessServer) ListenAndServe() (err error) {
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    // 异步处理信号量
    go srv.handleSignals()
    // 获取端口监听
    l, err := srv.getListener(addr)
    if err != nil {
        log.Println(err)
        return
    }
    // 将监听转为 endlessListener
    srv.EndlessListener = newEndlessListener(l, srv)

    // 如果是子进程,那么发送 SIGTERM 信号给父进程
    if srv.isChild {
        syscall.Kill(syscall.Getppid(), syscall.SIGTERM)
    }

    srv.BeforeBegin(srv.Addr)
    // 响应Listener监听,执行对应请求逻辑
    return srv.Serve()
}

信号处理主要是信号的一个监听,然后根据不同的信号循环处理。

func (srv *endlessServer) handleSignals() {
    var sig os.Signal
    // 注册信号监听
    signal.Notify(
        srv.sigChan,
        hookableSignals...,
    )
    // 获取pid
    pid := syscall.Getpid()
    for {
        sig = <-srv.sigChan
        // 在处理信号之前触发hook
        srv.signalHooks(PRE_SIGNAL, sig)
        switch sig {
        // 接收到平滑重启信号
        case syscall.SIGHUP:
            log.Println(pid, "Received SIGHUP. forking.")
            err := srv.fork()
            if err != nil {
                log.Println("Fork err:", err)
            } 
        // 停机信号
        case syscall.SIGINT:
            log.Println(pid, "Received SIGINT.")
            srv.shutdown()
        // 停机信号
        case syscall.SIGTERM:
            log.Println(pid, "Received SIGTERM.")
            srv.shutdown()
        ...
        // 在处理信号之后触发hook
        srv.signalHooks(POST_SIGNAL, sig)
    }
}

当我们用kill -1 $pid 的时候这里 srv.sigChan 就会接收到相应的信号,并进入到 case syscall.SIGHUP 这块逻辑代码中。

参考链接:https://www.luozhiyun.com/archives/584