GracefulStop() 不能直接调用,因其阻塞等待所有连接和 handler 自然退出,若 handler 未监听 ctx.Done() 或卡死则永久阻塞;生产中需封装超时逻辑并显式关闭 listener。
GracefulStop() 为什么不能直接调用?因为 GracefulStop() 是阻塞的,且依赖内部状态同步——它必须等所有活跃连接自然关闭、所有 handler goroutine 退出后才返回。如果你在信号处理里直接调用它,而某个 handler 正卡在无 context 控制的 select{} 或数据库死锁中,整个关闭流程就会卡住,服务“关不掉”。
s.drain = true 阻止新连接,再逐个 Close() listener,最后 Wait() 所有连接和 handlerctx.Done()(比如用 grpc.ServerStream.Context()),连接就不会主动退出,GracefulStop() 就永远等不到 len(s.conns) == 0
别被协议层迷惑:gRPC over HTTP/2,它的优雅关闭底层逻辑和 http.Server.Shutdown() 一致——都是「拒新、容旧、限时、兜底」。
srv.Serve() 或 srv.ListenAndServe() 阻塞 main)signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 监听信号,而不是只捕获 Ctrl+C
context.WithTimeout() 包裹关闭动作,例如 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
Shutdown(),但你可以用 GracefulStop() + time.AfterFunc() 模拟等效行为这些不是“关得不够优雅”,而是关得“太晚”或“根本没关干净”。典型表现是重启时报 address already in use,或 Prometheus 显示 grpc_server_handled_total 突降但连接数不归零。
grpcServer.Stop() 不等于关闭 net.Listener,要手动 ln.Close()
sync.WaitGroup 或 context 管理生命周期503 Service Unavailable 当 s.drain == true
kill -9,所以你的 graceful shutdown 必须比这个更短(建议 ≤7s)不用框架,纯标准库,覆盖信号监听、超时控制、listener 清理、handler 协作。
// 启动
ln, _ := net.Listen("tcp", ":9000")
s := grpc.NewServer()
go s.Serve(ln) // 异步
// 关闭
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 7*time.Second)
defer cancel()
// 主动触发 graceful stop
go func() {
<-ctx.Done()
if ctx.Err() == context.DeadlineExceeded {
log.Println("forced shutdown: timeout reached")
s.Stop() // 强制终止,丢弃未完成连接
}
}()
s.GracefulStop() // 尝试优雅停止
ln.Close() // 显式关 listen
er,避免 fd 泄漏
真正难的不是写这几行,而是确保每个 handler 都检查 ctx.Err()、每个 goroutine 都受 context 约束、每个外部资源(DB conn、redis pool)都有 close hook。平滑下线,从来不是 server 一家的事。