grace(热重启)剖析

本文介绍了Beego框架中的graceful(热重启)功能,如何在其他项目中独立使用,以及如何配置热启动。通过分析源码,详细阐述了grace如何实现优雅关闭、启动新进程以及何时关闭父进程的逻辑。热重启主要依靠信号监听,当收到HUP信号时,启动子进程并关闭父进程,确保所有请求在关闭前得到处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Beego中的热重启是以一个单独的模块而存在的,你可以在其他项目单独使用,例如:

import(
   "log"
    "net/http"
    "os"
    "strconv"

   "github.com/astaxie/beego/grace"
 )

  func handler(w http.ResponseWriter, r *http.Request) {
      w.Write([]byte("WORLD!"))
      w.Write([]byte("ospid:" + strconv.Itoa(os.Getpid())))
  }

  func main() {
      mux := http.NewServeMux()
      mux.HandleFunc("/hello", handler)

      err := grace.ListenAndServe("localhost:8080", mux)
      if err != nil {
           log.Println(err)
        }
      log.Println("Server on 8080 stopped")
         os.Exit(0)
}

打开两个终端

一个终端输入:ps -ef|grep 应用名

一个终端输入请求:curl "http://127.0.0.1:8080/hello"

热升级

kill -HUP 进程 ID

打开一个终端输入请求:curl "http://127.0.0.1:8080/hello?sleep=0"

 

在使用beego的框架时,你可以在配置文件中加上如下配置项:

#Listen config(system) Graceful = true

则会在beego项目中配置了热启动,只需要 kill -HUP pid 这样的命令就可以热升级项目

 

具体 beego 中的 grace 的原理是什么呢?我们通过查看源码来看看:

首先我们看下 beego.Run()函数:

// Run beego application.
// 省略了不少代码,我们只关注 grace相关的 http
func (app *App) Run(mws ...MiddleWare) {
   addr := BConfig.Listen.HTTPAddr

   if BConfig.Listen.HTTPPort != 0 {
      addr = fmt.Sprintf("%s:%d", BConfig.Listen.HTTPAddr, BConfig.Listen.HTTPPort)
   }

   var (
      err        error
      l          net.Listener
      endRunning = make(chan bool, 1)
   )

   // run cgi server
   if BConfig.Listen.EnableFcgi {
   }

   app.Server.ReadTimeout = time.Duration(BConfig.Listen.ServerTimeOut) * time.Second
   app.Server.WriteTimeout = time.Duration(BConfig.Listen.ServerTimeOut) * time.Second
   app.Server.ErrorLog = logs.GetLogger("HTTP")

   // run graceful mode
   // 
   if BConfig.Listen.Graceful {  // 在配置文件中开启grace 热重启
      httpsAddr := BConfig.Listen.HTTPSAddr
      app.Server.Addr = httpsAddr
      if BConfig.Listen.EnableHTTP {
         go func() {
            server := grace.NewServer(addr, app.Handlers) // 调 grace 模块里面的server函数,初始化一些成员变量
            server.Server.ReadTimeout = app.Server.ReadTimeout // 设置连接的读超时时间
            server.Server.WriteTimeout = app.Server.WriteTimeout // 设置连接的写超时时间
            if BConfig.Listen.ListenTCP4 {
               server.Network = "tcp4"
            }
            if err := server.ListenAndServe(); err != nil {
               logs.Critical("ListenAndServe: ", err, fmt.Sprintf("%d", os.Getpid()))
               time.Sleep(100 * time.Microsecond)
               endRunning <- true
            }
         }()
      }
      <-endRunning
      return
   }
<-endRunning
}

我们看下 server.ListenAndServe() 函数:

// ListenAndServe listens on the TCP network address srv.Addr and then calls Serve
// to handle requests on incoming connections. If srv.Addr is blank, ":http" is
// used.
func (srv *Server) ListenAndServe() (err error) {
   addr := srv.Addr
   if addr == "" {
      addr = ":http"
   }

   go srv.handleSignals() //  启动信号监听器

   l, err := srv.getListener(addr)  // 设置 listen监听文件描述符
   if err != nil {
      log.Println(err)
      return err
   }

   srv.GraceListener = newGraceListener(l, srv)

   if srv.isChild { // 如果是使用 grace 热重启
      process, err := os.FindProcess(os.Getppid()) //找到父进程id
      if err != nil {
         log.Println(err)
         return err
      }
      err = process.Signal(syscall.SIGTERM) // 给父进程发送 SIGTERM信号
      if err != nil {
         return err
      }
   }

   log.Println(os.Getpid(), srv.Addr)
   return srv.Serve()  // 启动 Server
}

