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

运行完上面的命令我们可以看到,对于第一个请求返回的是:Hello world,在发送第二个请求之前,我将 handler 里面的返回值改成了:Hello world2222,然后进行构建重启。

由于我设置了 60s 才返回第一个请求,第二个请求设置的是 1s 返回,所以这里会先返回第二个请求的值,然后再返回第一个请求的值。

整个时间线如下所示:

graceful_restart2

并且在等待第一个请求返回期间,可以看到同时有两个进程在跑:

$ ps -ef |grep main root 84636 80539 0 22:25 pts/2 00:00:00 ./main root 85423 84636 0 22:26 pts/2 00:00:00 ./main

在第一个请求响应之后,我们再看进程可以发现父进程已经关掉了,实现了父子进程无缝切换:

$ ps -ef |grep main root 85423 1 0 22:26 pts/2 00:00:00 ./main 实现原理

在实现上,我这里用的是 endless 的实现方案,所以下面原理和代码都通过它的代码进行讲解。

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

我们要做的不停机重启,实现原理如上图所示:

监听 SIGHUP 信号;

收到信号时 fork 子进程(使用相同的启动命令),将服务监听的 socket 文件描述符传递给子进程;

子进程监听父进程的 socket,这个时候父进程和子进程都可以接收请求;

子进程启动成功之后发送 SIGTERM 信号给父进程,父进程停止接收新的连接,等待旧连接处理完成(或超时);

父进程退出,升级完成;

代码实现

我们从上面的示例可以看出,endless 的入口是 ListenAndServe 函数:

func ListenAndServe(addr string, handler http.Handler) error { // 初始化 server server := NewServer(addr, handler) // 监听以及处理请求 return server.ListenAndServe() }

这个方法分为两部分,先是初始化 server,然后再监听以及处理请求。

初始化 Server

我们首先看一下一个 endless 服务的 Server 结构体是怎样:

type endlessServer struct { // 用于继承 http.Server 结构 http.Server // 监听客户端请求的 Listener EndlessListener net.Listener // 用于记录还有多少客户端请求没有完成 wg sync.WaitGroup // 用于接收信号的管道 sigChan chan os.Signal // 用于重启时标志本进程是否是为一个新进程 isChild bool // 当前进程的状态 state uint8 ... }

这个 endlessServer 除了继承 http.Server 所有字段以外,因为还需要监听信号以及判断是不是一个新的进程,所以添加了几个状态位的字段:

wg:标记还有多少客户端请求没有完成;

sigChan:用于接收信号的管道;

isChild:用于重启时标志本进程是否是为一个新进程;

state:当前进程的状态。

下面我们看看如何初始化 endlessServer :

func NewServer(addr string, handler http.Handler) (srv *endlessServer) { runningServerReg.Lock() defer runningServerReg.Unlock() socketOrder = os.Getenv("ENDLESS_SOCKET_ORDER") // 根据环境变量判断是不是子进程 isChild = os.Getenv("ENDLESS_CONTINUE") != "" // 由于支持多 server,所以这里需要设置一下 server 的顺序 if len(socketOrder) > 0 { for i, addr := range strings.Split(socketOrder, ",") { socketPtrOffsetMap[addr] = uint(i) } } else { socketPtrOffsetMap[addr] = uint(len(runningServersOrder)) } srv = &endlessServer{ wg: sync.WaitGroup{}, sigChan: make(chan os.Signal), isChild: isChild, ... state: STATE_INIT, lock: &sync.RWMutex{}, } srv.Server.Addr = addr srv.Server.ReadTimeout = DefaultReadTimeOut srv.Server.WriteTimeout = DefaultWriteTimeOut srv.Server.MaxHeaderBytes = DefaultMaxHeaderBytes srv.Server.Handler = handler runningServers[addr] = srv ... return }

这里初始化都是我们在 net/http 里面看到的一些常见的参数,包括 ReadTimeout 读取超时时间、WriteTimeout 写入超时时间、Handler 请求处理器等,不熟悉的可以看一下这篇:《 一文说透 Go 语言 HTTP 标准库 https://www.luozhiyun.com/archives/561 》。

需要注意的是,这里是通过 ENDLESS_CONTINUE 环境变量来判断是否是个子进程,这个环境变量会在 fork 子进程的时候写入。因为 endless 是支持多 server 的,所以需要用 ENDLESS_SOCKET_ORDER变量来判断一下 server 的顺序。

ListenAndServe func (srv *endlessServer) ListenAndServe() (err error) { addr := srv.Addr if addr == "" { addr = ":http" } // 异步处理信号量 go srv.handleSignals() // 获取端口监听 l, err := srv.getListener(addr) if err != nil { log.Println(err) return } // 将监听转为 endlessListener srv.EndlessListener = newEndlessListener(l, srv) // 如果是子进程,那么发送 SIGTERM 信号给父进程 if srv.isChild { syscall.Kill(syscall.Getppid(), syscall.SIGTERM) } srv.BeforeBegin(srv.Addr) // 响应Listener监听,执行对应请求逻辑 return srv.Serve() }

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

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