使用Go实现优雅重启服务功能

 更新时间:2019年11月04日 08:58:08   作者:songsong001  
这篇文章主要介绍了如何使用Go来实现优雅重启服务,本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下

暴力的重启服务方案

一般服务器重启可以直接通过 kill 命令杀死进程,然后重新启动一个新的进程即可。但这种方法比较粗暴,有可能导致某些正在处理中的客户端请求失败,如果请求正在写数据,那么还有可能导致数据丢失或者数据不一致等。

那么有什么方式可以优雅的重启服务呢?

优雅的重启服务方案

优雅的重启方式流程如下:

 

从上面的流程可以看出,旧进程必须等待所有的请求连接完成后才会退出,请求不会被强制关闭,所以是个优雅的重启方式。

使用Go实现优雅重启

下面我们使用Go语言来演示怎么实现优雅启动功能,我们先来看看原理图:

 

从原理图可以知道,重启时首先通过发送 SIGHUP信号 给服务进程,服务进程收到  SIGHUP信号 后会  fork 一个新进程来处理新的请求,然后新进程会发送  SIGTERM信号 给旧服务进程(父进程),旧服务进程接收到  SIGTERM信号 后会关闭监听的  socket句柄 (停止接收新请求),并且等待未处理完成的请求完成后再退出进程。

下面通过代码来说明这个流程,代码主要参考 endless 这个库,有兴趣可以查看其源码。

首先我们定义一个名为 endlessServer 的结构并且继承  http.Server 结构:

type endlessServer struct {
  http.Server
  EndlessListener net.Listener
  wg        sync.WaitGroup
  sigChan     chan os.Signal
  isChild     bool
  state      uint8
  lock       *sync.RWMutex
}

Go的继承很简单,就是在定义结构时把要继承的结构嵌入到里面就可以了。

这里说明一下 endlessServer 各个成员的作用吧:

  • Server:用于继承 http.Server 结构
  • EndlessListener:监听客户端请求的 Listener
  • wg:用于记录还有多少客户端请求没有完成
  • sigChan:用于接收信号的管道
  • isChild:用于重启时标志本进程是否是为一个新进程
  • state:当前进程的状态
  • lock:用于锁定一些资源

定义一个创建 endlessServer 结构的函数:

func NewServer(addr string, handler http.Handler) (srv *endlessServer) {
  isChild := os.Getenv("ENDLESS_CONTINUE") != ""
  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 = 0
  srv.Server.WriteTimeout = 0
  srv.Server.MaxHeaderBytes = 0
  srv.Server.Handler = handler
  return
}

NewServer() 函数的实现比较简单,就是创建一个  endlessServer 结构,然后初始化其各个成员。要注意的是,是否为新进程是通过读取环境变量  ENDLESS_CONTINUE 来判断的,如果定义了  ENDLESS_CONTINUE 环境变量,就是说当前进程是新的服务进程。

用过Go语言的HTTP包的同学应该知道,要进行监听客户端请求的话必须调用其 ListenAndServe() 函数,所以我们要定义这个函数:

func ListenAndServe(addr string, handler http.Handler) error {
  server := NewServer(addr, handler)
  return server.ListenAndServe()
}

函数的实现很简单,就是先调用 NewServer() 函数创建一个  endlessServer 结构,然后调用其  ListenAndServe() 方法。所以我们要为  endlessServer 结构定义一个  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
  }
  srv.EndlessListener = newEndlessListener(l, srv)
  if srv.isChild {
    syscall.Kill(syscall.Getppid(), syscall.SIGTERM)
  }
  return srv.Serve()
}

ListenAndServe() 方法首先会创建一个协程处理  handleSignals() 方法,这个方法主要是处理信号,下面会介绍。然后调用  getListener() 方法获取一个类型为  net.Listener 的对象,然后调用  newEndlessListener() 函数创建一个类型为  endlessListener 的对象。再通过判断当前进程是否为新的处理进程,如果是就调用  syscall.Kill() 方法发送一个  SIGTERM信号 给父进程(旧的服务处理进程),最后调用  Serve() 方法开始处理客户端连接。