下面继续看 srv.Server() 函数:

// Serve accepts incoming connections on the Listener l,
// creating a new service goroutine for each.
// The service goroutines read requests and then call srv.Handler to reply to them.
func (srv *Server) Serve() (err error) {
   srv.state = StateRunning
   err = srv.Server.Serve(srv.GraceListener)
   log.Println(syscall.Getpid(), "Waiting for connections to finish...")
   srv.wg.Wait()  // 等待直到该进程的所有请求都断开,才退出,这个对应每个连接来了都会使用 srv.wg.Add(1), 然后在连接关闭的时候调用 srv.wg.Done()
   srv.state = StateTerminate
   return
}

再看下信号监听处理程序那:

// handleSignals listens for os Signals and calls any hooked in function that the
// user had registered with the signal.
func (srv *Server) handleSignals() {
   var sig os.Signal

   signal.Notify(
      srv.sigChan,
      hookableSignals...,
   )

   pid := syscall.Getpid() // 获取当前的进程id
   for {
      sig = <-srv.sigChan
      srv.signalHooks(PreSignal, sig)
      switch sig {
      case syscall.SIGHUP: //这里就是 grace 热重启的子进程处理
         log.Println(pid, "Received SIGHUP. forking.")
         err := srv.fork()
         if err != nil {
            log.Println("Fork err:", err)
         }
      case syscall.SIGINT:
         log.Println(pid, "Received SIGINT.")
         srv.shutdown() //关闭进程
      case syscall.SIGTERM:
         log.Println(pid, "Received SIGTERM.")
         srv.shutdown()
      default:
         log.Printf("Received %v: nothing i care about...\n", sig)
      }
      srv.signalHooks(PostSignal, sig)
   }
}

svr.fork 是启动一个新的子进程:

func (srv *Server) fork() (err error) {
   regLock.Lock()
   defer regLock.Unlock()
   if runningServersForked {
      return
   }
   runningServersForked = true

   var files = make([]*os.File, len(runningServers))
   var orderArgs = make([]string, len(runningServers))
   for _, srvPtr := range runningServers {
      switch srvPtr.GraceListener.(type) {
      case *graceListener:
         files[socketPtrOffsetMap[srvPtr.Server.Addr]] = srvPtr.GraceListener.(*graceListener).File() // 获取监听的文件描述符
      default:
         files[socketPtrOffsetMap[srvPtr.Server.Addr]] = srvPtr.tlsInnerListener.File()
      }
      orderArgs[socketPtrOffsetMap[srvPtr.Server.Addr]] = srvPtr.Server.Addr
   }
 ....
   args = append(args, "-graceful") // 添加上 -graceful 参数,标识为是重新启动了新的子进程,为了设置 svr.isChild() 参数,用于处理一些逻辑
   if len(runningServers) > 1 {
      args = append(args, fmt.Sprintf(`-socketorder=%s`, strings.Join(orderArgs, ",")))
      log.Println(args)
   }
   cmd := exec.Command(path, args...) 
   cmd.Stdout = os.Stdout
   cmd.Stderr = os.Stderr
   cmd.ExtraFiles = files  // 继承父进程监听的文件描述符()
   err = cmd.Start() // 启动一个新的子进程
   if err != nil {
      log.Fatalf("Restart: Failed to launch, error: %v", err)
   }

   return
}

总结:

(1)如何实现优雅关闭

grace 在每次创建一个连接的时候使用 wg.Add(1), 然后关闭连接的时候使用 wg.Done(), 使用 wg.Wait() 来保证处理完用户的请求之后再关闭。

(2)如何启动一个新的进程呢?

监听 HUP 信号,当接收到 HUP 信号的时候,重启启动一个新的子进程来处理请求(继承父进程的监听文件描述符)。

(3)何时关闭父进程

子进程启动的时候,获取父进程的id,并给父进程发送 TREM 信号,然后父进程设置 status 为关闭状态并关闭父进程监听的文件描述符,当父进程所有的连接都断开了,父进程就会退出程序。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值