从nginx热更新聊一聊Golang中的热更新(下)
静态语言在服务器编程时都会遇到这样的问题:如何保证已有的连接服务不中断同时又升级版本?
在上一篇介绍热升级的时候时候,讲到了通过信号通知nginx进行热升级。我们在这一篇中介绍下平滑重启go http server。
目录结构
热更新
热更新目标:
- 1、正在处理中的连接/服务/请求不能立即中断,需要继续提供服务
- 2、socket对用户来说要保持可用,可以接受新的请求
直接沿用上篇的思路,热更新(单进程)流程,其基本流程如下:
- 1、用新的bin文件去替换老的bin文件
- 2、发送信号告知server进程(通常是USR2信号),进行平滑升级
- 3、server进程收到信号后,通过调用 fork/exec 启动新的版本的进程
- 4、子进程调用接口获取从父进程继承的 socket 文件描述符重新监听 socket
- 5、老的进程不再接受请求,待正在处理中的请求处理完后,进程自动退出
- 6、子进程托管给init进程
我们可以按照这个思路完成一个简单的可以热更新的http server
简易的http server
首先,我们需要一个最简单的http server
func main() {
fmt.Println("Hello World!")
var err error
// 注册http请求的处理方法
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello world!"))
})
// 在8086端口启动http服务,其内部有一个循环accept 8086端口
// 每当新的HTTP请求过来则开一个协程处理
err = http.ListenAndServe("localhost:8086", nil)
if err != nil {
log.Println(err)
}
}
fork一个新的进程
在go语言里面可以有很多种方法fork一个新的进程,但是在这里我更倾向于推荐exec.Command接口来启动一个新的进程。因为Cmd struct中有一个ExtraFiles变量,子进程可以通过它直接继承文件描述符fd。
func forkProcess() error {
var err error
files := []*os.File{gListen.File()} //demo only one //.File()
path := "/Users/yousa/work/src/graceful-restart-demo/graceful-restart-demo"
args := []string{
"-graceful",
}
env := append(
os.Environ(),
"ENDLESS_CONTINUE=1",
)
env = append(env, fmt.Sprintf(`ENDLESS_SOCKET_ORDER=%s`, "0,127.0.0.1"))
cmd := exec.Command(path, args...)
//cmd := exec.Command(path, "-graceful", "true")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.ExtraFiles = files
cmd.Env = env
err = cmd.Start()
if err != nil {
log.Fatalf("Restart: Failed to launch, error: %v", err)
return err
}
return nil
}
代码浅析:
在上面的files是存储父进程的文件描述符,path的内容是新的要替换的可执行文件的路径。
重要的一点是,.File()返回一个dup(2)的文件描述符。这个重复的文件描述符不会设置FD_CLOEXEC 标志,这个文件描述符操作容易出错,容易被在子进程中被错误关闭。
在其他语言(或者go里面)里面你可能通过使用命令行将文件描述符传递给子进程,在这里比较推荐使用ExtraFile传递fd。不过ExtraFiles在windows中不支持。
args中传递的-graceful参数是告诉子进程这是优雅热升级的一部分,这样子进程可以通过它知道,自己需要重用套接字而不是重新打开一个新的套接字
子进程初始化
func main() {
fmt.Println("Hello World!")
...
var gracefulChild bool
var netListen net.Listener
var err error
args := os.Args
...
if len(args) > 1 && args[1] == "-graceful" {
gracefulChild = true
} else {
gracefulChild = false
}
fmt.Println("gracefulChild:", gracefulChild)
if gracefulChild {
//重用套接字
log.Print("main: Listening to existing file descriptor 3.")
f := os.NewFile(3, "")
netListen, err = net.FileListener(f)
} else {
log.Print("main: Listening on a new file descriptor.")
netListen, err = net.Listen("tcp", gServer.Addr)
}
if err != nil {
log.Fatal(err)
return
}
...
}
args用于解析入参,gracefulChild表示进程自己是否是子进程(对应到fork中的-graceful)(这里更推荐flag.BoolVar,但是写demo的时候使用起来有些问题,故临时使用args)
net.FileListener重用套接字,ExtraFiles中传递的套接字