实现优雅关闭
问题
如何让 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()
}
关闭顺序
推荐关闭顺序
- 停止接受新请求(健康检查返回非健康,K8s 开始摘流量)
- 等待 in-flight 请求完成(HTTP Shutdown / gRPC GracefulStop)
- 关闭消费者(停止消费消息队列)
- 关闭数据库和缓存连接
- 退出进程
常见面试问题
Q1: SIGTERM 和 SIGKILL 的区别?
答案:
SIGTERM:可被捕获,程序可以做清理工作后退出SIGKILL:不可捕获,操作系统直接杀进程- K8s 先发 SIGTERM,等
terminationGracePeriodSeconds(默认 30s)后发 SIGKILL
Q2: Shutdown 超时了怎么办?
答案:Context 超时后 Shutdown 返回错误,此时可以 log.Fatal 强制退出,或记录哪些请求未完成作为告警信息。