6 Gin 实现优雅关闭
1. 什么是优雅关闭
1.1 现象
假如,我们正在请求一个接口,获取用户的个人信息,这个接口需要1秒钟的时间进行数据查询,回溯,拼接等逻辑,然后再返回给我们。在这一秒钟内,服务因为某些原因,出BUG了,Panic了,那么服务可能会爆出500的错误。如果是被人为的关闭了,会发生什么?应该发生什么?
相关信息
这是架构领域的一道经典面试题。解决方案就是:优雅关闭。
1.2. 思路
既然已经摸清楚了问题:请求还没有结束,程序整体已经退出了。那么解决方法就是,程序退出时,先看下目前还有没有正在进行中的请求,延缓几秒钟,等请求都结束了,再退出。看下流程图:

这个方案可以解决我们现有问题,但他不够完美,它仍然存在安全隐患。我们刚才说了,它主要解决了关闭时有正在进行的请求,如果在延迟N秒的时候,又来了新的请求,怎么办?
如果这种情况一直延续,每次都延迟N秒,那我们的服务还怎么正常关闭。因此,还需要进一步优化方案:
相关信息
在延迟N秒期间,停止新进请求的承接,直接返回500。
2. 实现方法
鉴于,目前的GO版本都已经到1.20以上了。我们直接用Gin的官方文档提供的方法来实现,gin框架的仓库里提供了两种优雅关闭的方案,大家可以参考:
examples/graceful-shutdown/graceful-shutdown at master · gin-gonic/examples
提示
我们这边会在源码的基础上,增加一些必要的注释,帮助大家快速理解。
2.1. 使用Context
重点,需要认真看一下,最好能在自己的项目中实现一下。另外,最好能举一反三,思考下其他框架是如何实现优雅关闭的。
// build go1.16
package main
import (
"context"
"log"
"net/http"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
func main() {
// 创建一个监听操作系统信号的Context
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
router := gin.Default()
router.GET("/", func(c *gin.Context) {
time.Sleep(10 * time.Second)
c.String(http.StatusOK, "Welcome Gin Server")
})
//这里启动的是官方包的server,把gin的组件塞进去。
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
//以协程的方式拉起服务,这样不会阻塞后续的优雅退出
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
//监听操作系统的关闭信号
<-ctx.Done()
// Restore default behavior on the interrupt signal and notify user of shutdown.
stop()
log.Println("shutting down gracefully, press Ctrl+C again to force")
//起一个5秒超时的Context,传递给srv,它可以实现在5秒之后关闭服务。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown: ", err)
}
log.Println("Server exiting")
}
2.2. 不使用Context
这是1.8版本之前的方案,只了解一下即可。基本上不会再使用了。
警告
不用花太多时间研究这个版本,了解下即可。
//go:build go1.8
// +build go1.8
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
time.Sleep(5 * time.Second)
c.String(http.StatusOK, "Welcome Gin Server")
})
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
// Initializing the server in a goroutine so that
// it won't block the graceful shutdown handling below
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal, 1)
// kill (no param) default send syscall.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall.SIGKILL but can't be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
// The context is used to inform the server it has 5 seconds to finish
// the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown: ", err)
}
log.Println("Server exiting")
}
3. 线上环境真实方案
优雅关闭是每个线上服务都应该具备的基本功能,毕竟每次服务变动,发版,优化,修改bug等,都需要关闭旧的服务,或者重启服务。通常情况下,会有专业的运维开发工程师和基建部门共同提供这个功能。我们以线上正在运行V1.1版本,我们准备发布V1.2版本为例:
- 先拉起一批新的服务器,发布V1.2版本。
- 网关服务会把新的流量分批从旧服务迁到新服务。
- 旧服务开启优雅关闭,等待所有请求结束后再关闭。
- 最后,回收旧服务的资源。
提示
注意:如果这里没有优雅关闭,可以等到旧服务完全没有流量再关闭;如果没有网关服务进行流量转发必然会导致服务出现短暂不可用。