一、优雅重启/热更新

对外提供服务程序,在升级或因其它原因需重启时,若考虑不影响用户体验情况下,应当使用优雅重启或者说热更新。

二、目的

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优雅重启

facebook整体实现比较简单,其只提供了一个简单的继承监听套接字方案,并不具备处理子进程失败、已有连接的功能。

jpillora/overseer采用主从进程设计,有父进程创建监听 socket ,然后fork-exec派生出子进程,将全部监听 socket 继承给子进程,业务逻辑由子进程来运行。自带定时拉取新版本升级的功能,比较适合用来写App/Agent。由于框架设计的开发性不足,用户定制性差,比如动态增加端口等功能无法在该框架下实现。

cloudflare/tableflip采用继承监听套接字方案,整体设计开放性足够,目前看起来是最好的一个实现。其提供在升级/重启过程中的父子进程之间同步功能,例如Ready()、WaitForParent()等。也能够灵活处理多个监听 socket和已存在的链接等。

简单易用的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

参考文章链接: