Go HTTP 服务优雅关闭:先拒绝新请求,再等待旧请求
背景 发布时立即退出进程,会中断正在处理的请求;无限等待又会超出编排系统给的终止窗口。退出应该有顺序和总预算。 核心判断 先标记不可就绪,并按端点传播延迟安排排空。 收到信号后使用新的带超时 context 调用 Shutdown。 HTTP 排空后再等待后台任务,超时策略和强制关闭必须显式记录。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 signalCtx, stop := signal.NotifyContext( context.Background(), os.Interrupt, syscall.SIGTERM, ) defer stop() <-signalCtx.Done() // ready 是探针读取的 atomic.Bool;摘流量传播等待按部署环境安排。 ready.Store(false) shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() if err := server.Shutdown(shutdownCtx); err != nil { slog.Error("HTTP drain failed", "error", err) _ = server.Close() } // 在总退出预算内,另行停止并等待后台任务。 边界与误区 信号 context 收到 SIGTERM 后已经取消,不能直接拿它执行排空。Shutdown 不负责回收 hijacked/WebSocket 连接;主协程也必须等待关闭流程结束。 验证方式 同时发送短请求、长请求和 WebSocket 再终止服务,检查完成率与退出时长。ListenAndServe 返回 ErrServerClosed 属于正常关闭,不应记成启动失败。 小结 readiness 先摘流量,Shutdown 再等待连接和后台任务,并设置明确上限。