平滑重启(热重启)是指WebServer在重启的时候不会中断已有请求的执行。该特性在不同的项目版本发布的时候特别有用,例如,当需要先后发布两个版本: AB,那么在A执行的过程当中,我们可以将B的程序发布直接覆盖 A的程序, 并使用平滑重启特性(使用Web或者命令行)无缝地将请求过渡到新版本的服务中。 

GoFrame框架支持非常方便的Web管理功能,也就是说我们可以通过Web页面/接口直接进行Server的重启/关闭等管理操作。同时,框架也支持通过命令行终端指令(仅限*nix系统)的形式进行Server的重启/关闭等管理操作。

特性开启

默认情况下平滑重启特性是关闭的,可以通过graceful配置选项打开,具体请查看WebServer的配置管理章节:服务配置-配置文件模板

目前平滑重启特性需要在本地随机打开一个端口的tcp监听服务用于新老进程通信交换状态信息。

注意事项

  • 该特性仅限于*nix系统(Linux/Unix/FreeBSD等等),在Windows下仅支持完整重启功能(请求无法平滑过渡)。
  • 测试平滑重启特性时请不要使用IDE run(例如Goland)或者go run命令来运行进程,因为这两种方式本身会创建父进程来管理运行的Go进程,会引起平滑重启时子父进程状态交换的失败。
  • 后续示例中的SetGraceful配置方法在v2.7.4版本后新增,低于v2.7.4版本请使用配置管理方式启用平滑重启特性。

管理方法

我们先来看一下WebServer中涉及到管理操作方法有哪些:

        
          func (s *Server) Restart
          (newExeFilePath...
          string
          ) error
func (s *Server) Shutdown
          (
          ) error

func (s *Server) EnableAdmin
          (pattern ...
          string
          )
        
      

Restart用于重启服务(*nix系统下为平滑重启,windows下为完整重启),Shutdown用于关闭服务,EnableAdmin用于将管理页面注册到指定的路由规则上,默认地址是/debug/admin(我们可以指定一个私密的管理地址,也可以使用中间件来对该页面进行鉴权)。

以下对其中两个方法做详细说明。

Restart

Restart的参数可指定自定义重启的可执行文件路径(newExeFilePath),不传递时默认为原可执行文件路径。特别是在windows系统下,当可执行文件正在使用时,无法对其进行文件替换更新(新版本文件替换老版本文件)。当指定自定义的可执行文件路径后,Server重启时将会执行新版本的可执行文件,不再使用老版本文件,这种特性简化了在某些系统上的版本更新流程。

EnableAdmin

  • 首先,该方法为用户管理Server提供了简便的页面和接口,在单Server下管理非常方便,直接访问管理页面点击对应链接即可。需要注意的是,由于带有管理功能,如果是在生产环境上,建议自定义该管理地址为一个私密地址。
  • 同时,EnableaAdmim提供的restart接口也支持自定义可执行文件路径,直接通过GET参数往restart接口传递newExeFilePath变量即可,例如:  http://127.0.0.1/debug/admin/restart?newExeFilePath=xxxxxxx
  • 此外,在大多数时候,Server往往不只有1个节点,因此大多数服务管理运维中,例如:重启操作,当然不是直接访问每个Serveradmin页面手动执行重启操作。而是充分利用admin页面提供的功能接口,通过接口控制来实现统一的Server管理控制。

示例1:基本使用

package main

import (
	"time"

	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
	"github.com/gogf/gf/v2/os/gproc"
)

func main() {
	s := g.Server()
	s.BindHandler("/", func(r *ghttp.Request) {
		r.Response.Writeln("哈喽!")
	})
	s.BindHandler("/pid", func(r *ghttp.Request) {
		r.Response.Writeln(gproc.Pid())
	})
	s.BindHandler("/sleep", func(r *ghttp.Request) {
		r.Response.Writeln(gproc.Pid())
		time.Sleep(10 * time.Second)
		r.Response.Writeln(gproc.Pid())
	})
	s.SetGraceful(true)
	s.EnableAdmin()
	s.SetPort(8199)
	s.Run()
}

我们通过以下几个步骤来测试平滑重启:

  1. 访问  http://127.0.0.1:8199/pid  查看当前进程的pid

  2. 访问  http://127.0.0.1:8199/sleep  ,这个页面将会执行10秒,用于测试重启时该页面请求执行是否会断掉

  3. 访问  http://127.0.0.1:8199/debug/admin  ,这是s.EnableAdmin后默认注册的一个WebServer管理页面

  4. 随后我们点击restart管理链接,WebServer将会立即平滑重启(*nix系统下)

    同时在终端也会输出以下信息:

                 2018-05-18 11:02:04.812 11511: http server started listening on [:8199]
     2018-05-18 11:02:09.172 11511: server reloading
     2018-05-18 11:02:09.172 11511: all servers shutdown
     2018-05-18 11:02:09.176 16358: http server restarted listening on [:8199]
                
              
  5. 我们可以发现在整个操作中,sleep页面的执行并没有被中断,继续等待几秒,当sleep执行完成后,页面输出内容为:

  6. 可以发现,sleep页面输出的进程pid和之前的不一样了,代表请求的执行被新的进程平滑接管,旧的服务进程也随之销毁;

