在生产环境中运行的服务,直接强制终止可能会导致一些问题:正在处理的请求被中断、数据库连接没有正确释放、正在写入的文件可能损坏。优雅关闭就是让服务在收到停止信号后,先完成手头的工作,再安全退出。
Go 1.8+ 提供了 Shutdown 方法,Gin 可以直接使用。
假设你的服务正在处理一个支付请求,用户已经扣款成功,正在写入订单记录。这时候如果直接 kill 掉进程,订单可能写入失败,但用户钱已经扣了——这就是生产事故。
优雅关闭要解决的问题:
Go 1.8 之后,http.Server 内置了 Shutdown 方法,配合 context 可以实现优雅关闭:
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,
}
// 启动服务器(非阻塞)
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// 等待中断信号
quit := make(chan os.Signal, 1)
// SIGINT: Ctrl+C, SIGTERM: kill 命令
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
// 给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")
}
这段代码的关键点:
signal.Notify 捕获 SIGINT 和 SIGTERM 信号srv.Shutdown 开始优雅关闭调用 Shutdown 后,服务器会:
需要注意的是,Shutdown 不会关闭 WebSocket 连接或长连接,这些需要单独处理。
实际项目中,优雅关闭通常还需要处理数据库连接、Redis 连接等资源:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var db *gorm.DB
func initDB() error {
dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
var err error
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
return err
}
func closeDB() error {
sqlDB, err := db.DB()
if err != nil {
return err
}
return sqlDB.Close()
}
func main() {
// 初始化数据库
if err := initDB(); err != nil {
log.Fatalf("Failed to connect database: %v", err)
}
router := gin.Default()
router.GET("/users", func(c *gin.Context) {
// 模拟数据库查询
time.Sleep(2 * time.Second)
c.JSON(http.StatusOK, gin.H{"users": []string{"Alice", "Bob"}})
})
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
go func() {
log.Println("Server starting on :8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// 优雅关闭
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 关闭 HTTP 服务器
if err := srv.Shutdown(ctx); err != nil {
log.Printf("Server shutdown error: %v", err)
}
// 关闭数据库连接
if err := closeDB(); err != nil {
log.Printf("Database close error: %v", err)
}
log.Println("Server exited properly")
}
如果项目中有多个需要关闭的资源,可以用通道来协调:
func main() {
// ... 初始化代码 ...
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 并发关闭多个资源
done := make(chan struct{})
go func() {
var wg sync.WaitGroup
// 关闭 HTTP 服务器
wg.Add(1)
go func() {
defer wg.Done()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("HTTP server shutdown error: %v", err)
}
}()
// 关闭数据库
wg.Add(1)
go func() {
defer wg.Done()
if err := closeDB(); err != nil {
log.Printf("Database close error: %v", err)
}
}()
// 关闭 Redis
wg.Add(1)
go func() {
defer wg.Done()
if err := closeRedis(); err != nil {
log.Printf("Redis close error: %v", err)
}
}()
wg.Wait()
close(done)
}()
select {
case <-done:
log.Println("All resources closed")
case <-ctx.Done():
log.Println("Shutdown timeout, forcing exit")
}
}
可以写一个中间件,在服务关闭期间拒绝新请求:
var isShuttingDown atomic.Bool
func shutdownMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if isShuttingDown.Load() {
c.JSON(http.StatusServiceUnavailable, gin.H{
"error": "Server is shutting down",
})
c.Abort()
return
}
c.Next()
}
}
func main() {
router := gin.Default()
router.Use(shutdownMiddleware())
// ... 其他代码 ...
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// 标记服务正在关闭
isShuttingDown.Store(true)
// ... 继续关闭流程 ...
}
这样,在关闭期间新进来的请求会立即收到 503 响应,而不是等待超时。
| 环境 | 信号 | 触发方式 |
|---|---|---|
| 本地开发 | SIGINT | Ctrl+C |
| Docker | SIGTERM | docker stop |
| Kubernetes | SIGTERM | 删除 Pod |
| Systemd | SIGTERM | systemctl stop |
Docker 和 Kubernetes 默认会给容器 10 秒时间优雅关闭,超时后会发送 SIGKILL 强制终止。可以通过配置调整这个时间。
优雅关闭是生产环境必备的功能,核心是:
http.Server.Shutdown 等待请求完成这样做可以保证服务停止时不丢失请求、不损坏数据,用户体验更好,运维也更安心。