最近领导问我是怎么优雅重启的,好像说不出个因此然,又仔细研究一下这个facebookgo的grace项目了。git
从原理上来讲是这样一个过程:github
1)发布新的bin文件去覆盖老的bin文件 2)发送一个信号量,告诉正在运行的进程,进行重启 3)正在运行的进程收到信号后,会以子进程的方式启动新的bin文件 4)新进程接受新请求,并处理 5)老进程再也不接受请求,可是要等正在处理的请求处理完成,全部在处理的请求处理完以后,便自动退出 6)新进程在老进程退出以后,由init进程收养,可是会继续服务。
因此一步一步来看,关键是从第2步开始以后怎么作,因此咱们先来看看第2步的实现,这个应该说很简单,发送信号量到一个进程,使用kill命令便可,在facebook这个项目中发送的信号量有3个:SIGINT,SIGTERM,SIGUSR2,前面两个信号收到后程序会直接退出,后面一个信号SIGUSR2才会执行所谓的优雅重启。
第3步,正在运行的进程收到SIGUSR2信号后,会以子进程的方式启动新的bin文件。先直接上代码看:https://github.com/facebookgo/grace/blob/master/gracehttp/http.gogolang
func (a *app) signalHandler(wg *sync.WaitGroup) { ch := make(chan os.Signal, 10) signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2) for { sig := <-ch switch sig { case syscall.SIGINT, syscall.SIGTERM: // this ensures a subsequent INT/TERM will trigger standard go behaviour of // terminating. 执行标准的go终止行为,程序就结束了 signal.Stop(ch) a.term(wg) return case syscall.SIGUSR2: // 这里开始执行优雅重启 err := a.preStartProcess() // 这个函数在源代码中没有具体实现功能,只是预留了一个钩子函数,用户能够注册本身的函数,能够在重启以前作些自定义的事情。通常状况下也没有什么能够作的,除非有些特殊的服务环境或是状态保存之类的,至少目前,咱们的server尚未遇到 if err != nil { a.errors <- err } // we only return here if there's an error, otherwise the new process // will send us a TERM when it's ready to trigger the actual shutdown. if _, err := a.net.StartProcess(); err != nil { // 这里开始正式所谓的优雅重启 a.errors <- err } } } }
a.net.StartProcess的过程咱们来看看基本过程:app
func (n *Net) StartProcess() (int, error) { listeners, err := n.activeListeners() // 获取目前在监听的端口,这块也是重点,下面重点介绍 if err != nil { return 0, err } // Extract the fds from the listeners. 从监听端口中把文件描述符取出来 files := make([]*os.File, len(listeners)) for i, l := range listeners { files[i], err = l.(filer).File() if err != nil { return 0, err } defer files[i].Close() } // Use the original binary location. This works with symlinks such that if // the file it points to has been changed we will use the updated symlink. // 获取可执行bin文件的路劲,也能够是连接路劲,会使用最新的连接路径做为启动文件路劲的 argv0, err := exec.LookPath(os.Args[0]) if err != nil { return 0, err } // Pass on the environment and replace the old count key with the new one. // 获取 LISTEN_FDS 换进变量值 var env []string for _, v := range os.Environ() { if !strings.HasPrefix(v, envCountKeyPrefix) { env = append(env, v) } } env = append(env, fmt.Sprintf("%s%d", envCountKeyPrefix, len(listeners))) allFiles := append([]*os.File{os.Stdin, os.Stdout, os.Stderr}, files...) // 这里调用一个golang底层的进程启动函数,来指定,上面获取的参数来启动进程 process, err := os.StartProcess(argv0, os.Args, &os.ProcAttr{ Dir: originalWD, Env: env, Files: allFiles, }) if err != nil { return 0, err } // 返回新进程id。 return process.Pid, nil }
以上是启动新进程,而且接管监听端口的过程, 通常状况下端口是不能够重复监听的,因此这里就要须要使用比较特别的办法,从上面的代码来看就是读取监听端口的文件描述符,而且把监听端口的文件描述符传递给子进程,子进程里从这个文件描述符实现对端口的监听。ide