示例2:HTTPS支持

package main

import (
    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/net/ghttp"
)

func main() {
    s := g.Server()
    s.BindHandler("/", func(r *ghttp.Request){
        r.Response.Writeln("哈罗!")
    })
    s.SetGraceful(true)
    s.EnableHTTPS("/home/john/temp/server.crt", "/home/john/temp/server.key")
    s.EnableAdmin()
    s.SetPort(8200)
    s.Run()
}

GoFrame框架的平滑重启特性对于HTTPS的支持也是相当友好和简便,操作步骤如下:

  1. 访问  https://127.0.0.1:8200/debug/admin/restart  平滑重启HTTPS服务;
  2. 访问  https://127.0.0.1:8200/debug/admin/shutdown  平滑关闭WebServer服务;

在命令行终端可以看到以下输出信息:

        2018-05-18 11:13:05.554 17278: https server started listening on [:8200]
2018-05-18 11:13:21.270 17278: server reloading
2018-05-18 11:13:21.270 17278: all servers shutdown
2018-05-18 11:13:21.278 17319: https server reloaded listening on [:8200]
2018-05-18 11:13:34.895 17319: server shutting down
2018-05-18 11:13:34.895 17269: all servers shutdown
        
      

示例3:多服务及多端口

GoFrame框架的平滑重启特性相当强大及稳定,不仅仅支持单一服务单一端口监听管理,同时也支持多服务多端口等复杂场景的监听管理。

package main

import (
    "github.com/gogf/gf/v2/frame/g"
)

func main() {
    s1 := g.Server("s1")
    s1.SetGraceful(true)
    s1.EnableAdmin()
    s1.SetPort(8100, 8200)
    s1.Start()

    s2 := g.Server("s2")
    s2.SetGraceful(true)
    s2.EnableAdmin()
    s2.SetPort(8300, 8400)
    s2.Start()

    g.Wait()
}

以上示例演示的是两个WebServer  s1s2,分别监听8100820083008400。我们随后访问  http://127.0.0.1:8100/debug/admin/reload  平滑重启服务,然后再通过  http://127.0.0.1:8100/debug/admin/shutdown  平滑关闭服务,最终在终端打印出的信息如下:

2018-05-18 11:26:54.729 18111: http server started listening on [:8400]
2018-05-18 11:26:54.729 18111: http server started listening on [:8100]
2018-05-18 11:26:54.729 18111: http server started listening on [:8300]
2018-05-18 11:26:54.729 18111: http server started listening on [:8200]
2018-05-18 11:27:08.203 18111: server reloading
2018-05-18 11:27:08.203 18111: all servers shutdown
2018-05-18 11:27:08.207 18124: http server reloaded listening on [:8300]
2018-05-18 11:27:08.207 18124: http server reloaded listening on [:8400]
2018-05-18 11:27:08.207 18124: http server reloaded listening on [:8200]
2018-05-18 11:27:08.207 18124: http server reloaded listening on [:8100]
2018-05-18 11:27:19.379 18124: server shutting down
2018-05-18 11:27:19.380 18102: all servers shutdown

命令行管理

GoFrame框架除了提供Web方式的管理能力以外,也支持命令行方式来进行管理,由于命令行采用了信号进行管理。

重启服务

使用SIGUSR1信号量实现,使用方式:

        
          kill -SIGUSR1 进程ID
      

关闭服务

使用SIGINT/SIGQUIT/SIGKILL/SIGHUP/SIGTERM其中任意一个信号量来实现,使用方式:

        
          kill -SIGTERM 进程ID
      

其他管理方式

由于GoFrame框架的WebServer采用了单例设计,因此任何地方都可以通过g.Server(名称)来获得对应Server的单例对象,随后通过RestartShutdown方法可以实现对该Server的管理。



Content Menu

  • No labels