我们先来看看处理信号的 handleSignal() 方法:

func (srv *endlessServer) handleSignals() {
  var sig os.Signal
  signal.Notify(
    srv.sigChan,
    syscall.SIGHUP,
    syscall.SIGINT,
    syscall.SIGTERM,
  )
  pid := syscall.Getpid()
  for {
    sig = <-srv.sigChan
    srv.signalHooks(PRE_SIGNAL, sig)
    switch sig {
    case syscall.SIGHUP:
      err := srv.fork()
      if err != nil {
        log.Println("Fork err:", err)
      }
    case syscall.SIGINT:
      srv.shutdown()
    case syscall.SIGTERM:
      srv.shutdown()
    default:
      log.Printf("Received %v: nothing i care about...\n", sig)
    }
  }
}

handleSignal() 方法主要监听3种信号, syscall.SIGHUP 、 syscall.SIGINT 和  syscall.SIGTERM 。 syscall.SIGHUP 信号为重启信号,而  syscall.SIGINT 信号为关闭服务信号,而  syscall.SIGTERM 信号主要是新的服务进程发送给旧的服务进程,告诉其关闭监听处理客户端的socket。当收到  syscall.SIGHUP 信号时,需要调用  fork() 方法来创建一个新的服务进程,而收到  syscall.SIGINT 和  syscall.SIGTERM 信号主要调用  shutdown() 方法来关闭当前进程。

再来看看创建新服务进程的 fork() 方法:

func (srv *endlessServer) fork() (err error) {
  files := []*os.File{
    srv.EndlessListener.(*endlessListener).File(),
  }
  env := append(
    os.Environ(),
    "ENDLESS_CONTINUE=1",
  )
  path := os.Args[0]
  var args []string
  if len(os.Args) > 1 {
    args = os.Args[1:]
  }
  cmd := exec.Command(path, args...)
  cmd.Stdout = os.Stdout
  cmd.Stderr = os.Stderr
  cmd.ExtraFiles = files
  cmd.Env = env
  err = cmd.Start()
  if err != nil {
    log.Fatalf("Restart: Failed to launch, error: %v", err)
  }
  return
}

fork() 方法也比较简单,主要是使用  exec 包的  Command() 方法来创建一个  Cmd 对象,然后调用其  Start() 方法来启动一个新进。要注意的是,创建新进程前需要设置环境变量  ENDLESS_CONTINUE ,这是告诉新进程需要发送  syscall.SIGTERM 信号给父进程。还有就是通过  Cmd 对象的  ExtraFiles 成员把监听客户端连接的socket句柄传递给新服务处理进程了。

再来看看关闭服务进程的 shutdown() 方法:

func (srv *endlessServer) shutdown() {
  err := srv.EndlessListener.Close()
}

这个方法很简单,就是调用 net.Listener 对象的  Close() 方法来关闭监听客户端请求的socket。关闭监听客户端请求的socket后,主循环会退出处理,然后会退出进程。

接着我们来看看接收客户端请求的 endlessListener.Accept() 方法:

func (el *endlessListener) Accept() (c net.Conn, err error) {
  tc, err := el.Listener.(*net.TCPListener).AcceptTCP()
  if err != nil {
    return
  }
  tc.SetKeepAlive(true)         // see http.tcpKeepAliveListener
  tc.SetKeepAlivePeriod(3 * time.Minute) // see http.tcpKeepAliveListener
  c = endlessConn{
    Conn:  tc,
    server: el.server,
  }
  el.server.wg.Add(1)
  return
}

主要要注意的是,函数最后会调用 el.server.wg.Add(1) 这行代码来增加客户端请求的计数器,这是优雅重启的关键。因为在  endlessServer.Serve() 方法中会等待所有客户端请求处理完毕才会退出,我们来看看  endlessServer.Serve() 方法的实现:

func (srv *endlessServer) Serve() (err error) {
  err = srv.Server.Serve(srv.EndlessListener)
  srv.wg.Wait()
  return
}

