Code前端首页关于Code前端联系我们

以Golang Gin框架为例,讲一下平滑关机背后的处理逻辑

terry 2年前 (2023-09-24) 阅读数 63 #后端开发

关闭软件可以分为平滑关机(软关机)和硬关机。就像我们关闭电脑时一样,有时当电脑死机时,我们会立即按住电源按钮,直到电脑关闭。这是硬关机。通过电脑上的菜单选择“关机”就是软关机(平滑关机)。在软关机过程中,你应该注意到需要很长时间,并且时不时会出现弹出窗口询问是否要退出。

在我们自己编写的Web应用中,其实是需要软关闭的。今天我就以Golang中的Gin框架为例,讲一下平滑关闭背后的处理逻辑。 golang gin框架为例,聊聊平滑关闭背后的处理逻辑

1。为什么平稳关闭如此重要?

硬关机是指当程序收到关机信号时,立即停止正在做的事情。例如,如果我们强制关机时代码没有保存,那么代码就会丢失。平滑关闭有以下优点:

首先,平滑关闭可以及时释放资源。其次,顺利完成可以保证交易的完整性。

例如,在Web服务中,请求在返回之前就被关闭,这会影响体验。平滑关闭,请求处理完成后即可关闭连接。因此,平滑关闭本质上就是当程序收到关闭信号时,等待程序完成正在做的事情,释放所有资源,然后关闭服务。

2。 Web 服务如何接收和处理请求?

无论是服务的正常关闭还是正常关闭,本质上都是资源关闭服务。因此,首先需要了解Web服务是如何启动的,以及启动后如何处理http请求的。这样,当你关闭时,你可以知道哪些资源将被关闭以及如何关闭它们。

我们以Gin框架为例来说明处理http请求的过程。

    • 首先构建一个服务器对象
    • 根据传入的网络地址创建一个网络监听器。实际上就建立了一个socket。
    • 将监听器添加到服务器对象的资源池中,以监视代表服务器使用的监听器资源。
    • listner 开始侦听相应网络地址(套接字)上的请求。
    • 当用户发起http请求时,accept函数可以对其进行监听。
    • 为新收到的请求创建 TCP 连接
    • 将新的 TCP 连接打包成 conn 对象,并将 conn 对象添加到服务器的关羽 conn 资源池中。这样,服务器就可以跟踪当前有多少个请求连接正在处理请求。
    • 启动新协程,异步处理连接
      • 读取请求内容
      • 执行具体处理逻辑
      • 输出响应同时从资源池中释放相应的conn资源。

  • 继续接受以监控后续 HTTP 请求。
golang gin框架为例,聊聊平滑关闭背后的处理逻辑

通过上面的流程图,我们实际上可以将Web服务器处理http请求的整个过程分为两个部分:创建网络监听器lister(socket)阶段和监听并处理HTTP请求。相应地,这两个阶段对应使用的资源就是网络监听器Lister和每个HTTP请求连接Conn。也就是上图中服务器中的两个资源池。

两种资源有不同的状态。我们简单看一下两种资源各自的状态和转换。

  • 监听器资源状态

监听器的作用是监听网络连接。所以资源有两种状态:正常和关闭。

  • Conn资源状态

conn本质上是一个TCP连接,但是为了方便跟踪当前监控的连接,服务器对象将TCP连接包装在conn中,并为conn定义了以下状态:新连接(New)、活动状态活动(Active)、关闭状态(Closed)、空闲状态(Idle)和劫持状态(hijacked)。各状态之间的转换关系如下: golang gin框架为例,聊聊平滑关闭背后的处理逻辑

启动阶段,资源建立。然后,在服务器关闭阶段,主要目的就是释放这些资源。那么这些资源如何释放是我们接下来讨论的重点。

3。关闭网络服务是什么意思?

在Web框架中,服务器的关闭函数对应的功能就是立即关闭服务器服务。在Gin框架中,对应的代码如下: golang gin框架为例,聊聊平滑关闭背后的处理逻辑

从代码中可以看到,基本上第一件事就是给服务器对象设置关闭标志;然后关闭doneChan;关闭所有监听资源,停止接收新连接;最后循环conn资源,一一关闭。 golang gin框架为例,聊聊平滑关闭背后的处理逻辑

这里需要注意的一点是,当你关闭conn资源时,无论conn当前处于什么状态,它都会立即关闭。也就是说,如果一个conn处于active状态,这意味着该请求还没有被处理,那么处理立即终止。对于客户端的性能,他收到错误“连接被拒绝,无法访问网站”。

让我们实验一下。以下代码注册“/home”路径。在处理函数中,我们等待20秒来模拟关闭时我们的请求处理未完成的场景。那么程序终止信号的下一步就是通过signal.Notify函数(即按Ctrl+C)退出通道上的os.Interrupt。当通过在终端上按 Ctrl + C 将中断信号发送到 Quit 通道时,将执行 Server.Close() 函数。下面的代码:

package main

import (
 "context"
 "log"
 "net/http"
 "os"
 "os/signal"
 "time"

 "github.com/gin-gonic/gin"
)

