endless 如何实现不停机重启 Go 程序?

前几篇文章讲解了如何实现一个高效的 HTTP 服务,这次我们来看一下如何实现一个永不不停机的 Go 程序

前提

事情是这样的,在一天风和日丽的周末,我正在看 TiDB 源码的时候,有一位胖友找到我说,Go 是不是每次修改都需要重启才行?由于我才疏学浅不知道有不停机重启这个东西,所以回答是的。然后他说,那完全没有 PHP 好用啊,PHP 修改逻辑完之后直接替换一个文件就可以实现发布,不需要重启。我当时只能和他说可以多 Pod 部署,金丝雀发布等等也可以做到整个服务不停机发布。但是他最后还是带着得以意笑容离去。

当时看着他离去的身影我就发誓,我要研究一下 Go 语言的不停机重启,证明不是 Go 不行,而是我不行 [DOGE] [DOGE] [DOGE],所以就有了这么一篇文章。

那么对于一个不停机重启 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 子进程

在Go语言中 exec 包为我们很好的封装好了 Fork 调用,并且使用它可以使用 ExtraFiles 很好的继承父进程已打开的文件。

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 }

我们可以看到 dup 的命令介绍:

dup and dup2 create a copy of the file descriptor oldfd. After successful return of dup or dup2, the old and new descriptors may be used interchangeably. They share locks, file position pointers and flags; for example, if the file position is modified by using lseek on one of the descriptors, the position is also changed for the other. The two descriptors do not share the close-on-exec flag, however.

通过上面的描述可以知道,返回的新文件描述符和参数 oldfd 指向同一个文件,共享所有的索性、读写指针、各项权限或标志位等。但是不共享关闭标志位,也就是说 oldfd 已经关闭了,也不影响写入新的数据到 newfd 中。

graceful_restart3

上图显示了fork一个子进程,子进程复制父进程的文件描述符表。

endless 不停机重启示例

我这里稍微写一下 endless 的使用示例给没有用过 endless 的同学看看,熟悉 endless 使用的同学可以跳过。

import ( "log" "net/http" "os" "sync" "time" "github.com/fvbock/endless" "github.com/gorilla/mux" ) func handler(w http.ResponseWriter, r *http.Request) { duration, err := time.ParseDuration(r.FormValue("duration")) if err != nil { http.Error(w, err.Error(), 400) return } time.Sleep(duration) w.Write([]byte("Hello World")) } func main() { mux1 := mux.NewRouter() mux1.HandleFunc("/sleep", handler) w := sync.WaitGroup{} w.Add(1) go func() { err := endless.ListenAndServe("127.0.0.1:5003", mux1) if err != nil { log.Println(err) } log.Println("Server on 5003 stopped") w.Done() }() w.Wait() log.Println("All servers stopped. Exiting.") os.Exit(0) }

下面验证一下 endless 创建的不停机服务:

# 第一次构建项目 go build main.go # 运行项目,这时就可以做内容修改了 ./endless & # 请求项目,60s后返回 curl "http://127.0.0.1:5003/sleep?duration=60s" & # 再次构建项目,这里是新内容 go build main.go # 重启,17171为pid kill -1 17171 # 新API请求 curl "http://127.0.0.1:5003/sleep?duration=1s"

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zyspwy.html