可以看到, endlessServer.Serve() 方法最后会调用  srv.wg.Wait() 这行代码来等待所有客户端请求完成。那么客户端连接计数器什么时候会减少呢?在  endlessConn.Close() 方法中可以看到计数器减少的操作:

func (w endlessConn) Close() error {
  err := w.Conn.Close()
  if err == nil {
    w.server.wg.Done()
  }
  return err
}

可以看到, endlessConn.Close() 方法最后会调用  w.server.wg.Done() 这 行代码来减少客户端请求计数器。 至此,优雅重启服务的实现就完成。

当然,本篇文章主要介绍的是优雅重启的原理,完成的源码实现还是要查看 endless 这个库。

总结

以上所述是小编给大家介绍的使用Go实现优雅重启服务功能,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对脚本之家网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

相关文章

  • go gin中间件关于 c.next()、c.abort()和return的使用小结

    go gin中间件关于 c.next()、c.abort()和return的使用小结

    中间件的执行顺序是按照注册顺序执行的,中间件可以通过 c.abort() + retrurn 来中止当前中间件,后续中间件和处理器的处理流程, 这篇文章给大家介绍go gin中间件关于 c.next()、c.abort()和return的使用小结,感兴趣的朋友跟随小编一起看看吧
    2024-03-03
  • 详解Golang中创建error的方式总结与应用场景

    详解Golang中创建error的方式总结与应用场景

    Golang中创建error的方式包括errors.New、fmt.Errorf、自定义实现了error接口的类型等,本文主要为大家介绍了这些方式的具体应用场景,需要的可以参考一下
    2023-07-07
  • Golang time.Sleep()用法及示例讲解

    Golang time.Sleep()用法及示例讲解

    Go语言中的Sleep()函数用于在至少规定的持续时间d内停止最新的go-routine,这篇文章主要介绍了Golang time.Sleep()用法及示例讲解,需要的朋友可以参考下
    2023-02-02
  • Go高级特性探究之recover捕获panic详解

    Go高级特性探究之recover捕获panic详解

    在Go语言中,当程序出现panic(即运行时错误)时,程序会立即停止当前的执行流程,而recover函数的作用就是捕获这个panic,下面就来看看具体是怎么操作的吧
    2023-06-06
  • 详解go-zero如何实现令牌桶限流

    详解go-zero如何实现令牌桶限流

    令牌桶算法既能够将所有的请求平均分布到时间区间内,又能接受服务器能够承受范围内的突发请求,因此是目前使用较为广泛的一种限流算法,本文就来看看go-zero如何实现令牌桶限流的吧
    2023-08-08
  • Golang实现简易的rpc调用

    Golang实现简易的rpc调用

    RPC指(Remote Procedure Call Protocol)远程过程调用协议。本文将实现利用Golang进行rpc调用(只实现一个rpc框架基本的功能,不对性能做保证),需要的可以参考一下
    2023-03-03
  • Golang error使用场景介绍

    Golang error使用场景介绍

    我们在使用Golang时,不可避免会遇到异常情况的处理,与Java、Python等语言不同的是,Go中并没有try...catch...这样的语句块,这个时候我们如何才能更好的处理异常呢?本文来教你正确方法
    2023-03-03
  • golang踩坑实战之channel的正确使用方式

    golang踩坑实战之channel的正确使用方式

    Golang channel是Go语言中一个非常重要的特性,除了用来处理并发编程的任务中,它还可以用来进行消息传递和事件通知,这篇文章主要给大家介绍了关于golang踩坑实战之channel的正确使用方式,需要的朋友可以参考下
    2023-06-06
  • 解决goxorm无法更新值为默认值的问题

    解决goxorm无法更新值为默认值的问题

    这篇文章主要介绍了解决goxorm无法更新值为默认值的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • 一文详解Golang中的基础语法

    一文详解Golang中的基础语法

    这篇文章主要为大家详细介绍了Golang中基础语法的相关知识,文中的示例代码讲解详细,对我们学习Golang有一定的帮助,感兴趣的可以了解一下
    2023-03-03

最新评论