在 Go 语言中,优雅停止 HTTP 服务的关键在于使用 http.Server 的 Shutdown() 方法,并结合操作系统信号监听(SIGINT/SIGTERM)。其核心目的是:不再接收新请求,但等待已接收的正在处理的请求完成,然后干净地释放资源。
以下是最标准、最优雅的实现方案,并附带了生产环境必须考虑的细节。
1. 标准优雅停机模板(核心代码)
package mainimport ("context""log""net/http""os""os/signal""syscall""time"
)func main() {// 1. 初始化路由和 Server 对象(注意配置超时参数)mux := http.NewServeMux()mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {// 模拟耗时操作(如数据库查询)time.Sleep(2 * time.Second)w.Write([]byte("Hello, Graceful!"))})srv := &http.Server{Addr: ":8080",Handler: mux,ReadTimeout: 5 * time.Second, // 读取请求的最大时长WriteTimeout: 10 * time.Second, // 写入响应的最大时长(防止慢客户端)IdleTimeout: 120 * time.Second, // 空闲长连接保持时间}// 2. 在 goroutine 中启动服务(因为 ListenAndServe 是阻塞的)go func() {log.Printf("Server starting on %s", srv.Addr)if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {log.Fatalf("Server startup failed: %v", err)}}()// 3. 监听系统信号(Ctrl+C 或 kill 命令)quit := make(chan os.Signal, 1)signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)<-quit // 阻塞等待信号log.Println("Received shutdown signal, gracefully shutting down...")// 4. 核心:执行优雅关闭// 设置超时时间(如果超过该时间请求仍未完成,则强制断开)ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)defer cancel()if err := srv.Shutdown(ctx); err != nil {log.Fatalf("Server shutdown failed: %v", err)}// 如果 Shutdown 返回 nil,说明所有连接已安全关闭log.Println("Server gracefully stopped")
}
2. 让“优雅”更进一步:生产级进阶要点
代码写好只是基础,真正的“优雅”还需处理以下四个细节:
① 设置合理的 WriteTimeout 和 IdleTimeout
WriteTimeout:必须设置!如果客户端下载慢,或处理函数死锁,Shutdown会因等待这些连接完成而卡死。设置WriteTimeout后,超时的连接会被强制关闭,确保Shutdown能在指定时间内返回。IdleTimeout:对 Keep-Alive 长连接有效。如果不设置,大量空闲长连接会拖慢Shutdown过程(即使它们没有活跃请求),设置后服务器会主动关闭空闲连接。
② 处理 Shutdown 超时(二级熔断)
上面的 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 是保底策略。如果 10 秒后请求仍未结束,Shutdown 会返回 context.DeadlineExceeded 错误。
此时你需要决定:
- 直接退出(强行终止进程,适用于非关键任务)。
- 记录报警并重试关闭(更推荐),代码如下:
if err := srv.Shutdown(ctx); err != nil {// 超时后,可以尝试记录当前活跃连接数,或直接强制退出log.Printf("Graceful shutdown timed out, forcing exit: %v", err)// 这里可以调用 os.Exit(1) 或 再次调用 Shutdown(context.Background())// 但通常直接 os.Exit 即可,因为超时意味着业务逻辑有 bug,保留进程可能导致僵尸状态os.Exit(1)
}
③ 负载均衡环境下的“流量摘除”(预热与摘除)
如果在 Kubernetes 或 Nginx 后端,最优雅的做法是在收到信号时先摘除健康检查端点,再执行 Shutdown。
// 在收到信号时,先将 /health 返回 503(服务不可用)
// 等待几秒(如 5s)让负载均衡器感知到节点不健康,再执行 srv.Shutdown
time.Sleep(5 * time.Second)
// 然后再执行上面的 srv.Shutdown(ctx)
④ 利用 net.Listener 实现更精细的控制(可选)
如果需要“拒绝新连接但不关闭监听端口”的效果,可以不使用 Shutdown,改用 Listener.Close() + 等待连接数归零。但标准库的 Shutdown 已经封装了这些逻辑,除非有极特殊需求,否则直接用 Shutdown 即可。
3. 常见误区(避坑指南)
| 误区操作 | 后果 | 正确做法 |
|---|---|---|
直接 os.Exit(0) 或 log.Fatal |
正在处理中的请求被强制截断,客户端收到 RST 包报错。 |
必须使用 srv.Shutdown。 |
在 Shutdown 后未设置超时 |
如果某个请求死锁或阻塞,进程将永远挂起无法退出。 | 必须传入带 timeout 的 Context。 |
signal.Notify 不指定通道缓冲 |
信号可能丢失。 | 声明为 chan os.Signal 并设置缓冲 1。 |
忽略 http.ErrServerClosed |
当 Shutdown 调用后,ListenAndServe 会返回该错误,如果不判断,启动日志会误报错。 |
在启动 goroutine 中必须判断 err != http.ErrServerClosed。 |
4. 第三方库的替代方案(不再推荐)
早期 Go 流行使用 tylerb/graceful 或 facebookgo/grace。但自 Go 1.8 标准库引入 Shutdown 后,这些库已失去必要性。标准库的实现更可靠,且无额外依赖,请务必使用标准库方案。
总结:基于 http.Server.Shutdown + 信号监听 + 合理设置超时参数,就是 Go 语言中最优雅、最标准的停服方式。只需一个模板,即可满足 99% 的生产环境需求。