func main() {
 router := gin.Default()
 router.GET("/home", func(c *gin.Context) {
  time.Sleep(5 * time.Second)
  c.String(http.StatusOK, "Welcome Gin Server")
 })

 server := &http.Server{
  Addr:    ":8080",
  Handler: router,
 }

 quit := make(chan os.Signal)
 signal.Notify(quit, os.Interrupt)
 server.RegisterOnShutdown(func(){
  log.Println("start execute out shutown")
 })

 go func() {
  if err := server.ListenAndServe(); err != nil {
   if err == http.ErrServerClosed {
    log.Println("Server closed under request")
   } else {
    log.Fatal("Server closed unexpect")
   }
  }
 }()

 <-quit
 log.Println("receive interrupt signal")
 //ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 //defer cancel()

 if err := server.Close(); err != nil {
  log.Fatal("Server Close:", err)
 }
 log.Println("Server exiting")
}

好了,我们来总结一下立即关闭的特点: 先关闭服务器对象;然后关闭窃听器并停止接收新连接;最后关闭所有已建立的连接,无论连接是否存在。请求将立即关闭。请注意这里的关闭顺序是从大到小范围:先关闭大范围(服务器和监听器),最后关闭具体处理连接(conn)。

到目前为止,我们发现立即关闭的缺点是正在处理的请求也会立即关闭。而且,服务器所依赖的外部资源(如数据库连接等)并没有被释放。接下来我们看看平滑关闭是如何解决这两个问题的。

4。 web服务平滑关闭是关闭什么的?

解决了以上问题,平滑关闭就出现了。 Gin框架中对应的函数就是shutdown函数。代码如下: golang gin框架为例,聊聊平滑关闭背后的处理逻辑

从上面的代码中,首先设置了服务器对象的关闭标志;然后关闭所有监听器资源以停止接收新连接;然后执行外部注册函数关闭服务器外部依赖资源(如数据库等)。最后,循环关闭空闲的conn资源。这也是与上面所说的闭包的本质区别。 golang gin框架为例,聊聊平滑关闭背后的处理逻辑

image.png

同样,我们以如下代码为例来运行实验:

package main

import (
 "context"
 "log"
 "net/http"
 "os"
 "os/signal"
 "time"

 "github.com/gin-gonic/gin"
)

func main() {
 router := gin.Default()
 router.GET("/home", func(c *gin.Context) {
  time.Sleep(5 * time.Second)
  c.String(http.StatusOK, "Welcome Gin Server")
 })

 server := &http.Server{
  Addr:    ":8080",
  Handler: router,
 }

 quit := make(chan os.Signal)
 signal.Notify(quit, os.Interrupt)
 server.RegisterOnShutdown(func(){
  log.Println("start execute out shutown")
 })

 go func() {
  if err := server.ListenAndServe(); err != nil {
   if err == http.ErrServerClosed {
    log.Println("Server closed under request")
   } else {
    log.Fatal("Server closed unexpect")
   }
  }
 }()

 <-quit
 log.Println("receive interrupt signal")
 //ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 //defer cancel()

 if err := server.Shutdown(context.Background()); err != nil {
  log.Fatal("Server Close:", err)
 }
 log.Println("Server exiting")
}

启动服务,然后输入http://localhost:8080/home,在终端程序中按Ctrl+C 。你在浏览器中看到的是页面仍在输出。

好啦,我们来总结一下平滑关闭的特点:等到所有连接都处理完才关闭

5。 Web服务如何顺利关闭?

那么,Golang 是如何监听关闭事件并联动平滑关闭的呢?在上面的示例代码中,我们还可以看到它是信号。 信号是进程间通信的一种方式。在Golang中,通过以下函数来监听信号:

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

当监听到相应的信号时,会向退出通道写入一条消息。此时,Quit不再阻塞,然后调用服务的关闭函数。

这里必须注意两点:

  • 启动服务必须放在协程中,所以
  • 对于监听信号的通道退出,必须使用缓冲通道。因为signal.Notify函数中的信号监听并不是阻塞的。这意味着什么?即当监听到对应的信号时,如果没有成功发送到通道,则该信号被丢弃。下面给出一个例子来说明使用缓冲通道和无缓冲通道之间的区别。
package main

import (
 "fmt"
 "os"
 "os/signal"
)

func main() {
 // Set up channel on which to send signal notifications.
 // We must use a buffered channel or risk missing the signal
 // if we're not ready to receive when the signal is sent.
 c := make(chan os.Signal)
 signal.Notify(c, os.Interrupt)
        
    time.Sleep(5*time.Second)
 // Block until a signal is received.
 s := <-c
 fmt.Println("Got signal:", s)
}

这是一个无缓冲通道。如果在前5秒内继续按Ctrl+C,当5秒过去,程序转到第18行时,程序将收不到输出信号。原因是Signal.Notify函数监听到中断信号后,由于向通道c传输不成功,已经丢弃了该信号。

总结

平滑关机的本质是释放闲置资源。连接终止和关闭的是信号机制。信号是进程间通信的手段之一,其基本实现是硬件中断原理。如果想进一步了解信号和中断机制,建议阅读王爽的《内部中断和外部中断》的《深入理解计算机系统》第8章和《汇编语言》第12章到第15章。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门