从nginx热更新聊一聊Golang中的热更新(下)

本文探讨了Golang中如何实现HTTP服务器的热更新,通过信号通知进行平滑升级,详细介绍了使用`fork`创建新进程、子进程初始化、优雅停止父进程等步骤,并提到了github上的开源解决方案如`facebookgo/grace`、`fvbock/endless`等,以及遇到的问题和对比分析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

从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中传递的套接字,从idx 3的位置开始获取。

给父进程发送信号停止父进程

func main() {
	//init
	...
	
	if gracefulChild {
		syscall.Kill
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值