This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hzh/GIN/13-graceful-shutdown.md
T

209 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 滚动发布与优雅停机