优雅启动http服务

一般的时候我们对服务进行升级的时候,会直接终止当前服务,ctrl + c 终止,或者发送其他信号量终止。 生产的话,

  • 一般部署在k8s ,k8s 做对应的升级摘除,一批次一批次的进行摘除更新挂载
  • nginx做反代,一台一台的摘机器,升级完毕在挂在
  • 或者DNS做对应的域名解析

这些或多或少都存在一个问题,如果我当前的http请求打在了当前要摘除的服务上,如何保证请求完毕才进行服务升级重启。有的是给定一个时间10s,或者更长,那如果我的请求是超过这个时间呢,强制的操作,导致连接中断,对于一些业务要求很严谨的场景,我们不知道最后的结果,很显然,这很难接受。

我们来看下,如何解决这个问题

首先我们来看下信号量有哪几种

命令信号含义

因此在我们执行ctrl + c关闭gin服务端时,会强制进程结束,导致正在访问的用户等出现问题

常见的 kill -9 pid 会发送 SIGKILL 信号给进程,也是一样的。

怎么样才是优雅

  1. 首先现有的请求是不能关闭的
  2. 新的进程启动并替代旧进程
  3. 新的进程接管新的连接
  4. 旧服务的连接依然存在,新服务处理新的请求

流程是怎样的

  1. 发送信号量
  2. 服务fork新的进程
  3. 新的进程处理新的请求
  4. 旧的进程要处理完毕之前到达的请求,然后退出进程

endless

https://github.com/fvbock/endless

endless 的作用:

  • 不需要在负载均衡器或其他东西上挂入或挂出——只需要编译、SIGHUP、启动新的请求、完成旧的请求等等。

需要注意

Hammer Time(子进程多久通知父进程关闭)

为了在父进程重新启动后处理挂起的请求,endless将在收到来自分支子进程的关闭信号60秒后打击父进程。当收到通知信号时,仍然在运行的请求会被终止。这种行为可以由另一个导出的变量控制:

DefaultHammerTime time.Duration

默认值是60秒。当设置为-1时,不会自动调用hammerTime()。然后,您可以通过发送SIGUSR2来手动确定父节点。这只会在父程序已经处于关机模式时打击它。因此,除非进程在SIGUSR2之前接收到SIGTERM、SIGSTOP或SIGINT(手动或通过分支),否则将被忽略。

如果你有挂起的请求,服务器被敲打,你会看到这样的日志消息:

2015/04/04 13:04:10 [STOP - Hammer Time] Forcefully shutting down parent

使用案例

func main(){
 endless.DefaultReadTimeOut =  5 * time.Second
 endless.DefaultWriteTimeOut = 5* time.Second
 endless.DefaultMaxHeaderBytes = 1 << 20
 endPoint := fmt.Sprintf(":%d",8088)


 r := Router{}
 Routes(r)
 server := endless.NewServer(endPoint,r)

 server.BeforeBegin = func(add string){
  log.Printf("Actual pid is %d",syscall.Getpid())
 }

 err := server.ListenAndServe()
 if err != nil{
  log.Printf("Server err : %v",err)
 }

}
func Routes(r Router){
 r.Route("GET", "/hello", func(w http.ResponseWriter, r *http.Request) {
  fmt.Println(r.Body)
  fmt.Println(r.URL)
  fmt.Println(r)
  fmt.Printf("%#v",r)
  w.Write([]byte("Hello,Chongchong!"))
 })
 r.Route("GET", `/hello/(?P<Message>\w+)`, func(w http.ResponseWriter, r *http.Request) {
  message := URLParam(r, "Message")
  w.Write([]byte("Hello " + message))
 })
}

go run main.go
2021/08/18 11:07:39 Actual pid is 31020

服务启动,pid为 31020

我们在另一个终端输入 kill -1 31020 结果

2021/08/18 11:27:11 31020 Received SIGHUP. forking.
2021/08/18 11:27:11 Actual pid is 31094
2021/08/18 11:27:11 31020 Received SIGTERM.
2021/08/18 11:27:11 31020 Waiting for connections to finish...
2021/08/18 11:27:11 31020 Serve() returning...
2021/08/18 11:27:11 Server err : accept tcp [::]:8088: use of closed network connection
2021/08/18 11:27:11 31020 [::]:8088 Listener closed.

我们看到 31020 已经挂起,并且 fork 了新的子进程 pid 为 31094

ps -ef|grep 31094 
  501 31094     1   0 11:27AM ttys006    0:00.01 /var/folders/69/m2hx0d9532q3cnkt80_ndtxh0000gn/T/go-build3023669437/b001/exe/main

大致意思为主进程(pid为31020)接受到 SIGTERM 信号量,关闭主进程的监听并且等待正在执行的请求完成。

接下来见证奇迹

http://127.0.0.1:8088/hello

原来的进程活了


只需要给该进程发送SIGTERM信号,而不需要强制结束应用,是多么便捷又安全的事!

问题

endless 热更新是采取创建子进程后,将原进程退出的方式,这点不符合守护进程的要求

http.Server - Shutdown()

如果你的Golang >= 1.8,也可以考虑使用 http.Server 的 Shutdown 方法

r := &routerApi.Router{}
 r.Route("GET", "/hello", func(w http.ResponseWriter, r *http.Request) {
  fmt.Println(r.Body)
  fmt.Println(r.URL)
  fmt.Println(r)
  fmt.Printf("%#v",r)
  time.Sleep(10 * time.Second)
  fmt.Println(12345)
  w.Write([]byte("Hello,Chongchong!"))
 })
 r.Route("GET", `/hello/(?P<Message>\w+)`, func(w http.ResponseWriter, r *http.Request) {
  message := routerApi.URLParam(r, "Message")
  w.Write([]byte("Hello " + message))
 })
 server := &http.Server{
  Addr:              fmt.Sprintf(":%d",8089),
  Handler:           r,
  TLSConfig:         nil,
  ReadTimeout:       0,
  ReadHeaderTimeout: 0,
  WriteTimeout:      0,
  IdleTimeout:       0,
  MaxHeaderBytes:    0,
  TLSNextProto:      nil,
  ConnState:         nil,
  ErrorLog:          nil,
  BaseContext:       nil,
  ConnContext:       nil,
 }


 go func() {
  quit := make(chan os.Signal)
  signal.Notify(quit,os.Interrupt)

  <- quit
  log .Println("shutdown Server ...")
  ctx ,cancel := context.WithTimeout(context.Background(),5 *time.Second)
  defer cancel()
  if err := server.Shutdown(ctx);nil != err{
   log.Fatal("Server Shutdown:" ,err)
  }
  log .Println("Server exiting")
 }()

 if err := server.ListenAndServe();nil != err{
  log.Println(err)
 }

在实际的工作中,无损的热重启是右很多场景需要的,我们要根据自己的需要来设置