golang 服务器零停机热重启原理详解

本文详细解析了如何使用Go语言实现服务器零停机热重启。通过理解TCP服务实现、fork子进程的限制、exec系统调用的作用以及进程间通讯,结合具体的实现思路,包括监听信号、停止accept新请求、复制socket资源到子进程,并在子进程中判断并执行相应操作,确保服务不间断。最后引用了GitHub上的endless库源码作为参考。
摘要由CSDN通过智能技术生成
关于TCP服务的实现过程

首先需要了解tcp服务器,socker机制,这些和语言无关。

一个简单的tcp server实现过程:
创建socket资源,socker1 ,是一个打开的文件描述符。
bind绑定到Hosu, Port。
开始监听listen。

经过以上过程,一个tcp服务就搭建好了。

客户端发起tcp连接请求
操作系统与其完成三次握手阶段,此连接就算建立了,操作系统根据tcp协议中的端口号标识将此连接放入指定的 backlog 队列中。

tcp server 从 backlog 队列中 accept 请求,也就是消费此队列,那么,它该从哪个 backlog 中消费呢,所以 accept 操作时需要带上自己的 socket1 。
被成功 accept 的请求才算和 tcp server 建立了连接,在此之前只是完成了三次握手。

于是 client 和 server 之间开始 read & write 。

于是,只要你得到了 socket1 ,你就可以冒充 tcp server ,也就可以 accept 客户端,并与之通讯。

关于fork子进程

我们还知道 fork 出的子进程会继承父进程的 socket 资源,于是便有了通常的操作:产生多个子进程来并行接受请求以达到提高并发能力的效果。

但是fork出的子进程是继承了父进程的上下文,和父进程用有相同的程序和代码,然而我们希望子进程加载新的程序从而实现热重启,所以fork操作显然无法满足需求。

关于exec系统调用

还有一个能产生子进程的方式就是exec族函数了,此函数会产生一个空的子进程,然后加载进指定的程序到进程空间并执行,这样一来虽然满足了使用新程序的需求,但是子进程却无法继承父进程的资源了,这就尴尬了,

但是,go封装的exec包提供了更加底层的能力,允许传递更多的参数,比如,你甚至可以将文件描述符复制到子进程中,这个能力还是很新颖的,在别的语言中貌似没看到。

也正是这一点,go才可以实现真正的零停机热重启,像java,swoole只能实现局部热重载,因此意义不大,所以一般是使用滚动发布的形式。

exec族函数系统调用,是通过启动一个新的进程来执行一段程序或命令,此进程将成为直属的子进程。
关于exec族可以看一下这个文章 https://blog.csdn.net/raoxiaoya/article/details/108073550

exec族函数:

exec(path, args..., env)

path: 要执行的命令
arges: 传递给命令的参数
env:可以携带一些环境变量到进程里面
关于进程通讯

以上概念可以实现使用新的程序来启动新的服务,还差一步就是关闭老的服务。

进程是个封闭的空间,进程间的通讯有很多方式,当然简单的通讯首选进程信号,这是操作最简单的。

完整的实现思路

1、go主协程提供服务。

2、开启子协程注册,监听并处理信号,比如 SIGINT --> 关机,SIGHUP --> 热重启。

3、进程收到 SIGINT 信号。
设置状态来停止 accept 操作,就是不再接受请求了,多余的请求就留在 backlog 里面了,然后在一定的时间延迟内完成对已有请求的处理,处理完毕或者时间到了就退出进程。

4、进程收到 SIGHUP 信号,创建新服务。
path 和 args 参数:和主进程的启动命令与参数一样,通过 os.Args 获取

env:复制父进程所有的环境变量,并且添加一个新的环境变量作为标志(比如 ENDLESS_CONTINUE=1),以此来区别当前进程是否是子进程,可想而知后面所有的热重启操作产生的进程都带有这个标志。

重点来了,复制 socket1 文件描述符到子进程中。

子进程启动之后又会从头执行新的代码,那么问题来了,这样会导致重新执行 bind 和 listen 操作,显然这样会报错的,因为端口已经被主进程监听着,所以在这里需要通过环境变量中的 ENDLESS_CONTINUE 的值来做个判断。

好了,现在有两个进程在同时提供服务,新程序继续向下执行,需要关闭父进程了,在子进程里面(通过ENDLESS_CONTINUE来判断),向父进程发送SIGINT信号(在子进程中可以获取父进程的PID)。

那么为什么不在父进程中执行完exec就主动终止自己呢,那是因为子进程不一定能启动成功并提供服务,所以终止信号得由子进程来发送,并且是在确保可以提供服务之后的地方尽早发送信号。

源码部分

执行子进程
https://github.com/fvbock/endless

endless.go


// 执行子进程部分

func (srv *endlessServer) fork() (err error) {
   
	runningServerReg.Lock()
	defer runningServerReg.Unlock()

	// only one server instance should fork!
	if runningServersForked {
   
		return errors.New("Another process already forked. Ignoring this one.")
	}

	runningServersForked = true

	var files = make([]*os.File, len(runningServers))
	var orderArgs = make([]string, len(runningServers)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值