跳到主要内容

实现优雅关闭

问题

如何让 Go 服务在收到终止信号时优雅关闭,确保正在处理的请求不被中断?

答案

核心原理

HTTP Server 优雅关闭

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
time.Sleep(3 * time.Second) // 模拟长请求
c.JSON(200, gin.H{"message": "pong"})
})

srv := &http.Server{
Addr: ":8080",
Handler: r,
}

// 启动服务器(非阻塞)
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Listen: %s", err)
}
}()

// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("收到关闭信号,开始优雅关闭...")

// 给正在处理的请求最多 30 秒完成
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("服务器强制关闭: %v", err)
}

log.Println("服务器已安全退出")
}

使用 signal.NotifyContext(Go 1.16+)

func main() {
// 更简洁的写法
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

srv := &http.Server{Addr: ":8080", Handler: setupRouter()}
go srv.ListenAndServe()

<-ctx.Done()
log.Println("正在关闭...")

shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(shutdownCtx)
}

多组件优雅关闭

func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

// 初始化组件
db := initDB()
rdb := initRedis()
srv := initHTTPServer()
consumer := initKafkaConsumer()

go srv.ListenAndServe()
go consumer.Start()

<-ctx.Done()
log.Println("收到信号,开始关闭...")

shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// 按依赖顺序关闭(先关入口,再关下游)
var wg sync.WaitGroup

// 1. 停止接受新请求
wg.Add(1)
go func() {
defer wg.Done()
srv.Shutdown(shutdownCtx)
log.Println("HTTP Server 已关闭")
}()

// 2. 停止消费者
wg.Add(1)
go func() {
defer wg.Done()
consumer.Close()
log.Println("Kafka Consumer 已关闭")
}()

wg.Wait()

// 3. 最后关闭基础设施连接
rdb.Close()
log.Println("Redis 已关闭")

sqlDB, _ := db.DB()
sqlDB.Close()
log.Println("数据库已关闭")

log.Println("所有组件已安全关闭")
}

gRPC 优雅关闭

func main() {
grpcServer := grpc.NewServer()
pb.RegisterXxxServer(grpcServer, &server{})

lis, _ := net.Listen("tcp", ":50051")
go grpcServer.Serve(lis)

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit

// GracefulStop: 停止接受新 RPC,等待正在处理的完成
grpcServer.GracefulStop()
}

关闭顺序

推荐关闭顺序
  1. 停止接受新请求(健康检查返回非健康,K8s 开始摘流量)
  2. 等待 in-flight 请求完成(HTTP Shutdown / gRPC GracefulStop)
  3. 关闭消费者(停止消费消息队列)
  4. 关闭数据库和缓存连接
  5. 退出进程

常见面试问题

Q1: SIGTERM 和 SIGKILL 的区别?

答案

  • SIGTERM:可被捕获,程序可以做清理工作后退出
  • SIGKILL:不可捕获,操作系统直接杀进程
  • K8s 先发 SIGTERM,等 terminationGracePeriodSeconds(默认 30s)后发 SIGKILL

Q2: Shutdown 超时了怎么办?

答案:Context 超时后 Shutdown 返回错误,此时可以 log.Fatal 强制退出,或记录哪些请求未完成作为告警信息。

相关链接