23 Comments

  1. 不知是否是我测试的有问题,测试下来发现  sleep的接口  在重启时会丢失掉请求。无论是通过 admin还是kill  都不行。linux和macos都不行。望大佬解惑!

    测试代码很简单  一个空接口sleep 5s。 等待过程中重启,操作之后本次请求直接503

    1. 默认平滑重启特性是关闭了的,你可以打开调试模式看看。

      1. 开启了的 ,重启能成功!就是丢掉了sleep哪一次长耗时的请求,最终接口会吐出503错误码。

        1. xx

          你好,我也是一样的情况,你解决了没?

          1. 没有 准备overseer来替代

    2. ghttp默认超时设置了2秒,所以5秒会超时,我给强哥提了个自定义配置,可以看看https://github.com/gogf/gf/pull/1214/files

  2. linux 下通过  kill -SIGUSR1 进程id 平滑重启,原进程自动kill掉,还是需要手动kill掉, 测试了一下,重启后,原进程id仍存在:

    版本信息:

      Go Version:  go1.15.5
      GF Version:  v1.15.3
      Build Time:  2021-03-01 23:10:15

    1. 我测试也存在这个情况。。你的请求能执行完吗 我这边模拟的,重启过程中请求会丢

    2. graceful设置为true了吗,我这边重启,旧进程会退出

  3. xx

    开启了平滑重启我也不行,和楼上雷辉遇到的情况一样,sleep后重启收不到消息返回

    1. ghttp默认超时设置了2秒,可以看看https://github.com/gogf/gf/pull/1214/files

  4. 命令行的方式是默认直接就支持,还是也需要调用EnableAdmin?

    我没有调用过EnableAdmin,直接给当前进程发送了个SIGUSR1信号,结果看到它fork了一个子进程出来,原来的进程一直不退出。等了半天也没有。然后直接kill,杀不死。最后用kill -s 9 才杀死。

    是不是我的用法有问题?对了,我除了开了web服务外,还有自行开了一些守护检查的协程出来。是不是这个影响了重启?

  5. Restart()方法没有封装,可以直接调用

    ghttp.RestartAllServer(path) 实现
  6. 已经在yaml文件中设置graceful为true,但是启动时会有以下错误:

    2022-03-20 11:22:17.678 [INFO] openapi specification is disabled 
    panic: os.OpenFile failed with name "", flag "1537", perm "438": open : no such file or directory

    goroutine 5 [running]:
    github.com/gogf/gf/v2/os/gproc.receiveTcpListening()
        ~/Golang/projects/Service/vendor/github.com/gogf/gf/v2/os/gproc/gproc_comm_receive.go:71 +0x3e6
    created by github.com/gogf/gf/v2/os/gproc.Receive
        ~/Golang/projects/Service/vendor/github.com/gogf/gf/v2/os/gproc/gproc_comm_receive.go:34 +0x51
    [1]  + 69300 exit 2     ./main


    请各位朋友帮忙看看

    1. 从命令行新建一个2.0项目后,去开启平滑重启,是正常的。现在这个出问题的是从1.16迁移过来的项目。如果把graceful设置为false,就没问题,但现在是要支持平滑重启。

    2.     github.com/gogf/gf/v2 v2.0.0-rc2
          github.com/gogf/gf/v2 v2.0.0-rc3
          github.com/gogf/gf/v2 v2.0.0-rc.0.20220117131058-9345eb5e946f

      后来发现是rc3出的问题,😂,其他2个版本都可以。

  7. 关于在docker部署里做过平滑重启操作的可能性.在docker内测试对进程执行kill -SIGUSR1命令会将容器down掉.

    1. zhl

      这个是因为kill掉原进程后,docker没有前台程序就自动关掉了,可以在docker run的时候加 -d -it 在宿主机启动一个后台的终端,或者启动时加一个 tail -f /dev/null 来维持一个前台程序,也可以直接写在dockerfile里,最后一行启动改为:CMD $WORKDIR/main && tail -f /dev/null

  8. 在supervisor下,该如何使用平滑重启?

    1. server gracefully shutting down by signal: terminated

      =======

      1) webserver的配置打开Graceful:

      graceful: true

      https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#ServerConfig

      2) supervisor program的配置项stopsignal默认就可以, 默认是TERM:

      参考源码:
      net/ghttp/ghttp_server_admin_unix.go:handleProcessSignal


      case syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL, syscall.SIGABRT:
        shutdownWebServers(ctx, sig.String())
         return

      // Shutdown the servers gracefully.
      // Especially from K8S when running server in POD.
      case syscall.SIGTERM:
        shutdownWebServersGracefully(ctx, sig.String())
        return


  9. 作者你好,找了很久的文档,我没有找到优雅退出的方式,请问如何实现。例如,不再接收新的请求,未完成的请求全部完成后再关闭,线程池中的线程完成后再关闭(以上都有超时机制)

    1. 我说我这边之前自行处理的方案,我在应用服务前加了一个nginx,如果服务是分布式最好了,通过nginx进行手动切换,这样会好一些

    2. 优雅退出一般都是结合全局状态和sync.WaitGroup{}.Wait()一起实现的。每个线程都使用sync.WaitGroup{}.Add()添加协程计量数并且在主协程的最后添加sync.WaitGroup{}.Wait()阻塞等待所有协程退出,当接收到kill信号的时候修改全局状态(使用系统信号监听实现),停止接受请求或者有死循环的协程退出死循环并使用sync.WaitGroup{}.Done()减少协程计量数。sync.WaitGroup{}这个结构体应作为一个全局变量,不要重复创建结构体,前面的文字只是为了说明需要用到的方法。

      对于http服务的平滑重启会更加复杂,原理是在当前进程中启动一个与自身一样的进程,进程启动参数有相关系统api可获取,并且在启动的过程中把当前进程的listen文件描述符一并传给新进程,这样就不会中断端口监听,然后旧进程在回复完请求后断开客户端,客户端重连后就会在新进程中执行请求,旧进程处理完所有已接收的请求后退出。tcp长连接的平滑重启暂未去了解,如果要实现不断开连接重启怕不是要把已连接的文件描述符也在处理完单次任务后传递给新进程?