一、优雅重启/热更新
对外提供服务程序,在升级或因其它原因需重启时,若考虑不影响用户体验情况下,应当使用优雅重启或者说热更新。
二、目的
1.服务升级/更新时不关闭现有连结,对用户友好/用户无感知
2.新的进程启动并替代旧进程
3.新的进程接管新的连结
4.已建立的连结随时响应用户的请求,不可以出现拒绝访问的情况;同时新连结请求到达时应请求新进程
三、两种实现
1.在建立套接字时设置SO_REUSEPORT,从而让多个进程能够被绑定到同一端口。从而有多个流量接收队列绑定到多个进程。
2.复制套接字,并把它以文件的形式传给子进程,然后在新的进程中重新创建这个套接字。使用这种方法,可以有一个接收队列向多个进程提供数据
对于SO_REUSEPORT的方法,SO_REUSEPOR这个socket选项可以让你将多个socket绑定在同一个监听端口,然后让内核给你自动做负载均衡,将请求平均地让多个线程进行处理。负载均衡使用(remote_ip, remote_port, local_ip, local_port)来进行哈希,因此可以保证同一个client的包可以路由到同一个进程。但是,当一个listen的进程加进来或者terminate的时候,由于没有实现一致性哈希,结果可能导致有些请求由于路由到另外一个进程上,client-server的三次握手过程可能会被重置,因为启用SO_REUSEPORT的 socket 在内核中拥有不同的队列,在老进程停止accept并关闭监听 socket 的过程中,内核仍然会给该 socket 分配新建的链接到队列中,当老进程关闭监听 socket 后,内核并不会将其队列中的 pending 链接转移另一个监听相同地址的 socket 的队列里去,这样就造成了,如果业务新建连接的QPS很高时,仍然会拒绝一些新建连接的请求。
第二种方法,基于Unix 的 fork/exec 模型,即将所有打开文件传递给子进程,nginx的实现就是这种原理。其利用父子进程fork-exec继承文件描述符的特性,在父子进程之间维护传递监听 socket。在升级/重启的过程中,父进程将监听 socket 继承给子进程,使得整个过程没有监听 socket 被关闭,从而不产生拒绝服务的问题。
四、流程
1)发布新的bin文件去覆盖老的bin文件
2)发送一个信号量,告诉正在运行的进程,进行重启
3)正在运行的进程收到信号后,会以子进程的方式启动新的bin文件
4)新进程接受新请求,并处理
5)老进程不再接受请求,但是要等正在处理的请求处理完成,所有在处理的请求处理完之后,便自动退出
6)新进程在老进程退出之后,由init进程收养,但是会继续服务。
五、golang优雅重启
1.https://github.com/facebookarchive/grace
facebook整体实现比较简单,其只提供了一个简单的继承监听套接字方案,并不具备处理子进程失败、已有连接的功能。
2.https://github.com/jpillora/overseer
jpillora/overseer采用主从进程设计,有父进程创建监听 socket ,然后fork-exec派生出子进程,将全部监听 socket 继承给子进程,业务逻辑由子进程来运行。自带定时拉取新版本升级的功能,比较适合用来写App/Agent。由于框架设计的开发性不足,用户定制性差,比如动态增加端口等功能无法在该框架下实现。
3.https://github.com/cloudflare/tableflip
cloudflare/tableflip采用继承监听套接字方案,整体设计开放性足够,目前看起来是最好的一个实现。其提供在升级/重启过程中的父子进程之间同步功能,例如Ready()、WaitForParent()等。也能够灵活处理多个监听 socket和已存在的链接等。
4.https://github.com/fvbock/endless
简单易用的Golang HTTP和HTTPS服务器的零停机时间重新启动包。使用endless包示例代码:
```
package main
import (
"github.com/fvbock/endless"
"github.com/gin-gonic/gin"
"log"
)
func main() {
r := gin.New()
r.GET("/hello", func(c *gin.Context) {
c.String(200, "world")
})
s := endless.NewServer(":8080", r)
s.BeforeBegin = func(add string) {
log.Printf("Actual pid is %d", syscall.Getpid())
}
err := s.ListenAndServe()
if err != nil {
log.Printf("server err: %v", err)
}
}
```
输出:
2020/03/15 18:06:29 4146 Received SIGHUP. forking.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /hello --> main.main.func1 (1 handlers)
2020/03/15 18:06:30 Actual pid is 4189
2020/03/15 18:06:30 4146 Received SIGTERM.
2020/03/15 18:06:30 4146 Waiting for connections to finish...
2020/03/15 18:06:30 4146 Serve() returning...
2020/03/15 18:06:30 4146 [::]:8080 Listener closed.
2020/03/15 18:06:30 server err: accept tcp [::]:8080: use of closed network connection
参考文章链接:
【1】https://www.cnblogs.com/sunsky303/p/11121409.html
【2】https://studygolang.com/articles/14038?fr=sidebar
【3】https://www.jianshu.com/p/37216e299bff
【4】https://www.jb51.net/article/137069.htm
【5】https://studygolang.com/articles/21175
【6】https://blog.cloudflare.com/graceful-upgrades-in-go/
有疑问加站长微信联系(非本文作者)