--- tags: [后端, Go, Gin, 优雅停止, 部署] create time: 2026-04-28 00:00 --- # 优雅停止与重启 ## 概述 生产环境的 Go 服务不能简单地 `kill -9`——正在处理的请求可能会丢失数据、数据库事务可能中断。Gin 基于 `http.Server.Shutdown()` 实现了优雅的停机流程:停止接受新请求,等待已有请求处理完毕后再退出。 思考题:如果一个长时间运行的 WebSocket 连接一直不关闭,`server.Shutdown()` 会因为等它而永远卡住吗?怎么解决? ## 正文 ### 1. 信号监听 + `Shutdown()` 这是最常见的优雅停止模式: ```go func main() { r := gin.Default() r.GET("/health", healthCheck) r.GET("/api/users", listUsers) srv := &http.Server{ Addr: ":8080", Handler: r, } // 在后台 goroutine 中启动服务 go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("listen failed: %v", err) } }() // 监听终止信号 quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit // 阻塞直到收到信号 log.Println("shutting down...") // 创建一个带超时的 context,给请求处理留出时间 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // 停止接收新连接,等待活跃连接完成 if err := srv.Shutdown(ctx); err != nil { log.Fatalf("server forced to shutdown: %v", err) } log.Println("server exited properly") } ``` **流程图:** ```mermaid flowchart TD A["服务正常运行"] -->|"收到 SIGINT/SIGTERM"| B["停止 Accept 新连接"] B --> C["已有请求继续处理"] C --> D{"所有请求完成?"} D -->|"是"| E["srv.Shutdown 返回 ✅"] D -->|"否, 超时"| F["强制退出 ⚠️"] style E fill:#e8f5e9,stroke:#2e7d32 style F fill:#fff3e9,stroke:#e65100 ``` ### 2. Shutdown 内部发生了什么 ``` signal.Notify 捕获 SIGTERM ↓ srv.Shutdown(ctx) 调用 ↓ 1. StopListener.Accept() — 不再接受新连接 ↓ 2. 遍历所有活跃连接 ├─ 没有活跃请求 → 立即关闭 └─ 有活跃请求 → 等待 context 超时 ├─ 请求在超时内完成 → 正常关闭连接 └─ 超时未完成 → 强制关闭连接 ``` **关键细节:** - `Shutdown()` 是同步阻塞调用——必须放在 goroutine 中,否则会卡死主流程 - 超时期间**不会断开**已有连接的 TCP socket——只是不在上面调度新请求 - 已经分配给请求的 Context(`c.Request.Context()`)不会被取消 ### 3. 长连接的处理问题 像 WebSocket、SSE(Server-Sent Events)这类长连接不会因为 `Shutdown()` 而自动关闭: ```go func cleanupLongConnections(srv *http.Server) { // Server 没有内置清理长连接的方法 // 需要自己在中间件中跟踪活跃连接,Shutdown 时主动关闭 } ``` **解决方案:** ```go type ConnectionManager struct { conns map[*websocket.Conn]bool mu sync.RWMutex } func (m *ConnectionManager) CloseAll() { m.mu.Lock() defer m.mu.Unlock() for conn := range m.conns { conn.Close() // 主动关闭每个 WebSocket } m.conns = make(map[*websocket.Conn]bool) } ``` 在 Shutdown 流程中调用: ```go // 1. 先关闭所有长连接 wsManager.CloseAll() // 2. 再等 HTTP 请求处理完 ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() srv.Shutdown(ctx) ``` ### 4. 优雅重启(fork + exec) 某些场景下不只是"停",还需要"换"——比如更新了二进制文件。经典的 Unix 优雅重启模式: ``` 旧进程接收 SIGHUP ↓ 1. 启动新进程 ↓ 2. 旧进程等待新进程就绪 ↓ 3. 旧进程 Shutdown(等现有请求完成) ↓ 4. 新进程接管端口 ↓ 5. 旧进程退出,新进程继续服务(无感知的版本切换) ``` ```go func gracefulRestart() error { args := os.Args[1:] args = append([]string{"-fork"}, args...) cmd := exec.Command(os.Args[0], args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { return err } // 等待新进程就绪(简单做法:轮询 health endpoint) time.Sleep(2 * time.Second) // 旧进程开始优雅关停 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() return server.Shutdown(ctx) } ``` > **实际建议:** 大多数现代部署(Kubernetes Docker)使用滚动发布(Rolling Update),不需要自己实现 fork-restart。SIGTERM 信号已由 K8s 自动处理。 ### 5. Kubernetes 中的优雅停止 K8s 的优雅停止默认给予 30 秒: ```yaml spec: template: spec: terminationGracePeriodSeconds: 30 # 秒,默认 30 ``` K8s 行为: 1. 发送 `SIGTERM` 给 Pod 主进程 2. 开始倒计时 3. 倒数为 0 时发送 `SIGKILL`(强制杀) 你的 Go 服务需要在 SIGTERM 后 30 秒内完成 `Shutdown()`,否则会被强杀。 **最佳实践清单:** | 事项 | 说明 | |------|------| | 设置合理的 `WriteTimeout` | 防止某个慢请求拖垮整个关机过程 | | 提前关闭长连接 | WebSocket/SSE 不会自动关 | | 设置 `context.WithTimeout` | 比 K8s deadline 略早一点,留缓冲 | | 记录关闭日志 | 确认关机是否正常完成 | ## 关联笔记 - [[GIN/12-server-config]] — http.Server 的配置和启动方式 - [[GIN/15-advanced-running]] — 多服务运行和特殊场景 - [[部署与运维基础]] — K8s 滚动发布与优雅停机