17370845950

Golang RPC服务如何优雅关闭_Golang RPC平滑下线方案
GracefulStop() 不能直接调用,因其阻塞等待所有连接和 handler 自然退出,若 handler 未监听 ctx.Done() 或卡死则永久阻塞;生产中需封装超时逻辑并显式关闭 listener。

gRPC-Go 的 GracefulStop() 为什么不能直接调用?

因为 GracefulStop() 是阻塞的,且依赖内部状态同步——它必须等所有活跃连接自然关闭、所有 handler goroutine 退出后才返回。如果你在信号处理里直接调用它,而某个 handler 正卡在无 context 控制的 select{} 或数据库死锁中,整个关闭流程就会卡住,服务“关不掉”。

  • 它内部会设置 s.drain = true 阻止新连接,再逐个 Close() listener,最后 Wait() 所有连接和 handler
  • 若你没在 handler 中监听 ctx.Done()(比如用 grpc.ServerStream.Context()),连接就不会主动退出,GracefulStop() 就永远等不到 len(s.conns) == 0
  • 它不提供超时机制,也没有 fallback 路径,生产环境必须自己包一层带 deadline 的封装

HTTP Server 和 gRPC Server 关闭逻辑本质相同

别被协议层迷惑:gRPC over HTTP/2,它的优雅关闭底层逻辑和 http.Server.Shutdown() 一致——都是「拒新、容旧、限时、兜底」。

  • 两者都需在主 goroutine 异步启动服务(不能 srv.Serve()srv.ListenAndServe() 阻塞 main)
  • 都必须用 signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 监听信号,而不是只捕获 Ctrl+C
  • 都必须用带 timeout 的 context.WithTimeout() 包裹关闭动作,例如 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  • gRPC 没原生 Shutdown(),但你可以用 GracefulStop() + time.AfterFunc() 模拟等效行为

常见崩溃场景:端口被占用、连接泄漏、K8s readiness probe 失败

这些不是“关得不够优雅”,而是关得“太晚”或“根本没关干净”。典型表现是重启时报 address already in use,或 Prometheus 显示 grpc_server_handled_total 突降但连接数不归零。

  • 忘记关闭 listener:仅调用 grpcServer.Stop() 不等于关闭 net.Listener,要手动 ln.Close()
  • 没等 handler 结束就退出:handler 里开了 goroutine 去写 DB 或发消息,但没用 sync.WaitGroupcontext 管理生命周期
  • K8s 场景下 readiness probe 还在成功,但 server 已进入 drain 状态,流量继续打入,导致请求失败——必须配合 probe 接口返回 503 Service Unavailables.drain == true
  • Docker stop 默认只给 10 秒,超时就 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 一家的事。